You're setting up revenue for a SaaS and the choices are piling up: Checkout or an embedded form, flat or metered pricing, what to do when a card fails. Start with one rule that settles most of them: Stripe owns the money and the billing schedule, your webhook handler owns the decision about who has access.
This is the hub for my Stripe-related posts. It maps how a SaaS billing system fits together, links to the deeper guides, and flags the facts that change, each checked against Stripe's pages on 30 September 2026. The SaaS products I build with Stripe include BookBed and Callidus, and I refer to them only for what their case studies document.
How does Stripe subscription billing work end to end?
You define products and recurring prices in Stripe, send the customer through a payment flow, and let Stripe invoice and collect on schedule while your app mirrors the resulting state to decide access.
The flow most SaaS products follow:
- A visitor chooses a plan on your pricing page.
- Your server creates a Checkout Session (or a subscription with Elements) and sends the customer to pay.
- Stripe charges the card, creates the subscription and emits webhook events.
- Your webhook handler verifies the signature and updates the customer's plan and entitlements in your database.
- Your app reads its own database on the hot path, not Stripe, to decide what a user may do.
Step 5 is a design choice, not a Stripe requirement: calling the API on every request adds latency and a dependency you don't need, so cache the state and keep it fresh with events. Store the Stripe customer ID and subscription ID against your tenant record from the first webhook, so every later event can be matched to an account without guesswork. The setup details (session creation, proration, portal) live in the Stripe subscription billing setup guide, and the state machine you'll mirror is in the subscription lifecycle states post. For a Supabase-backed version of the mirror table, see the Stripe and Supabase subscription pattern.
Should you use Checkout or build on Elements?
Start with Checkout if you're validating that people will pay; move to Elements-based flows when the upgrade moment is a designed part of the product.
Stripe's Checkout documentation offers four variants: a full hosted page, a full embedded page, an embedded form, and Elements. So "hosted or embedded" is no longer the real fork; how much of the payment UI you want to own is. On compliance, Stripe's integration security guide says businesses that handle untokenized card numbers directly might face more than 300 PCI DSS controls, and recommends low-risk integrations that send card data straight to Stripe to reduce your obligations. Pick the lowest-scope option that still gives the experience you need.
Both paths produce the same subscription objects and webhooks, so you can switch the front end later. Callidus, which collects patient deposits through each clinic's own Stripe account using Stripe Connect Standard with direct charges, handles twenty webhook events end to end according to its case study. The Connect marketplace teardown explains that model.
Which SaaS billing model fits: flat, usage-based, or hybrid?
Flat per-seat or per-tier pricing fits most early SaaS, usage-based fits products where cost or value tracks consumption, and hybrid adds a base fee to metered overage once you have evidence you need it.
| Model | Good fit when | Build cost | What to watch |
|---|---|---|---|
| Flat (seat or tier) | Value scales with people or plan limits are easy to explain | Lowest | Heavy users subsidised by light ones |
| Usage-based | Cost or value follows API calls, tokens, documents | Higher: event capture, aggregation, reporting | Invoice surprises; event errors |
| Hybrid (base plus overage) | You need predictable revenue and consumption upside | Highest | Hardest to build and explain |
The schema follows the model, so decide from how customers perceive value before you design tables. Usage-based billing has a current wrinkle: Stripe's own documentation now says to use Metronome for new usage-based integrations and keeps Billing Meters for existing ones. The usage-based billing post covers that decision, the meter limits and the error handling.
What does Stripe Billing itself cost?
On its Billing pricing page, Stripe lists pay-as-you-go at 0.7% of Billing volume, with monthly subscription plans for higher volumes, and payment processing fees separate. Prices change, so read that page on the day you budget, and remember it sits on top of card processing fees.
Does the pricing page belong in this system?
Yes, because tier names, plan limits and the annual toggle decide which price objects you create. Treat those as hypotheses to test rather than rules; the B2B pricing page guide goes through structure and copy, and the software cost and pricing guide helps when you're still working out what to charge for the build. For trials, compare the free trial versus freemium billing models.
Why should webhooks, not the success page, grant access?
Because the browser redirect after checkout can be closed, blocked or never fire while the payment still succeeds, only signed server-to-server events reflect what Stripe actually did.
Stripe's subscription webhook documentation tells you to provision access on invoice.paid once the subscription status is active, to handle invoice.payment_failed and invoice.payment_action_required, to use customer.subscription.trial_will_end (sent three days before a trial ends), and to revoke access when a subscription becomes canceled or unpaid. It also says past_due should prompt you to notify the customer.
The webhook documentation lists what a production handler has to do:
- Verify the
Stripe-Signatureheader against the raw request body; any framework that alters the body breaks verification. - Return a
2xxquickly, before complex logic, and process the work through an asynchronous queue. - Deduplicate by event ID, because the same event can arrive more than once.
- Don't depend on order: Stripe doesn't guarantee events arrive in the order they were generated.
- Expect retries: in live mode Stripe retries for up to three days with exponential backoff.
- Log each event ID with its outcome, so a support question about a charge can be answered from your own records.
Step 4 is the one that bites people. If invoice.paid arrives before the subscription event, fetch the objects from the API instead of trusting arrival order. The same reliability discipline applies to every webhook in a system, which the SaaS backend infrastructure guide covers for jobs, email and admin tooling. BookBed applies it in a browser context too: its case study describes a BroadcastChannel and Firestore listener so the original tab learns that a checkout in another tab completed, with the webhook remaining authoritative.
How do you keep one tenant's billing data away from another's?
Enforce it at the data layer, not in query filters. Callidus keeps everything under tenants/{tenantId} in Firestore, carries tenantId and role in JWT claims, and gates mutations in its Firestore rules. On a Postgres stack the equivalent is row-level security keyed on the tenant. Whatever you choose, attach billing to the isolation model the rest of the product already enforces instead of adding a weaker one for payments. The multi-tenant architecture guide goes deeper.
How do you recover failed payments and involuntary churn?
Turn on Smart Retries, act on invoice.payment_failed, tell the customer how to update a card, and revoke access only when the subscription reaches unpaid or canceled.
Stripe's retry documentation says Smart Retries picks retry times using signals such as how many devices presented a payment method recently, with a recommended default of 8 tries within 2 weeks. It also lists what can't be retried: payments with hard decline codes such as stolen_card or lost_card only execute once you obtain a new payment method. After the last attempt, your settings decide whether the subscription is canceled, marked unpaid or left past_due.
How big is the prize? Stripe reports that its recovery tools helped users recover over 6.5 billion dollars in revenue in 2024, per Stripe's own usage-based billing resource page. That figure is Stripe's internal data with no external audit cited, so read it as direction, not as a benchmark for your product.
Build the human side around those events. A workable loop has a clear email with a link to fix the card, a grace period before access drops, and a path through the customer portal; the customer portal with Stripe and Auth.js post shows one, and the dunning and failed payment recovery guide covers the email sequence. As an example of a written policy, Callidus documents a 14-day trial with reminders at 3 days and 1 day, a 3-day grace window after expiry, then suspension. If you're sequencing billing against the rest of a first release, the SaaS MVP development guide places it relative to auth and data.
Your next step: open your webhook handler and check four things in order: signature verification on the raw body, event-ID deduplication, access granted from invoice.paid plus an active status, and a path for unpaid. Which of the four is missing today?
