# How to Build a SaaS Billing System: Complete Guide (2026)

> Learn how to build a SaaS billing system with Stripe, handle subscriptions, usage-based billing, tax compliance, and payment recovery. A complete implementation guide for SaaS developers in 2026.

Source: https://designrevision.com/blog/saas-billing-system

---

Every SaaS product eventually needs to charge money. The billing system you build determines whether revenue flows smoothly or leaks through proration bugs, failed payment gaps, and tax compliance oversights.

Building a SaaS billing system is not just "add Stripe and ship." It involves choosing the right billing model, implementing subscription lifecycle management, handling upgrades and downgrades without revenue loss, managing tax compliance across jurisdictions, recovering failed payments, and making the entire system reliable enough that you never accidentally lose a paying customer.

This guide walks through the complete SaaS billing system implementation. You will choose a payment processor, implement subscription billing with Stripe in Next.js, set up usage-based metering, handle tax compliance, build dunning flows for failed payments, and avoid the billing mistakes that cost SaaS companies 10 to 20 percent of their revenue.
## Key Takeaways

> If you remember nothing else:
>
> * **Stripe** is the best choice for most SaaS billing systems. The 2.9% + $0.30 transaction fee plus 0.7% Stripe Billing fee is worth the control and flexibility you get
> * **Paddle** or **Lemon Squeezy** eliminate tax compliance headaches by acting as your Merchant of Record, but charge 5%+ per transaction
> * Always handle billing events through **webhooks**, not client-side callbacks. Webhooks are the source of truth for subscription state
> * **Proration** is where most SaaS billing systems break. Use Stripe's built-in proration and always preview amounts before confirming plan changes
> * Implement **dunning** from day one. Failed payment recovery can save 20-40% of otherwise-churned revenue
> * Never build billing from scratch. Stripe Billing costs 0.7% of revenue and saves 200-500 hours of engineering time

## Table of Contents

