Skip to main content

Stripe Billing vs Chargebee vs Recurly

·PkgPulse Team
0

Stripe Billing vs Chargebee vs Recurly: SaaS Subscriptions 2026

TL;DR

Recurring subscription billing is more complex than one-time payments — you need trials, proration when plans change, dunning for failed payments, tax compliance, and revenue reporting. Stripe Billing is the developer-first default — deep API control, excellent TypeScript SDK, Customer Portal for self-serve plan management, and tight integration with the rest of the Stripe ecosystem (Radar, Checkout, Connect). Chargebee is the subscription lifecycle specialist — handles dunning, revenue recovery, and complex pricing models with pre-built workflows that save engineering time; better for non-technical teams managing billing. Recurly is the enterprise subscription platform — advanced metered billing, hierarchical accounts, contract-based billing, and deep reporting for high-volume B2B. For developer-built SaaS products: Stripe Billing. For products where non-engineers manage billing logic: Chargebee. For enterprise B2B with complex metering and contracts: Recurly.

Key Takeaways

  • Stripe Billing has the best developer API — TypeScript SDK, webhook events, test mode with 100% feature parity
  • Chargebee has built-in dunning flows — automated retry sequences, email campaigns, and payment update flows without code
  • Recurly supports hierarchical accounts — parent/child account structures for resellers and enterprise
  • All three integrate with Stripe's payment methods — cards, ACH, SEPA, and 50+ payment methods
  • Stripe Customer Portal — self-serve plan upgrades/downgrades/cancellations with zero code
  • Chargebee's revenue recovery — typically 4-8% lift in recovered revenue from failed payments
  • Metered billing is supported by all three — usage-based pricing for API products

The Subscription Billing Stack

One-time payments:  Stripe Checkout / PaymentIntent
Subscriptions:      Stripe Billing / Chargebee / Recurly
  - Trial management
  - Proration (mid-cycle plan changes)
  - Dunning (failed payment recovery)
  - Invoicing + tax calculation
  - Customer Portal (self-serve plan management)
  - Revenue reporting (MRR, ARR, churn)

Stripe Billing: Developer-First Subscriptions

Stripe Billing extends Stripe with subscription primitives: Products, Prices, Subscriptions, and Invoices. If you're already on Stripe for payments, Billing integrates directly.

Installation

npm install stripe

Create Products and Prices

import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Create a product (represents your SaaS plan)
const product = await stripe.products.create({
  name: "Pro Plan",
  description: "Unlimited projects, 10 team members, priority support",
  metadata: {
    features: "unlimited_projects,team_collaboration,priority_support",
  },
});

// Create a monthly price for the product
const monthlyPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 2900,          // $29.00/month
  currency: "usd",
  recurring: {
    interval: "month",
    interval_count: 1,
  },
  lookup_key: "pro_monthly",  // Stable reference across price updates
});

// Annual price with discount
const annualPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 29000,         // $290/year (~17% discount)
  currency: "usd",
  recurring: {
    interval: "year",
  },
  lookup_key: "pro_annual",
});

Create a Subscription with Trial

// Create a customer first
const customer = await stripe.customers.create({
  email: user.email,
  name: user.name,
  metadata: { userId: user.id },
});

// Attach payment method
await stripe.paymentMethods.attach(paymentMethodId, {
  customer: customer.id,
});

await stripe.customers.update(customer.id, {
  invoice_settings: { default_payment_method: paymentMethodId },
});

// Create subscription with 14-day trial
const subscription = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: monthlyPrice.id }],
  trial_period_days: 14,
  payment_settings: {
    payment_method_types: ["card"],
    save_default_payment_method: "on_subscription",
  },
  expand: ["latest_invoice.payment_intent"],
});

// Store subscription ID in your database
await db.users.update({
  where: { id: user.id },
  data: {
    stripeCustomerId: customer.id,
    stripeSubscriptionId: subscription.id,
    subscriptionStatus: subscription.status,
  },
});

Plan Upgrade with Proration

