Skip to main content

Stripe Billing vs Chargebee vs Recurly: SaaS Subscriptions 2026

·PkgPulse Team

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

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.