1. [Choosing Your Payment Stack](#choosing-your-payment-stack)
2. [Subscription Billing Models](#subscription-billing-models)
3. [Setting Up Stripe Billing in Next.js](#setting-up-stripe-billing-in-nextjs)
4. [Handling the Subscription Lifecycle](#handling-the-subscription-lifecycle)
5. [Usage-Based Billing](#usage-based-billing)
6. [Tax Compliance](#tax-compliance)
7. [Free Trials That Convert](#free-trials-that-convert)
8. [Dunning and Payment Recovery](#dunning-and-payment-recovery)
9. [Common Billing Mistakes](#common-billing-mistakes)
10. [The SaaS Billing Checklist](#the-saas-billing-checklist)
11. [Conclusion](#conclusion)

## Choosing Your Payment Stack

The first decision for any SaaS billing system is the payment processor. Three options dominate in 2026, and each targets a different level of complexity.

| Feature | <a href="https://stripe.com" rel="nofollow">Stripe</a> | <a href="https://paddle.com" rel="nofollow">Paddle</a> | <a href="https://lemonsqueezy.com" rel="nofollow">Lemon Squeezy</a> |
|---------|--------|--------|---------------|
| **Best for** | Custom billing, global scale | Tax compliance, hands-off | Simple SaaS, creators |
| **Pricing** | 2.9% + $0.30 + 0.7% Billing | 5% + $0.50 | ~5% + $0.50 |
| **Tax handling** | Manual or Stripe Tax add-on | Automatic (Merchant of Record) | Automatic (Merchant of Record) |
| **Customization** | Full API control | Limited checkout | Limited checkout |
| **Subscription management** | Complete (Billing) | Built-in | Built-in |
| **Usage-based billing** | Meter API, usage records | Limited | Limited |
| **Developer experience** | Excellent docs, SDKs | Good docs | Basic docs |

**Stripe** gives you complete control over every billing detail. You own the customer relationship, build custom checkout flows, and handle complex pricing models. The trade-off is that you manage tax compliance yourself (or pay for Stripe Tax).

**Paddle** acts as your Merchant of Record. It handles sales tax, VAT, and compliance in 200+ countries. You never file a tax return related to software sales. The trade-off is higher fees and less checkout customization.

**Lemon Squeezy** offers a similar Merchant of Record model with slightly simpler tooling. It works well for solo founders and small SaaS products that want automatic tax handling without complex billing logic.

For a detailed pricing breakdown, see our [Stripe vs Paddle comparison](/blog/stripe-vs-paddle) and [Stripe vs Lemon Squeezy comparison](/blog/stripe-vs-lemonsqueezy).

**The recommendation:** Start with Stripe if your SaaS billing system needs custom pricing, usage-based billing, or per-seat models. Switch to Paddle only if international tax compliance becomes a significant operational burden.

## Subscription Billing Models

Your billing model shapes revenue predictability, customer perception of value, and implementation complexity. Choose before writing code.

### Flat-Rate Pricing

One price, unlimited usage. Basecamp charges $299/month for unlimited users and projects.

**When it works:** Products with predictable, consistent value regardless of usage. Simple to implement, simple to communicate, simple to forecast revenue.

**When it breaks:** Customers who use your product heavily subsidize light users. No natural expansion revenue as teams grow.

### Per-Seat Pricing

Price scales with team size. Slack charges $7.25-$12.50 per user per month. Notion charges $8 per member per month.

**When it works:** Products where value increases with each team member. Natural expansion revenue as companies grow. Easy for customers to understand.

**When it breaks:** Teams game the system by sharing accounts. Adding seats feels like a penalty, not an upgrade.

### Usage-Based Pricing

Price scales with consumption. OpenAI charges per token. Twilio charges per API call. Vercel charges per bandwidth.

**When it works:** Products where value directly correlates with usage. Low barrier to entry since customers pay only for what they use. Revenue grows automatically as customers succeed.

**When it breaks:** Revenue is unpredictable month to month. Customers fear bill shock. Metering infrastructure adds complexity.

### Hybrid Pricing

Base subscription fee plus usage-based overages. GitHub charges $4/month per seat with included Actions minutes and storage, then usage-based pricing for overages.

**When it works:** Most SaaS products. Combines revenue predictability from the base fee with growth potential from usage overages. Customers get a predictable baseline cost with transparent scaling.

**The trend in 2026:** Hybrid models dominate. A flat base fee covers infrastructure and support costs, while usage-based components align pricing with the value customers receive. This is the pricing model that most [SaaS starter kits](/blog/best-saas-starter-kits) support out of the box.

## Setting Up Stripe Billing in Next.js

The core SaaS billing system implementation with Stripe involves four components: Checkout Sessions for payment collection, Customer Portal for self-service management, Webhooks for state synchronization, and your database for access control.

### Install and Configure

```bash
npm install stripe @stripe/stripe-js
```

Add environment variables:

```bash
# .env.local
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PORTAL_CONFIG_ID=bpc_...
```

### Create Checkout Sessions

The Checkout Session handles payment collection, tax calculation, and subscription creation in one API call.

```typescript
// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { auth } from '@/auth'

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

export async function POST(req: NextRequest) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { priceId } = await req.json()

  const checkoutSession = await stripe.checkout.sessions.create({
    mode: 'subscription',
    customer_email: session.user.email!,
    line_items: [{ price: priceId, quantity: 1 }],
    automatic_tax: { enabled: true },
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    subscription_data: {
      trial_period_days: 14,
    },
  })

  return NextResponse.json({ url: checkoutSession.url })
}
```

### Set Up the Customer Portal

The Stripe Customer Portal lets customers update payment methods, switch plans, cancel subscriptions, and download invoices without you building any UI.

```typescript
// app/api/stripe/portal/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { auth } from '@/auth'
import { db } from '@/lib/db'

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

export async function POST(req: NextRequest) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const user = await db.user.findUnique({
    where: { id: session.user.id },
  })

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user!.stripeCustomerId,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
  })

  return NextResponse.json({ url: portalSession.url })
}
```

Configure the portal in the <a href="https://dashboard.stripe.com" rel="nofollow">Stripe Dashboard</a> under Settings > Customer Portal. Enable plan switching, cancellation, and invoice history.

### Implement Webhooks

Webhooks are the backbone of your SaaS billing system. Every billing state change flows through webhooks. Never rely on client-side redirects to provision access.

```typescript
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { headers } from 'next/headers'
import { db } from '@/lib/db'

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

export async function POST(req: NextRequest) {
  const body = await req.text()
  const signature = 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 }
    )
  }

  // Idempotency: skip duplicate events
  const existing = await db.webhookEvent.findUnique({
    where: { stripeEventId: event.id },
  })
  if (existing) {
    return NextResponse.json({ received: true })
  }
  await db.webhookEvent.create({
    data: { stripeEventId: event.id, type: event.type },
  })

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session
      await db.user.update({
        where: { email: session.customer_email! },
        data: {
          stripeCustomerId: session.customer as string,
          subscriptionId: session.subscription as string,
          subscriptionStatus: 'active',
        },
      })
      break
    }

    case 'invoice.paid': {
      const invoice = event.data.object as Stripe.Invoice
      await db.user.update({
        where: { stripeCustomerId: invoice.customer as string },
        data: { subscriptionStatus: 'active' },
      })
      break
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice
      await db.user.update({
        where: { stripeCustomerId: invoice.customer as string },
        data: { subscriptionStatus: 'past_due' },
      })
      // Trigger dunning email
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription
      await db.user.update({
        where: { subscriptionId: subscription.id },
        data: {
          subscriptionStatus: 'canceled',
          subscriptionId: null,
        },
      })
      break
    }
  }

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

**Critical rules for webhook handlers:**

- Always verify the signature before processing
- Store the event ID and skip duplicates (idempotency)
- Return a 200 response quickly, then process asynchronously for heavy operations
- Handle all subscription lifecycle events, not just the happy path
- Test locally with `stripe listen --forward-to localhost:3000/api/stripe/webhook`

## Handling the Subscription Lifecycle

The subscription lifecycle is where most SaaS billing systems get complicated. Upgrades, downgrades, cancellations, and reactivations all need specific handling.

### Plan Upgrades

When a customer upgrades, apply the change immediately and prorate the cost.

```typescript
// lib/billing.ts
export async function upgradePlan(
  subscriptionId: string,
  newPriceId: string
) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId)

  return stripe.subscriptions.update(subscriptionId, {
    items: [{
      id: subscription.items.data[0].id,
      price: newPriceId,
    }],
    proration_behavior: 'create_prorations',
  })
}
```

Stripe automatically credits the unused portion of the current plan and charges the prorated cost of the new plan.

### Plan Downgrades

When a customer downgrades, schedule the change for the end of the current billing period. This avoids issuing refunds and keeps revenue for the period the customer already paid for.

```typescript
export async function downgradePlan(
  subscriptionId: string,
  newPriceId: string
) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId)

  return stripe.subscriptions.update(subscriptionId, {
    items: [{
      id: subscription.items.data[0].id,
      price: newPriceId,
    }],
    proration_behavior: 'none',
    billing_cycle_anchor: 'unchanged',
  })
}
```

### Proration Preview

Always show customers what they will pay before confirming a plan change.

```typescript
export async function previewProration(
  customerId: string,
  subscriptionId: string,
  newPriceId: string
) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId)

  const invoice = await stripe.invoices.retrieveUpcoming({
    customer: customerId,
    subscription: subscriptionId,
    subscription_items: [{
      id: subscription.items.data[0].id,
      price: newPriceId,
    }],
    subscription_proration_behavior: 'create_prorations',
  })

  return {
    amountDue: invoice.amount_due,
    prorationCredit: invoice.lines.data
      .filter(line => line.proration)
      .reduce((sum, line) => sum + line.amount, 0),
  }
}
```

### Cancellations

Cancel at the end of the billing period rather than immediately. This gives you a window to win the customer back.

```typescript
export async function cancelSubscription(subscriptionId: string) {
  return stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true,
  })
}
```

Handle the `customer.subscription.deleted` webhook to revoke access when the period ends.

## Usage-Based Billing

Usage-based billing lets you charge customers for what they consume. This model works for API platforms, AI tools, storage services, and any product where value scales with usage.

### Setting Up Metered Billing

Create a Meter in the <a href="https://dashboard.stripe.com" rel="nofollow">Stripe Dashboard</a> to define what you are measuring (API calls, tokens, storage GB). Then create a price that references the Meter.

### Reporting Usage

Track usage in your application and report it to Stripe:

```typescript
// lib/usage.ts
import Stripe from 'stripe'

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

export async function reportUsage(
  customerId: string,
  quantity: number
) {
  await stripe.billing.meterEvents.create({
    event_name: 'api_calls',
    payload: {
      stripe_customer_id: customerId,
      value: String(quantity),
    },
  })
}
```

Call this function whenever a billable event occurs in your application. Stripe aggregates the usage and includes it on the customer's next invoice.

### Usage Alerts

Prevent bill shock by setting up usage alerts:

```typescript
export async function createUsageAlert(
  subscriptionItemId: string,
  threshold: number
) {
  await stripe.subscriptionItems.update(subscriptionItemId, {
    billing_thresholds: {
      usage_gte: threshold,
    },
  })
}
```

When a customer approaches the threshold, Stripe triggers an `invoice.upcoming` event that you can use to send a notification.

### Best Practices for Usage-Based Billing

- **Batch usage reports** rather than reporting every single event. Hourly or daily batches reduce API calls and handle spikes gracefully
- **Set spending caps** so customers never receive an unexpectedly large invoice. Caps reduce churn from bill shock
- **Show real-time usage** in your dashboard so customers can monitor their consumption
- **Offer committed-use discounts** where customers pre-purchase usage at a lower rate

## Tax Compliance

Tax is the part of the SaaS billing system that nobody wants to build and nobody can ignore. The rules are complex, vary by jurisdiction, and change frequently.

### Your Three Options

**Option 1: Stripe Tax.** Enable `automatic_tax: { enabled: true }` in your Checkout Sessions and subscription creation. Stripe calculates, collects, and reports the correct tax for each transaction. You still remit the tax yourself, but Stripe tells you exactly how much and where.

**Option 2: Merchant of Record (Paddle/Lemon Squeezy).** The payment processor is the legal seller. They handle all tax calculation, collection, and remittance. You never file a sales tax return. The trade-off is higher transaction fees.

**Option 3: Manual compliance.** Calculate and collect tax yourself using services like <a href="https://avalara.com" rel="nofollow">Avalara</a> or <a href="https://taxjar.com" rel="nofollow">TaxJar</a>. Only reasonable for SaaS products selling in a small number of jurisdictions.

### Where Tax Applies

| Region | Tax Type | Rate Range | Registration Required? |
|--------|----------|-----------|----------------------|
| **EU** | VAT | 17-27% | Yes (VAT OSS or per-country) |
| **UK** | VAT | 20% | Yes (above threshold) |
| **US** | Sales Tax | 0-10% | Yes (per state with nexus) |
| **Canada** | GST/HST | 5-15% | Yes (above threshold) |
| **Australia** | GST | 10% | Yes (above threshold) |

**The practical approach:** Use Stripe Tax from day one. It adds a small fee per transaction but eliminates the risk of tax compliance errors. If your SaaS grows to significant international revenue and the Stripe Tax fees become material, evaluate Paddle as a Merchant of Record.

For a deeper comparison of payment processors and their tax handling, see our [Stripe vs Paddle guide](/blog/stripe-vs-paddle).

## Free Trials That Convert

Free trials are the most common SaaS acquisition model, and the billing implementation directly affects conversion rates.

### Card-Required vs No-Card Trials

**Card-required trials** convert 2 to 3 times better than no-card trials. Customers who enter payment information have higher intent and lower friction at conversion. Implement with Stripe by collecting the card during checkout and setting `trial_period_days`:

```typescript
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: priceId, quantity: 1 }],
  subscription_data: {
    trial_period_days: 14,
  },
  payment_method_collection: 'always',
  success_url: `${appUrl}/dashboard`,
  cancel_url: `${appUrl}/pricing`,
})
```

### Trial Length

14 days is the sweet spot for most SaaS products. 7-day trials create urgency but may not give customers enough time to experience value. 30-day trials reduce urgency and delay the conversion decision without meaningfully improving conversion rates.

### Trial Email Sequence

| Day | Email | Goal |
|-----|-------|------|
| **Day 1** | Welcome + quick start guide | Drive first activation |
| **Day 3** | Feature highlight + use case | Show core value |
| **Day 7** | Case study or social proof | Build confidence |
| **Day 12** | Trial ending soon + upgrade CTA | Create urgency |
| **Day 14** | Trial expired + special offer | Recover stragglers |

Handle the `customer.subscription.trial_will_end` webhook (sent 3 days before trial end) to trigger the urgency email automatically.

## Dunning and Payment Recovery

Failed payments cause involuntary churn, the silent revenue killer. A well-implemented dunning flow in your SaaS billing system recovers 20 to 40 percent of failed payments.

### Stripe Smart Retries

Enable Smart Retries in the <a href="https://dashboard.stripe.com" rel="nofollow">Stripe Dashboard</a> under Settings > Subscriptions. Stripe uses machine learning to retry failed payments at the optimal time over a 21-day window.

### Build a Dunning Email Flow

When `invoice.payment_failed` fires:

| Attempt | Timing | Action |
|---------|--------|--------|
| **1st failure** | Immediately | Email: "Payment failed, update your card" + portal link |
| **2nd failure** | Day 3 | Email: "Action required" + warn about access loss |
| **3rd failure** | Day 7 | Email: "Final notice" + downgrade to free tier |
| **All retries fail** | Day 21 | Cancel subscription + "We miss you" email with reactivation link |

### Keep Access During Dunning

Do not immediately revoke access when a payment fails. Keep the subscription active during the retry window. Cutting access too early frustrates customers who have a legitimate card issue and would have paid otherwise. Downgrade to a limited free tier rather than full cancellation when possible.

## Common Billing Mistakes

These are the SaaS billing system mistakes that cost real revenue. Every one of them is preventable.

### 1. Trusting Client-Side Redirects

The Stripe Checkout success_url redirect is not confirmation that payment succeeded. Customers can reach your success page without paying (browser back button, URL manipulation). Always provision access through webhooks, never through redirect URLs.

### 2. Skipping Webhook Idempotency

Stripe sends webhook events at least once, not exactly once. Without idempotency checks, your handler processes the same event multiple times, creating duplicate subscriptions, double charges, or corrupted state. Store every `event.id` and skip duplicates.

### 3. Ignoring Proration Edge Cases

Mid-cycle plan changes without proper proration cause customer disputes. A customer who upgrades on day 15 of a 30-day cycle should pay half the difference, not the full upgrade price. Always use `proration_behavior: 'create_prorations'` and preview the amount before confirming.

### 4. Not Implementing Dunning

SaaS products without dunning flows lose 10 to 20 percent of MRR to involuntary churn. Most failed payments are recoverable with a simple email and a card update link. Ignoring this is leaving money on the table.

### 5. Manual Tax Compliance

Calculating sales tax and VAT manually works until it doesn't. One missed jurisdiction, one rate change, and you owe back taxes plus penalties. Automate tax from day one with Stripe Tax or use a Merchant of Record.

### 6. Hard-Deleting Canceled Subscriptions

When a customer cancels, do not delete their data or subscription record. Mark the subscription as canceled and retain the data. Customers who cancel frequently reactivate. If their data is gone, so is the reactivation opportunity.

## The SaaS Billing Checklist

Run through this before launching your SaaS billing system:

### Payment Infrastructure

- [ ] Stripe account configured with products and prices
- [ ] Checkout Sessions creating subscriptions with correct prices
- [ ] Customer Portal enabled for self-service management
- [ ] Webhook endpoint deployed and receiving events
- [ ] Webhook signature verification implemented
- [ ] Idempotency checks preventing duplicate event processing

### Subscription Management

- [ ] Upgrade flow with immediate proration
- [ ] Downgrade flow scheduled for period end
- [ ] Proration preview showing exact amounts before confirmation
- [ ] Cancellation at period end (not immediate)
- [ ] Reactivation flow for canceled customers

### Revenue Protection

- [ ] Smart Retries enabled for failed payments
- [ ] Dunning email sequence triggered on payment failure
- [ ] Grace period during payment retry window
- [ ] Usage alerts preventing bill shock
- [ ] Spending caps on usage-based plans

### Compliance

- [ ] Stripe Tax or Merchant of Record configured
- [ ] Tax calculation enabled on all Checkout Sessions
- [ ] Invoice generation with correct tax breakdowns
- [ ] Customer tax IDs collected for B2B sales

### Monitoring

- [ ] Webhook delivery monitoring with alerting
- [ ] MRR tracking dashboard
- [ ] Churn and recovery rate reporting
- [ ] Failed payment alerts sent to the team

{{ partial:cta/forge }}

## Conclusion

A SaaS billing system is infrastructure that directly affects revenue. Every bug is a potential charge dispute. Every missed webhook is a customer who loses access. Every skipped dunning email is revenue that walks out the door.

The architecture in 2026 is straightforward: Stripe handles payment processing, subscription management, and tax calculation. Your application handles access control through webhooks. The Customer Portal handles self-service billing management. You write the glue code that connects them.

Start with the basics: Checkout Sessions, webhook handlers, and the Customer Portal. Add usage-based billing if your pricing model requires it. Enable Stripe Tax before your first international customer. Build a dunning flow before your first failed payment.

The recurring billing system you build today runs every month for the life of your product. Invest the time to get it right, test every subscription lifecycle event, and monitor webhook delivery. Your MRR depends on it.

---

## Related Resources

- [Stripe vs Paddle for SaaS: Payments Compared](/blog/stripe-vs-paddle)
- [Stripe vs Lemon Squeezy: Which Payment Platform?](/blog/stripe-vs-lemonsqueezy)
- [Best SaaS Starter Kits in 2026](/blog/best-saas-starter-kits)
- [SaaS Authentication: Complete Implementation Guide](/blog/saas-authentication-guide)
- [Auth Providers Compared: Clerk vs Auth0 vs Supabase vs Firebase](/blog/auth-providers-compared)
- [How to Start a SaaS Business: The $0-to-$10K MRR Playbook](/blog/how-to-start-a-saas-business)

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "How to Build a SaaS Billing System",
  "description": "Step-by-step guide to implementing a complete SaaS billing system with Stripe, including subscription management, usage-based billing, tax compliance, and payment recovery.",
  "step": [
    {
      "@type": "HowToStep",
      "position": 1,
      "name": "Choose Your Payment Stack",
      "text": "Select between Stripe for full control (2.9% + $0.30 + 0.7% Billing), Paddle for Merchant of Record tax compliance (5% + $0.50), or Lemon Squeezy for simple SaaS products (~5% + $0.50). Most SaaS products choose Stripe for flexibility and lower fees at scale."
    },
    {
      "@type": "HowToStep",
      "position": 2,
      "name": "Select Your Billing Model",
      "text": "Choose between flat-rate, per-seat, usage-based, or hybrid pricing. Hybrid models combining a base subscription fee with usage-based overages are the most common in 2026. Create your products and prices in the Stripe Dashboard."
    },
    {
      "@type": "HowToStep",
      "position": 3,
      "name": "Implement Checkout Sessions",
      "text": "Create Stripe Checkout Sessions in your Next.js API routes. Configure subscription mode, line items, automatic tax, trial periods, and success/cancel URLs. The Checkout Session handles payment collection and subscription creation in one API call."
    },
    {
      "@type": "HowToStep",
      "position": 4,
      "name": "Set Up Webhook Handlers",
      "text": "Create a webhook endpoint that handles checkout.session.completed, invoice.paid, invoice.payment_failed, and customer.subscription.deleted events. Verify webhook signatures, implement idempotency checks, and sync subscription state to your database. Webhooks are the source of truth for billing state."
    },
    {
      "@type": "HowToStep",
      "position": 5,
      "name": "Configure the Customer Portal",
      "text": "Enable the Stripe Customer Portal for self-service subscription management. Customers can update payment methods, switch plans, cancel subscriptions, and download invoices without you building any billing UI."
    },
    {
      "@type": "HowToStep",
      "position": 6,
      "name": "Handle Subscription Lifecycle",
      "text": "Implement upgrade flows with immediate proration, downgrade flows scheduled for period end, proration previews showing exact amounts, and cancellation at period end. Use stripe.subscriptions.update() with proration_behavior for plan changes."
    },
    {
      "@type": "HowToStep",
      "position": 7,
      "name": "Add Usage-Based Billing",
      "text": "If your pricing includes usage components, create Meters in Stripe and report usage via the Meter Events API. Set up usage alerts and spending caps to prevent bill shock. Batch usage reports hourly or daily rather than per-event."
    },
    {
      "@type": "HowToStep",
      "position": 8,
      "name": "Enable Tax Compliance",
      "text": "Enable Stripe Tax with automatic_tax: { enabled: true } on all Checkout Sessions and subscription creation. Stripe calculates the correct tax for each transaction based on customer location. Alternatively, use Paddle as a Merchant of Record to eliminate tax liability entirely."
    },
    {
      "@type": "HowToStep",
      "position": 9,
      "name": "Build Dunning and Payment Recovery",
      "text": "Enable Stripe Smart Retries for automatic payment retry. Build a dunning email sequence triggered by the invoice.payment_failed webhook. Send escalating emails on day 1, day 3, and day 7. Keep access active during the retry window to avoid frustrating customers with temporary card issues."
    },
    {
      "@type": "HowToStep",
      "position": 10,
      "name": "Test and Monitor",
      "text": "Test every subscription lifecycle event using Stripe Test Clocks. Monitor webhook delivery with alerting. Track MRR, churn rate, and failed payment recovery. Run through the billing checklist before launching to production."
    }
  ],
  "totalTime": "PT16H",
  "estimatedCost": {
    "@type": "MonetaryAmount",
    "currency": "USD",
    "value": "0"
  }
}
</script>
