This chapter follows Webhooks in Stripe.
Subscriptions connect a Customer to one or more recurring Prices. Stripe creates Invoices each cycle, attempts payment with the default payment method, and emits Events your app uses to grant or revoke access.
Checkout in subscription mode
The fastest path mirrors one-time Checkout—set mode: 'subscription' and pass recurring Price ids:
import { stripe } from '../stripe-client.js';import { usersById } from '../store.js';
const BASE_URL = process.env.BASE_URL ?? 'http://localhost:4242';
export async function createSubscriptionCheckout(req, res) { const { userId, priceId } = req.body; const user = usersById.get(userId); if (!user) return res.status(404).json({ error: 'Unknown user' });
const session = await stripe.checkout.sessions.create({ mode: 'subscription', customer: user.stripeCustomerId, line_items: [{ price: priceId, quantity: 1 }], success_url: `${BASE_URL}/success.html?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${BASE_URL}/cancel.html`, client_reference_id: userId, subscription_data: { trial_period_days: 14, metadata: { appUserId: userId }, }, });
res.json({ url: session.url });}After Checkout, retrieve session.subscription or listen for customer.subscription.created in your webhook handler.
Subscription lifecycle and statuses
stateDiagram-v2 [*] --> incomplete: create incomplete --> active: first payment succeeds incomplete --> incomplete_expired: timeout active --> past_due: payment fails past_due --> active: payment recovers past_due --> canceled: dunning exhausted active --> canceled: cancel at period end trialing --> active: trial ends with payment trialing --> canceled: cancel during trial
| Status | Access in your app |
|---|---|
trialing |
Grant trial features until trial end |
active |
Full paid access |
past_due |
Grace period policy—warn, limit, or read-only |
canceled |
Revoke at ended_at or immediately if you canceled now |
unpaid |
Treat as lapsed |
Store subscription.id and status on your user record; refresh from webhooks, not from the success page alone.
Invoices and recurring payment
Each billing period Stripe generates an Invoice and attempts automatic collection. Handle:
invoice.paid— Extend entitlement for the new period.invoice.payment_failed— Notify the customer; Stripe retries based on your Dashboard smart retries settings.
function onInvoicePaid(invoice) { const subscriptionId = invoice.subscription; console.log('Subscription paid', subscriptionId, invoice.period_end);}Upgrades, downgrades, and proration
Change items on an existing subscription:
const subscription = await stripe.subscriptions.retrieve('sub_...');await stripe.subscriptions.update(subscription.id, { items: [ { id: subscription.items.data[0].id, price: 'price_new_plan' }, ], proration_behavior: 'create_prorations',});Proration credits unused time and charges the new plan proportionally. Choose none when swapping at period boundaries only. Expose plan changes through your UI or the Customer Portal.
Cancellation
Cancel immediately or at period end:
await stripe.subscriptions.update('sub_...', { cancel_at_period_end: true,});Webhook customer.subscription.updated reflects cancel_at_period_end; customer.subscription.deleted fires when the subscription ends.
Customer Portal
Enable the Customer Portal in Dashboard settings so customers update payment methods, view invoices, and cancel without custom pages. Create a portal session on the server:
const portal = await stripe.billingPortal.sessions.create({ customer: user.stripeCustomerId, return_url: `${BASE_URL}/account.html`,});res.json({ url: portal.url });Portal configuration controls which Prices customers can switch to—keep it aligned with your public catalog.
Next: Stripe beyond payments.
Check your understanding
Question 1 of 6
0 of 6 checked