Billing is a product, not a feature
Treat billing like any other product surface. Customers churn faster from billing pain than from missing features.
The minimum viable billing stack
- Customer: 1-to-1 with a Stripe
Customerobject. - Subscription: a
Subscriptionwith a default price; allow upgrades, downgrades, and cancellations. - Invoice: store invoice IDs and PDF URLs so support can answer questions in seconds.
- Webhook handler: idempotent processing of
invoice.paid,invoice.payment_failed,customer.subscription.updated. - Internal billing events: a write-once table of every state change for auditability.
Proration and plan changes
Stripe handles proration if you set proration_behavior: 'create_prorations'. Decide upfront whether upgrades take effect immediately (charge now) or at next renewal. Be explicit in the UI — "you will be charged $X today" — to prevent disputes.
Usage-based billing
Two patterns:
- Metered subscriptions: report usage to Stripe with
subscription_item.usage_records. Stripe invoices automatically. - Threshold billing: track usage in your DB, charge when thresholds cross, reconcile monthly.
For most products, metered subscriptions are simpler and audit-friendly.
Dunning (failed payments)
A failed charge is not a churn event — yet. Implement:
- Smart retries (Stripe's built-in retry schedule is a good baseline).
- In-app banners when payment is past due.
- Email reminders at day 0, 3, 7.
- Grace period before downgrading access.
Customers in active dunning recover ~30% of the time with a good flow; ~5% with no flow at all.
Pitfalls to avoid
- Storing prices in your DB and Stripe — pick one source of truth.
- Returning before saving the webhook event (idempotency keys).
- Treating tax as an afterthought — use Stripe Tax or a TaxJar-like provider from day one.
- Hard-coding "trial 14 days" instead of making it configurable per plan.
Take-aways
Build billing once, instrument it heavily, and treat every failed payment as a recoverable revenue event, not a churn.