Ethan Ha

Money2 min read

The Stripe Setup I Reuse on Every Product

By Ethan Ha

Every one of my products handles billing close to identically now, because I stopped reinventing this per product and just carried the same pattern forward.

The webhook events that actually matter

Most Stripe integrations I see handle checkout.session.completed and stop there. That's not enough. At minimum you need to also handle:

  • customer.subscription.updated — catches upgrades, downgrades, and plan changes
  • customer.subscription.deleted — catches cancellations, including ones that happen at period end
  • invoice.payment_failed — catches a card declining on renewal, which is common and easy to miss
  • charge.refunded — keeps your database in sync when a refund happens from the Stripe dashboard directly, not just through your own UI

If you only handle the first one, your database silently drifts from what Stripe actually thinks is true the first time a renewal fails or someone cancels mid-cycle.

Making cancel and refund actually self-serve

The goal is zero emails for either action. That means: a visible "manage subscription" link that goes to Stripe's customer portal (not a custom-built one — use theirs, it's already correct), and a refund policy that's actually enforced in code, not just written on a page somewhere. If your refund window is 14 days, the "request refund" button should check the subscription start date itself and either process it or explain why not, without you touching it.

Keeping local state honest

Don't trust your database's idea of "is this user subscribed" over Stripe's. Treat Stripe as the source of truth and your database as a cache of it, kept in sync by webhooks. If they ever disagree, that's a bug in your webhook handling, not a reason to patch the database directly.

What breaks if you skip webhook signature verification

Anyone can POST a fake checkout.session.completed payload to an unverified endpoint and grant themselves access without paying. Verify every webhook's signature against your Stripe signing secret before trusting anything in the payload — this is a one-line check that closes a real hole.

This whole setup takes an afternoon once, and then it's the same afternoon of code copied into every new product instead of rebuilt from scratch.

This is the billing setup running on Insidr — see it live.

Try Insidr →

Brands: partner with me

Keep reading

← Back to Resources