Stripe Subscription Billing Lifecycle: Trials, Renewals, and Cancellations
Understand the full Stripe subscription billing lifecycle — trials, renewals, dunning, cancellations, and the webhook events needed to keep your database in sync.
Subscription billing continues after checkout through trials, renewals, payment failures, plan changes, and cancellations. Keep the application’s billing and access state aligned with Stripe events and reconciliation checks.
The Stripe Subscription Billing Lifecycle
Trial and activation
For a legacy trial_end free trial, the subscription starts in trialing status and Stripe creates a zero-amount invoice. When the trial ends, billing behavior depends on payment availability and the configured end behavior; the subscription can become past due, paused, or canceled rather than automatically becoming active.
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 subscription transitions and recovery paths before launch, including how cancellation changes access.