How Subscription Billing Works (and How to Build It)

Subscription billing is deceptively simple on the surface: charge a customer every month. What breaks it in practice is webhook handling, idempotency, failed payment recovery, and proration on plan changes. Most tutorials stop at the checkout button. That's not billing — that's a demo. Here's how subscription billing actually works, and how to build it so it doesn't fail silently at 2am.
Quick Answer
Subscription billing charges a customer a recurring fee (monthly or yearly) for ongoing access. It works by creating a customer and a subscription with your payment provider, then letting the provider charge the saved payment method each cycle and notify your app via webhooks. To build it, you store plans and subscriptions in your database, kick off checkout, and update access when webhook events arrive — never on the redirect alone.
The pieces of a subscription system
Piece | Job |
|---|---|
Plan / price | What you sell and how often it bills (monthly/yearly) |
Customer | The buyer record in your provider + your database |
Subscription | The link between a customer and a plan, with a status |
Checkout | Where the customer enters payment and starts the subscription |
Webhooks | The provider telling your app what happened (paid, canceled, failed) |
Entitlement | What the active subscription unlocks in your app |
How a subscription flows
The customer picks a plan and you start a checkout session.
The provider collects payment and creates the subscription.
The provider sends a webhook (for example,
checkout.session.completed).Your app verifies the event, then grants access and stores the subscription.
Each cycle, the provider charges again and sends renewal/failure events.
The golden rule: grant access on the verified webhook, not on the success redirect. Redirects can be faked or missed; webhooks are the source of truth.
Handling the hard parts
Failed payments: providers retry (dunning). Listen for failure events and downgrade or notify after the grace period.
Idempotency: the same webhook can arrive twice. Record processed event IDs and skip duplicates.
Upgrades/downgrades and proration: let the provider compute proration where possible.
Cancellations: keep access until the period ends unless you offer instant cancel.
How this maps to FastStaq
FastStaq implements this flow with a pluggable billing gateway — Stripe by default, Lemon Squeezy via a provider toggle. It models Plan, Subscription, BillingInterval, and PackageEntitlement, handles Stripe Checkout, the customer portal, and webhooks, and deduplicates events with StripeEvent / LemonSqueezyEvent ledgers so a replayed webhook never double-grants. The shared money path (provisionOneTimeGrant) is idempotent by design. See the SaaS billing guide.
Frequently asked questions
Do I need a billing platform or just Stripe? For most SaaS, Stripe (or Lemon Squeezy) plus your own plan/subscription tables is enough. Add a billing platform only when your pricing gets very complex.
Why use webhooks instead of the redirect? Redirects are unreliable and spoofable. Webhooks are signed, server-to-server, and are the source of truth for access.
How do I avoid double-charging or double-granting? Make webhook handling idempotent by recording processed event IDs.
Next steps
Read the SaaS billing guide
Compare usage-based billing


