Stripe billing for SaaS: the edge cases that break
Taking a payment is the easy part. Proration, failed renewals, downgrades, and refunds are where SaaS billing implementations quietly break.

Stripe Checkout gets you charging customers in an afternoon. The remaining 90% of billing work is everything that happens after the first successful payment.
The rule that prevents most bugs
Stripe is the source of truth for subscription state. Your database is a cache of it, updated by webhooks.
Every billing bug we are called in to fix traces back to violating this. Teams set user.plan = "pro" after checkout succeeds and then discover their database and Stripe disagree — because a card failed, a customer cancelled from the portal, or a webhook was missed.
Concretely:
- On checkout completion, do not grant access directly. Wait for the subscription webhook.
- Store
stripe_customer_id,stripe_subscription_id,status,current_period_end, andplan— all set from webhook payloads only. - Gate features on your mirrored
status, and reconcile it against Stripe on a schedule.
Webhooks need to be treated as unreliable
They arrive out of order, more than once, and occasionally late.
- Verify the signature. Always. An unverified billing webhook endpoint is a way for anyone to grant themselves a subscription.
- Make handlers idempotent. Key on the event ID and ignore events you have already processed.
- Handle out-of-order delivery. Compare timestamps rather than assuming the newest arrival is the newest event.
- Return 200 quickly, then process asynchronously. Slow handlers get retried, which produces duplicates.
- Reconcile nightly. Fetch active subscriptions and compare with your records. This catches everything the webhooks missed, and it will catch something.
Trials
Decide two things before you build:
Card up front or not? Requiring a card lowers signups and raises conversion — the same trade-off as showing pricing publicly. Not requiring one does the reverse. This is a business decision that changes the implementation.
What happens at the end? Automatic conversion, or expiry into a locked account? Either is fine; ambiguity is not.
Then handle the awkward parts: extending a trial for a customer who asks, upgrading mid-trial, and what a customer sees the day it ends. Send a reminder before it expires — this is the single highest-leverage email in a SaaS product.
Upgrades, downgrades, and proration
Upgrades should take effect immediately with prorated billing. The customer paid more; give them the thing now.
Downgrades are the interesting case. Immediately means they lose paid-for access; at period end means you carry them at the higher tier until then. Most products choose period end — but you must show clearly what they have until when.
The trap: a downgrade that puts an account over the new tier's limits. Ten team members downgrading to a five-seat plan. Decide the policy — block the downgrade, or lock the account until they resolve it — and communicate it before they click.
Failed payments
Cards expire and get declined constantly. Stripe's retry schedule handles much of it, but you need to decide:
- How long does access continue during retries? A grace period keeps customers who would otherwise churn to a bank issue.
- What does the customer see? An in-app banner outperforms email, which lands in a spam folder.
- What happens when retries are exhausted? Cancel, downgrade to free, or lock with data retained?
Retaining data for a defined period after cancellation is almost always the right call. Deleting immediately turns a recoverable lapse into a permanent loss.
Tax, and why it is not optional
Digital services attract VAT, GST, and US sales tax based on the customer's location. Stripe Tax handles calculation and collection; use it rather than building your own.
Collect and store the customer's country and, where required, a VAT number, at checkout. Retrofitting tax data onto existing customers is genuinely unpleasant.
Test the paths nobody tests
Use Stripe's test clocks to simulate time passing, then verify:
- Trial expiring with no card
- Renewal with an expired card, through all retries
- Upgrade mid-period, then downgrade next period
- Cancellation followed by resubscription
- Refund and its effect on access
- Two webhooks arriving out of order
Each of these is a real support ticket you would otherwise receive from a paying customer.
Billing is one of the largest slices of an MVP budget, and one of the few parts worth getting right before launch rather than after.
We build billing this way as standard, and it is one of the more common things we are asked to come in and repair. If yours drifts, the nightly reconciliation is where we would start.