// Upgrade from monthly to annual (mid-cycle proration)
async function upgradeSubscription(userId: string, newPriceId: string) {
  const user = await db.users.findUnique({ where: { id: userId } });
  const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId);

  const updatedSubscription = await stripe.subscriptions.update(
    subscription.id,
    {
      items: [{
        id: subscription.items.data[0].id,
        price: newPriceId,
      }],
      proration_behavior: "create_prorations",  // Charge immediately for upgrade
      // proration_behavior: "none" — no proration, start fresh next cycle
    }
  );

  return updatedSubscription;
}

Customer Portal (Self-Serve)

// Create a billing portal session — zero UI code needed
async function createPortalSession(userId: string, returnUrl: string) {
  const user = await db.users.findUnique({ where: { id: userId } });

  const session = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: returnUrl,
  });

  return session.url;
  // Customer can: view invoices, update payment method, upgrade/downgrade, cancel
}

Webhook Handling

import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature")!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  switch (event.type) {
    case "customer.subscription.created":
    case "customer.subscription.updated": {
      const subscription = event.data.object as Stripe.Subscription;
      await db.users.updateMany({
        where: { stripeCustomerId: subscription.customer as string },
        data: {
          subscriptionStatus: subscription.status,
          currentPeriodEnd: new Date(subscription.current_period_end * 1000),
          planId: subscription.items.data[0].price.id,
        },
      });
      break;
    }

    case "customer.subscription.deleted": {
      const subscription = event.data.object as Stripe.Subscription;
      await db.users.updateMany({
        where: { stripeCustomerId: subscription.customer as string },
        data: { subscriptionStatus: "canceled", planId: null },
      });
      break;
    }

    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      // Notify user of failed payment
      await sendPaymentFailedEmail(invoice.customer_email ?? "");
      break;
    }

    case "invoice.payment_succeeded": {
      // Extend subscription access
      break;
    }
  }

  return NextResponse.json({ received: true });
}

Metered Billing (Usage-Based)

// Report usage for a metered subscription
await stripe.subscriptionItems.createUsageRecord(
  subscriptionItemId,
  {
    quantity: apiCallsThisHour,
    timestamp: "now",
    action: "increment",  // "set" for absolute, "increment" for additive
  }
);

Chargebee: Subscription Lifecycle Management

Chargebee wraps subscription billing with pre-built workflows — dunning, revenue recovery, and complex pricing logic without writing business logic yourself.

Installation

npm install chargebee

Creating a Subscription

import chargebee from "chargebee";

chargebee.configure({
  site: process.env.CHARGEBEE_SITE!,
  api_key: process.env.CHARGEBEE_API_KEY!,
});

// Create customer + subscription in one call
const result = await chargebee.subscription.create_with_items("pro-monthly-usd", {
  customer: {
    email: user.email,
    first_name: user.name.split(" ")[0],
    last_name: user.name.split(" ")[1] ?? "",
  },
  subscription_items: [{
    item_price_id: "pro-monthly-usd",
    unit_amount: 2900,
  }],
  trial_end: Math.floor((Date.now() + 14 * 24 * 60 * 60 * 1000) / 1000),
  payment_method: {
    type: "card",
    gateway_account_id: "your-gateway-id",
    tmp_token: cardToken,
  },
}).request();

const subscription = result.subscription;
const customer = result.customer;

Dunning Configuration (No-Code)

Chargebee's dunning is configured in the dashboard:

Dunning Schedule Example:
  Day 0:  Payment fails → Retry immediately
  Day 3:  Retry again + send "Your payment failed" email
  Day 7:  Retry + send "Update your payment method" email
  Day 14: Retry + send "Final warning" email
  Day 21: Cancel subscription + send cancellation confirmation

vs Stripe: You write this logic yourself in webhooks
// Chargebee handles retries — you just handle the final events
app.post("/chargebee-webhook", async (req, res) => {
  const event = req.body;

  switch (event.event_type) {
    case "subscription_activated":
      await db.users.update({ ... });
      break;

    case "subscription_cancelled":
      // Chargebee has already done 4 retries over 21 days
      await downgradeUser(event.content.subscription.customer_id);
      break;

    case "payment_failed":
      // Chargebee is managing retries — just log
      await logPaymentFailure(event);
      break;
  }

  res.json({ received: true });
});

