
TLDR
- Use Stripe Checkout and the Customer Portal for v1. They handle the hard parts (payment forms, plan changes, billing history) for you.
- Webhook handling is where most teams fail. Make handlers idempotent, log every event, and use the retry mechanism Stripe provides.
- Store the minimum: stripe_customer_id, stripe_subscription_id, current_period_end, and plan_id. Treat Stripe as the source of truth.
- Handle failed payments gracefully. About 5-15% of subscription renewals fail. Stripe Smart Retries help, but you need a dunning flow too.
- Skip in v1: usage-based billing, complex proration, multi-currency, and tax automation. Add when you actually need them.
Building subscription billing into a SaaS app feels like it should be a small piece of work. It is not. Done right, it is a multi-week project with subtle traps. Done wrong, you get charged-off cards, stuck subscriptions, double-billed customers, and angry support emails.
Here is the architecture and the patterns we use for production-quality Stripe integrations.
Use Stripe Checkout for V1, Not Custom Forms
Stripe Checkout is a hosted payment page that handles the entire payment flow: card collection, validation, 3D Secure authentication, error handling, mobile optimization, and PCI compliance. You redirect the user to it. They pay. Stripe redirects them back. That is it.
Building your own card form using Stripe Elements is technically possible but rarely worth it for v1. Checkout handles edge cases (Apple Pay, Google Pay, BNPL options, regional payment methods) that you would have to implement and maintain. The customization tradeoff is rarely worth the cost.
Save the custom payment form for when you have a specific UX reason to build it.
Use the Customer Portal for Subscription Management
Stripe's Customer Portal is a hosted page where customers can manage their subscription: change plan, update payment method, see billing history, cancel. You enable it in your Stripe dashboard, generate a portal session for the logged-in customer, redirect them to it.
This single feature saves you weeks of work. Subscription management is full of edge cases (proration on plan changes, prepaid vs metered billing, handling overdue accounts, downgrades). Stripe handles all of it.
Build your own subscription management UI only when the Portal genuinely cannot do what you need. For 90%+ of SaaS, the Portal is enough.
The Minimum You Should Store Yourself
Treat Stripe as the source of truth for billing data. In your own database, store only what you need to authorize access:
- stripe_customer_id (links your user to the Stripe customer)
- stripe_subscription_id (the active subscription, if any)
- plan_id (so you know what features to grant)
- current_period_end (so you know when the subscription expires if not renewed)
- status (active, past_due, canceled, etc.)
Do not duplicate Stripe's data into your tables. When you need invoice history, query Stripe. When you need payment method details, query Stripe. The less you copy, the less you have to keep in sync.
Webhooks: Where Most Teams Get It Wrong
Stripe sends events about everything that happens in your account: payments, failed charges, subscription updates, refunds. Your application needs to listen for the events that matter and update your database accordingly.
The events to listen for in v1:
- checkout.session.completed - new subscription created, grant access
- customer.subscription.updated - plan changed, status changed
- customer.subscription.deleted - subscription canceled, revoke access
- invoice.paid - successful renewal, ensure access stays active
- invoice.payment_failed - renewal failed, start dunning flow
The mistakes we routinely fix in client codebases:
- Webhook handler is not idempotent. Stripe will sometimes deliver the same event twice. If your handler creates a new record without checking for duplicates, you get double-grants or double-charges.
- No webhook signature verification. Anyone can POST to your webhook endpoint pretending to be Stripe. Verify the signature on every request.
- Slow webhook handlers. Stripe expects a 200 response within 30 seconds. If your handler does heavy work synchronously, it will time out and Stripe will retry, which causes other problems. Acknowledge fast, process async.
- Trusting client redirects more than webhooks. The redirect after Checkout is a UX hint, not a guarantee. The webhook is the source of truth. Wait for the webhook before granting access.
Handle Failed Payments
Roughly 5-15% of subscription renewals fail on the first attempt. Cards expire, get blocked, get insufficient funds. Stripe's Smart Retries automatically attempts the charge again over the next several days. You should still build a dunning flow:
- When invoice.payment_failed fires, mark the subscription past_due in your system
- Email the customer with a link to update their payment method
- Continue showing them a banner in your app reminding them to update billing
- Decide on a grace period (7-14 days is typical) before downgrading or suspending access
- When the issue is resolved (invoice.paid fires), restore access immediately
This flow saves a meaningful percentage of revenue. Without it, customers who would have paid happily lose access and churn.
What to Skip in V1
Things that sound important but you almost certainly do not need at first:
- Usage-based billing. Adds significant complexity. Use plan tiers first. Add metered billing only if you have customers asking for it.
- Complex proration logic. Stripe handles standard proration. Custom proration is rarely needed in v1.
- Multi-currency. Operate in your home currency. Add other currencies when international demand justifies it.
- Tax automation. Stripe Tax handles this when you need it. For v1, you can charge tax on your prices (depending on your jurisdiction) and reconcile later.
- Custom invoicing. Stripe's automated invoices are fine for v1. Custom invoices come when enterprise customers demand them.
- Subscription pause. Useful but not necessary for v1.
The Realistic Build Effort
A clean v1 Stripe integration with Checkout, Customer Portal, webhook handling, and dunning typically takes 2-4 weeks of focused engineering work. That includes testing all the failure modes, which is where the time goes.
You will think you are done after week 1. Then you will discover the dozens of edge cases (refunds, partial refunds, disputes, failed renewals, plan changes mid-period, trial expirations) that need handling. Plan for the long tail.
Testing
Stripe has excellent test mode. Use it. Use the test card numbers (4242 for success, 4000 for decline, etc.) to verify every code path. Test your webhook handlers using the Stripe CLI which can forward live events to your local dev environment.
Things to specifically test:
- New subscription creation (full happy path)
- Plan upgrade with proration
- Plan downgrade
- Cancellation
- Failed payment and recovery
- Customer-initiated payment method update
- Webhook retry (manually trigger duplicate webhook delivery)
- Subscription expiration without renewal
At Stunzer Digital, we have built Stripe integrations for SaaS products at every stage. If you want a billing system that handles the long tail correctly the first time (so you do not spend the first year of your product issuing manual refunds), let's talk.
Tags
Related service
Want this built? See how we work on Web Development.


