Stripe Subscription Integration: A Full Walkthrough with Pitfalls
Complete Stripe subscription integration in a SaaS system — focusing on Webhook event handling, idempotency design, and common gotchas.
Integration Overview
Stripe subscriptions involve Checkout Session creation, Webhook listening, and subscription lifecycle management.
Stripe has the best payment documentation I've ever seen — but even so, real-world integration pitfalls far exceeded expectations.
Our SaaS system has three paid tiers (Basic / Pro / Enterprise) with monthly and annual billing. Users can upgrade, downgrade, cancel, or resume subscriptions at any time. Seemingly simple requirements, but a massive number of edge cases to handle.
Core Flow
Checkout Session Creation
When users click "Upgrade," the frontend calls backend API to create a Checkout Session, then redirects to Stripe's hosted payment page.
Key configuration:
Subscription Lifecycle
A subscription's complete lifecycle:
Don't try to manage subscription state yourself — let Stripe be the source of truth, and your system just syncs Stripe's state. Maintaining your own subscription state is a bug factory.
Webhooks Are the Core
Subscription state changes (creation, renewal, cancellation, failure) are all async Webhook notifications — idempotent handling is mandatory.
Key Events
Processing Flow
Each Webhook event's handling:
Webhook handling is like writing distributed systems — assume any step can fail, any event can arrive multiple times, and order may be scrambled.
Pitfalls
1. Webhook Signature Verification
Must use the raw request body (buffer), never JSON.parse before verifying. The signature is computed on raw bytes — parsing and re-serializing may change field ordering.
In NestJS, configure specific routes to use rawBody:
With Express, req.body after body-parser is no longer the raw buffer. Skip JSON parsing middleware on Webhook routes.
2. Idempotency Design
The same event may be delivered multiple times — deduplicate by event.id. Our approach:
3. User Experience
Don't rely on page redirect after Checkout to update status — the Webhook might not have arrived. Our solution:
4. Upgrade/Downgrade Handling
When upgrading from Basic to Pro, Stripe provides proration (pro-rate billing). But default behavior may be unexpected:
Testing
Local Testing
Use `stripe listen --forward-to` to forward Webhooks to local dev. Stripe CLI handles signature verification automatically.
Trigger Test Events
Use Stripe CLI to trigger test events:
Test Card Numbers
Stripe provides rich test card numbers:
Before going live, thoroughly test ALL scenarios: successful payment, failed payment, upgrade, downgrade, cancel, resume, Webhook retry. Every scenario can have bugs.
Conclusion
The core of Stripe integration isn't "getting the API to work" — it's "correctly handling all edge cases." Payment systems tolerate zero bugs — one extra charge or one missed permission update directly causes user complaints.
Spend 80% of your time on Webhook handling and edge cases, not on UI.
For first-time payment integration, strongly recommend completing all flows in test mode first, writing integration tests, then switching to production. For payment system releases, caution matters more than speed.