If your app treats a subscription as paying or not paying, open your billing code and check one thing first: what does it do when a renewal card fails? Stripe's subscription model has more states than that, and the gap between the two is where access bugs live. This post walks through the Stripe subscription status values, what each one means for access, and how to handle the awkward middle states without locking out a customer who's still paying.
What statuses can a Stripe subscription have?

A Stripe subscription can have eight statuses: trialing, active, incomplete, incomplete_expired, past_due, unpaid, paused and canceled, and your Dashboard settings decide which of several outcomes a failed payment leads to.
Stripe's subscription overview documents each one. This table is my condensed reading of it:
| Status | What it means | What leads there |
|---|---|---|
trialing | Trial running, safe to provision | Subscription created with a trial period |
active | Good standing | First invoice paid, or a past_due subscription's governing invoices settled |
incomplete | Waiting on the first payment | First payment needs a payment method or customer action, or is still processing |
incomplete_expired | First payment never completed | The customer didn't pay within 23 hours of creation |
past_due | Latest finalized invoice failed or wasn't attempted | A renewal payment failed |
unpaid | Retries are over, the subscription remains | Only if your Dashboard settings choose this outcome |
paused | Trial ended without a payment method | trial_settings.end_behavior.missing_payment_method set to pause |
canceled | Terminal, can't be updated | Cancellation, or a Dashboard setting after retries end |
Two details in that page are easy to miss. First, incomplete and incomplete_expired exist because a first payment can need customer action such as 3D Secure. Stripe gives the customer 23 hours, after which the subscription becomes incomplete_expired and you create a new one if they return. Treat that as a lost signup, not a billing failure, and your churn numbers stay honest. Second, payment methods with delayed confirmation, such as ACH Direct Debit, can move a subscription straight to active and bypass incomplete. If the payment fails later, Stripe voids the invoice but the subscription can remain active, so don't assume active means paid.
Does Stripe always move a failed subscription to unpaid or canceled?
No. What happens after retries end is a Dashboard choice. Stripe's docs say an invoice that is still unpaid after all retries can move the subscription to canceled or unpaid, or leave it past_due, depending on your settings.
That has a consequence for your code. Don't hardcode the assumption that past_due always ends in canceled. Read the setting you actually configured, and write the access rules for all three outcomes. Stripe also lets you choose whether status is resolved from the most recent invoice (the default) or from all applicable outstanding invoices, which changes when a past_due subscription returns to active.
Why does your subscription status column end up disagreeing with actual access?

