Skip to content
Tech Stack9 July 2026 · 7 min readUpdated 30 September 2026

SaaS Subscription Lifecycle in 2026: Trial, Active, Past Due, Cancelled

Stripe subscriptions have eight statuses, not two, and one of the costliest bugs is a status column that stops matching what actually gates access. What each status means and how to handle past_due.

SaaS Subscription Lifecycle in 2026: Trial, Active, Past Due, Cancelled

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 cross-sectioned seed pod with a ring of glowing cyan-lit chambers, growing from a vine wrapped around a cracked concrete server block

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:

StatusWhat it meansWhat leads there
trialingTrial running, safe to provisionSubscription created with a trial period
activeGood standingFirst invoice paid, or a past_due subscription's governing invoices settled
incompleteWaiting on the first paymentFirst payment needs a payment method or customer action, or is still processing
incomplete_expiredFirst payment never completedThe customer didn't pay within 23 hours of creation
past_dueLatest finalized invoice failed or wasn't attemptedA renewal payment failed
unpaidRetries are over, the subscription remainsOnly if your Dashboard settings choose this outcome
pausedTrial ended without a payment methodtrial_settings.end_behavior.missing_payment_method set to pause
canceledTerminal, can't be updatedCancellation, 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?

Eight leaves in a row along a glowing circuit trace, progressing from a small green bud through a bright cyan bloom to curled brown dead leaves

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.

  1. 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 from customer.subscription.updated, which Stripe says fires for many changes, including renewals, coupons and plan changes.
  2. 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.
  3. Surface it in the app. A dismissible banner pointing at the billing page beats silence and beats a support ticket.
  4. 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.
  5. Revoke at unpaid or canceled. Stripe's docs say to revoke access when a subscription is unpaid, because payment was already attempted and retried while it was past_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.

StateRecommended accessWhy
trialingFull accessStripe says it's safe to provision during a trial
activeFull accessGood standing, unless a delayed payment later fails
past_dueFull access plus a warning bannerRetries may still be running
unpaidRead-only or noneStripe says to revoke access here
canceledNo access, short export windowTerminal, but consider your data-retention promises
pausedReactivation flow onlyTrial 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?

Two green vines climbing the same wire-grid trellis, one rooted in a visible soil planter and the other trailing off above the floor with no roots shown

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?

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What are the Stripe subscription statuses?

A Stripe subscription can be trialing, active, incomplete, incomplete_expired, past_due, unpaid, paused or canceled. Stripe's subscription overview documents each one and the events that move a subscription between them. Two of them are easy to misread: incomplete_expired means the first payment wasn't completed within 23 hours, and unpaid only appears if your Dashboard settings choose it as the outcome after retries end. Design your access rules around all eight statuses instead of only active and canceled.

What does past_due mean for a Stripe subscription?

Past_due means payment on the latest finalized invoice failed or wasn't attempted, while the subscription keeps creating invoices. Stripe might retry payment during this status, but it doesn't guarantee another attempt. Your Dashboard settings decide what happens once retries end: the subscription can move to canceled or unpaid, or stay past_due. Stripe's guidance is to notify the customer directly and ask them to update their payment details, and to revoke access when the subscription becomes unpaid or canceled.

How long does Stripe retry a failed subscription payment?

With Smart Retries, Stripe's recommended default is 8 tries within 2 weeks, and you can choose a window of one week, two weeks, three weeks, one month or two months. Stripe says it doesn't retry after hard declines such as a stolen card, or for some payment types, so the schedule isn't guaranteed to run in full. You can also disable Smart Retries and define up to three custom retries instead. Check your own Dashboard retry settings, because they control what your subscriptions do.

Should subscription state be a boolean or a state machine?

Model it as a state machine that covers at least the statuses Stripe exposes, not a single is_active boolean. A boolean collapses trialing, past_due and paused into the same bucket as active, so customers in very different situations get identical access. The extra code is small compared with the support queue of confused, still-paying customers a boolean can create. Write the status once from the webhook into one field, and have every access check read that field.

What happens to a subscription if a Stripe customer is deleted?

Deleting a Stripe customer is permanent and immediately cancels any active subscriptions on that customer. Stripe's API reference states that the deletion can't be undone. Deleted customers can still be retrieved through the API to track history, but they can't have new subscriptions added. Recovery therefore means creating a new customer and a new subscription, then updating your own mapping. Restrict who can delete customers in the Dashboard, and make your webhook handler revoke access on customer.subscription.deleted.