SaaS Billing with Stripe: Subscription Setup Guide for Developers
Billing is where SaaS revenue lives. Get it wrong and you're leaking money through failed payments, missed upgrades, and manual intervention on what should be automatic.
Stripe's Data Model
- Product: What you're selling.
- Price: How you're selling it — recurring interval, amount, currency.
- Customer: A Stripe record linked to your user.
- Subscription: Links a Customer to Prices; drives recurring billing.
- Invoice: Generated automatically each billing cycle.
The Subscription Creation Flow
- Create Stripe Customer on signup and store the customer ID.
- Use Checkout or Elements to collect payment information.
- Stripe fires webhook on successful payment.
- Backend activates account via webhook handler.
const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: 'subscription',
line_items: [{ price: process.env.STRIPE_STARTER_PRICE_ID, quantity: 1 }],
success_url: `${BASE_URL}/dashboard`,
cancel_url: `${BASE_URL}/pricing`,
subscription_data: { trial_period_days: 14 },
});
Webhooks: The Most Critical Part
Never trust the Checkout success_url redirect to confirm payment. Webhooks are the authoritative source of truth. Always verify webhook signatures with stripe.webhooks.constructEvent.
Critical Webhook Events
| Event | Action |
|---|
| checkout.session.completed | Activate subscription, provision workspace |
| customer.subscription.updated | Sync plan, update feature access |
| customer.subscription.deleted | Downgrade or lock account |
| invoice.payment_succeeded | Record payment, update billing period |
| invoice.payment_failed | Start dunning sequence, email customer |
| invoice.payment_action_required | Prompt for 3D Secure (critical for EU) |
Trial Logic
Set trial_period_days on subscription creation. Stripe won't charge during trial. Schedule trial-end reminder emails at T-7 and T-1 days yourself via your email provider.
Stripe Customer Portal
Don't build your own billing management UI. Stripe's Customer Portal handles plan changes, cancellation, invoice history, and payment method updates out of the box.
const portal = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: `${BASE_URL}/dashboard/billing`,
});
return redirect(portal.url);
Dunning: Recovering Failed Payments
Stripe Smart Retries automatically retries failed payments using ML-optimized timing. Complement with a 3-email dunning sequence triggered by invoice.payment_failed: immediate, day 3, and day 7 notifications.
Common Implementation Mistakes
- Trusting the redirect over the webhook: Always use webhooks as the source of truth.
- Missing 3DS handling: invoice.payment_action_required silently breaks EU subscriptions.
- No idempotency keys: Required for safe retries on all creation requests.
For pricing strategy, see SaaS Pricing Models. For scaling architecture, see Scaling SaaS from MVP to Enterprise.
FAQs
How do I integrate Stripe subscriptions with my SaaS?
Create a Stripe Customer on signup, collect payment via Checkout or Elements, create the Subscription with the correct Price ID, then handle all billing events via signed webhooks. Sync subscription status to your database on every webhook event.
What Stripe webhooks should I handle for SaaS billing?
At minimum: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, and invoice.payment_failed. Add invoice.payment_action_required for European SCA compliance.
How does Stripe handle free trials for SaaS?
Set trial_period_days on subscription creation. Stripe won't charge during trial. At trial end it fires an invoice and sends a subscription update webhook. You must schedule your own trial-end reminder emails.
What is Stripe dunning?
Dunning is automatic retry logic for failed payments. Stripe Smart Retries retries at ML-optimized intervals. Complement with your own dunning email sequence triggered by the invoice.payment_failed webhook.