Stripe Subscription Billing Lifecycle: A Complete Developer Guide
Understand the full Stripe subscription billing lifecycle — trials, renewals, dunning, cancellations, and the webhook events needed to keep your database in sync.
Building a checkout flow is the easy part of subscription billing. The hard part is everything that happens after the first payment: renewals, failed charges, plan changes, dunning sequences, and cancellations. Each of these events emits Stripe webhook events, and each one needs a corresponding database update in your system. Miss one, and your billing state diverges from Stripe's — silently, often for weeks.
The Stripe Subscription Billing Lifecycle
Trial and activation
If your product offers a free trial, the subscription is created in trialing status. No invoice is generated until the trial ends. The moment the trial period expires, Stripe automatically transitions the subscription to active and generates the first invoice.
Events to handle:
customer.subscription.trial_will_end— fires three days before trial expiry; use this to send the "your trial ends soon" emailcustomer.subscription.updated— fires when status changes fromtrialingtoactiveinvoice.payment_succeeded— fires when the first real charge succeeds
Renewal cycle
For a monthly subscription, Stripe generates an invoice roughly one hour before the renewal date, then attempts to collect payment. If the charge succeeds, the subscription renews automatically.
Events to handle:
invoice.created— invoice is generated but not yet finalized; you can add line items here if neededinvoice.finalized— invoice is finalized and payment will be attempted shortlyinvoice.payment_succeeded— payment collected; extend access period in your databaseinvoice.payment_failed— payment declined; begin dunning sequence
Failed payments and dunning
When a renewal charge fails, Stripe does not immediately cancel the subscription. Instead, it retries the payment on a configurable schedule (Smart Retries or a custom schedule you define). The subscription moves to past_due status.
This is where many teams have gaps. A subscription in past_due status is still "active" in Stripe's model — the customer has not been cancelled. Your product needs to decide whether to restrict access immediately on the first failure, after a grace period, or only at final cancellation.
Events to handle:
invoice.payment_failed— send dunning email; optionally restrict accesscustomer.subscription.updated— fires when status changes topast_dueinvoice.payment_action_required— payment requires 3D Secure authentication; send the hosted invoice link to the customer
If all retries fail, Stripe can be configured to cancel the subscription automatically (set in your billing settings under Subscription and recovery).
Cancellation
When a subscription is cancelled — by the customer through the Customer Portal, by your code via API, or by Stripe after failed payment retries — the customer.subscription.deleted event fires.
At this point, you must revoke access. Common mistakes:
- Checking
status === "canceled"oncustomer.subscription.updatedinstead of listening tocustomer.subscription.deleted - Assuming cancellation is immediate when the customer has set
cancel_at_period_end = true(the subscription stays active until the period ends)
The cancel_at_period_end field is available on the subscription object. If it is true, the customer has scheduled a cancellation but access should remain active until current_period_end.
Events to handle:
customer.subscription.updatedwithcancel_at_period_end = true— subscription marked for cancellation; show the customer a confirmation, optionally offer a save incentivecustomer.subscription.deleted— access revoked; update your database to reflect cancelled status
Upgrades and downgrades
When a customer changes plans through the Customer Portal or via your API, Stripe handles proration automatically (or you can disable it). The subscription object updates with the new price and plan.
Events to handle:
customer.subscription.updated— theitemsarray will reflect the new plan; update your database to grant the new feature set
Subscription pause
Stripe supports pausing subscriptions (available on certain account configurations). A paused subscription does not generate invoices. Events:
customer.subscription.updatedwithpause_collectionset — subscription is pausedcustomer.subscription.updatedwithpause_collectioncleared — subscription is resumed
Building a Reliable Subscription State Machine
Your database's representation of subscription status should be derived from Stripe events, not managed independently. A practical schema:
subscriptions table:
stripe_subscription_id TEXT UNIQUE NOT NULL
stripe_customer_id TEXT NOT NULL
status TEXT NOT NULL -- active, trialing, past_due, canceled
current_period_end TIMESTAMPTZ NOT NULL
cancel_at_period_end BOOLEAN NOT NULL DEFAULT false
plan_id TEXT NOT NULL
updated_at TIMESTAMPTZ NOT NULL
Every webhook handler for subscription events updates this table. Your application checks status and current_period_end to determine access. This keeps your access logic simple: if status is active or trialing and current_period_end is in the future, the customer has access.
Testing the Full Lifecycle
The Stripe CLI lets you trigger any event in your local environment:
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failed
Test every state transition before launch. A missed customer.subscription.deleted handler means cancelled customers retain access indefinitely — a billing bug that also becomes a support burden.
If you need a subscription billing backend that handles the full lifecycle correctly — trials, dunning, plan changes, and access revocation — Start a build with Clixo.