Hosted Checkout and Portal

// Generate a Chargebee-hosted checkout page
const checkoutResult = await chargebee.hosted_page
  .checkout_new_for_items({
    subscription_items: [{
      item_price_id: "pro-monthly-usd",
    }],
    customer: {
      email: user.email,
    },
    redirect_url: "https://yourapp.com/checkout/success",
    cancel_url: "https://yourapp.com/checkout/cancel",
  })
  .request();

const checkoutUrl = checkoutResult.hosted_page.url;

Recurly: Enterprise Subscription Billing

Recurly is built for high-volume B2B — hierarchical account structures, complex metering, contract billing, and advanced revenue reporting.

Installation

npm install recurly

Creating an Account and Subscription

import recurly from "recurly";

const client = new recurly.Client(process.env.RECURLY_API_KEY!);

// Create account
const account = await client.createAccount({
  code: `user-${user.id}`,
  email: user.email,
  firstName: user.name.split(" ")[0],
  lastName: user.name.split(" ")[1] ?? "",
  billingInfo: {
    tokenId: billingToken,  // Recurly.js token from frontend
  },
});

// Create subscription
const subscription = await client.createSubscription({
  planCode: "pro-monthly",
  account: { code: account.code },
  currency: "USD",
  trialEndsAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
});

Metered (Add-On) Usage

// Report usage for metered add-ons
await client.createUsage(subscriptionAddOnId, {
  amount: 1,
  merchantTag: `api-call-${Date.now()}`,
  recordingTimestamp: new Date().toISOString(),
  usageTimestamp: new Date().toISOString(),
});

Feature Comparison

FeatureStripe BillingChargebeeRecurly
Developer API quality✅ Best in classGoodGood
Built-in dunningBasic (3 retries)✅ Advanced workflows✅ Smart Retry
Customer portal✅ No-code✅ No-code✅ No-code
Metered billing
Tax automation✅ (Stripe Tax)✅ (EU VAT)✅ (AvaTax integration)
Hierarchical accountsPartial
Revenue recoveryBasic✅ 4-8% lift
TypeScript SDK✅ Official
Pricing0.5-0.8% + Stripe fees$299/month+$199/month+
Global payment methods✅ 50+ via StripeLimitedLimited
Hosted checkout✅ Stripe Checkout
GitHub stars4k (SDK)N/AN/A

When to Use Each

Choose Stripe Billing if:

  • You're already on Stripe for one-time payments and want unified billing
  • Developer control over every aspect of billing logic is important
  • You need the widest global payment method support
  • Your team can write webhook handlers for subscription events
  • Pricing flexibility matters — 0.5% is cheaper than flat monthly fees at small scale

Choose Chargebee if:

  • Non-engineering teams (growth, finance) need to manage billing configuration
  • Advanced dunning workflows and automated payment recovery are priorities
  • You want pre-built revenue metrics dashboards (MRR, ARR, churn, LTV)
  • Mid-market SaaS product where billing complexity exceeds what you want to build

Choose Recurly if:

  • Enterprise B2B with complex pricing (usage-based, contract-based, volume tiers)
  • Hierarchical account structures (resellers, agencies, sub-accounts)
  • You need enterprise revenue reporting and financial integrations (NetSuite, Salesforce)
  • High-volume billing where dedicated enterprise support matters

Methodology

Data sourced from official Stripe Billing documentation (stripe.com/docs/billing), Chargebee documentation (chargebee.com/docs), Recurly documentation (recurly.com/developers), pricing pages as of February 2026, and community reviews from r/SaaS, Indie Hackers, and Patrick McKenzie's writing on SaaS billing. Revenue recovery statistics from Chargebee and Recurly published case studies.


Related: Polar vs Paddle vs Gumroad for developer-focused monetization platforms at smaller scale, or Resend vs SendGrid vs Brevo for sending the transactional emails that accompany subscription events.

Comments

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.