# 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.

- **Published:** 2026-02-21
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** stripe, subscriptions, billing, saas, payments
- **Canonical URL:** https://clixo.sh/blog/stripe-subscription-billing-lifecycle-guide

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" email
- `customer.subscription.updated` — fires when status changes from `trialing` to `active`
- `invoice.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 needed
- `invoice.finalized` — invoice is finalized and payment will be attempted shortly
- `invoice.payment_succeeded` — payment collected; extend access period in your database
- `invoice.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 access
- `customer.subscription.updated` — fires when status changes to `past_due`
- `invoice.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"` on `customer.subscription.updated` instead of listening to `customer.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.updated` with `cancel_at_period_end = true` — subscription marked for cancellation; show the customer a confirmation, optionally offer a save incentive
- `customer.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` — the `items` array 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.updated` with `pause_collection` set — subscription is paused
- `customer.subscription.updated` with `pause_collection` cleared — subscription is resumed

## Building a Reliable Subscription State Machine

```mermaid
stateDiagram-v2
  [*] --> trialing : created with trial
  [*] --> active : created without trial
  trialing --> active : trial ends, payment succeeds
  trialing --> canceled : trial ends, payment fails
  active --> past_due : renewal payment fails
  active --> canceled : cancel_at_period_end reached
  past_due --> active : retry payment succeeds
  past_due --> canceled : all retries exhausted
```

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](https://clixo.sh/#contact) with Clixo.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