It happens when the field your webhook handler writes isn't the field your feature check reads, so the two drift apart without any error being thrown.
I can't point to a study measuring how common this is, so treat it as a design warning rather than a statistic. The mechanism is simple. One code path updates a subscription_status column on every webhook event, while another path decides access from a separate flag such as a role, a plan name or a boolean. The first time someone edits the access logic without touching the webhook handler, the two disagree, and nothing fails loudly.
Stripe's own guidance points at a single source of truth. After invoice.paid, the webhook docs say to retrieve the subscription and confirm its status is active before extending access, because a paid invoice doesn't always move a subscription to active when status resolution uses all invoices. My recommendation: write the state once, from the webhook, into one field, and have every access check read that same field.
BookBed and Callidus are both Stripe builds, so this is a pattern I care about, but I'll stay with what their case studies actually say. BookBed's case study says its subscription infrastructure, including webhook handlers and a trial lifecycle, is wired and tested but not yet activated for paid traffic, so I have no paid-customer failure data from it. Callidus handles twenty Stripe webhook events end to end, including customer.subscription.trial_will_end and invoice.payment_action_required, and pairs that with a scheduled function that escalates payment-failed tenants. Its grace period blocks mutations at both the route and Firestore rules layers, so access doesn't depend on a single check.
How do you handle a past_due subscription without locking out a paying customer?
Keep the customer's access, show a clear payment warning, and let Stripe's retries run, then cut access only when the subscription reaches unpaid or canceled.
Consider a small SaaS where a customer's renewal card fails. The instinct is to flip a switch and lock the account. That instinct is the thing a state machine exists to override, because a failed card is often a bank problem, not a decision to leave.
- Listen for
invoice.payment_failed. Stripe lists it as the event to handle for failed subscription payments, and it carries retry information such as the attempt count. Don't infer payment failure fromcustomer.subscription.updated, which Stripe says fires for many changes, including renewals, coupons and plan changes. - Set a grace flag, not a lockout. Stripe's guidance is to notify the customer directly and ask them to update their payment details when a subscription becomes
past_due. - Surface it in the app. A dismissible banner pointing at the billing page beats silence and beats a support ticket.
- Let Smart Retries work. Stripe recommends Smart Retries, with a default of 8 tries within 2 weeks, and you can choose windows from one week to two months. Stripe also notes it doesn't retry after hard declines, such as a stolen card.
- Revoke at
unpaidorcanceled. Stripe's docs say to revoke access when a subscription isunpaid, because payment was already attempted and retried while it waspast_due.
The retry engine does real work. A January 2024 Stripe engineering post by Kiran Chandran reports that Smart Retries recovers $9 in revenue for every $1 customers spend on Billing, and that recovered subscriptions continue on average for seven more months. Those are Stripe's own figures about its own product, so read them as a vendor claim and not as an independent result. The practical point still holds: cancelling access the moment past_due appears throws away the window in which recovery happens. For the email side of that window, see the post on failed-payment recovery and dunning.
What should each subscription state be allowed to do?
Gate features per state, not per boolean, because a trial user, a grace-period customer and a cancelled customer deserve different rules. This is a recommended policy, not something Stripe prescribes beyond the revoke-at-unpaid-or-canceled rule.
| State | Recommended access | Why |
|---|---|---|
trialing | Full access | Stripe says it's safe to provision during a trial |
active | Full access | Good standing, unless a delayed payment later fails |
past_due | Full access plus a warning banner | Retries may still be running |
unpaid | Read-only or none | Stripe says to revoke access here |
canceled | No access, short export window | Terminal, but consider your data-retention promises |
paused | Reactivation flow only | Trial ended with no payment method |
The paused row only exists if you configure it. Stripe's free-trial documentation offers three choices for a trial that ends without a payment method: cancel, pause, or create an invoice, and the last one moves the subscription to past_due if no payment method arrives. Pick one deliberately instead of inheriting a default.
What happens to a subscription when someone deletes the Stripe customer?
It's cancelled, and it can't be brought back. Stripe's API reference says deleting a customer is permanent, can't be undone, and immediately cancels any active subscriptions on that customer.
So there's no repair path that reattaches an orphaned subscription. Recovery means creating a new customer and a new subscription and updating your own mapping. Because the deletion is irreversible, decide who is allowed to do it in the Dashboard, and make sure your webhook handler treats customer.subscription.deleted as the event that revokes access.
What changed in Stripe's subscription model in 2026?

Stripe added billing schedules and a billed_until property on subscription items, which let you collect payment for future periods before they begin.
The May 27, 2026 changelog entry lists use cases such as prepaying several months at signup, early renewal billing, paying twelve months upfront on a monthly price and charging an early cancellation fee. If your lifecycle code assumes one clean trialing to active transition, read that entry before you build anything prebilling-shaped on top of it. It also means billed_until is a field your entitlement logic may want to read.
None of this state machine works if the webhooks Stripe sends never reach your database, and it's one slice of what a full Stripe integration covers. If you're already on a Supabase and Stripe stack, the same eight statuses map onto Postgres rows in the same way. Decide your lifecycle logic early, before the rest of your SaaS MVP stack hardens around a boolean is_active column.
Open your access check today and search for past_due. Does your app treat it any differently from canceled?
