Guides · Updated October 27, 2026
Stripe subscription canceled or past due, but the user still has access
A customer canceled, or their card stopped working, and they can still log in and use the paid plan. Usually nothing is broken on Stripe's side: Stripe changed the subscription's status and sent an event, and your app either didn't receive it, didn't handle that case, or reads the wrong field. This guide explains the statuses and events involved, the common bugs, and how to find the customers it already affects.
What each status means for access
Stripe's subscription statuses are listed in Stripe: How subscriptions work. In short:
trialingandactive: Stripe says you can provision access.past_due: payment on the latest finalized invoice failed or wasn't attempted. The subscription keeps creating invoices, and Stripe might retry the payment, but this status doesn't guarantee another attempt.unpaid: only happens if your Dashboard settings choose it after retries end. Invoices keep being created but no payment is attempted. Stripe's docs say to revoke access here, because payment was already attempted and retried while the subscription waspast_due.canceled: the subscription ended. It's a terminal state; to bill the customer again you create a new subscription.incompleteandincomplete_expired: the first payment hasn't succeeded (Stripe allows 23 hours) or never did.paused: a trial ended without a default payment method and the subscription was set to pause. No invoices are created until it's resumed.
Stripe's guidance on tracking subscriptions sums it up: when a subscription changes to past_due, notify the customer and ask them to update their payment details; when it changes to canceled or unpaid, revoke access (Stripe: Using webhooks with subscriptions).
The events that should change access
customer.subscription.updated: sent when a subscription starts or changes, including status changes likeactivetopast_due, and whencancel_at_period_endis set totrue.customer.subscription.deleted: sent when a subscription ends, whether it was canceled immediately or reached the end of a period it was scheduled to cancel at (Stripe: Cancel subscriptions).invoice.payment_failed: a payment for an invoice failed. Itsattempt_counttells you how many attempts have been made (Stripe: Automatic retries).invoice.paid: the invoice was paid. Stripe's docs say to retrieve the subscription and confirm it'sactivebefore extending access, since a paid invoice doesn't always make the subscription active.customer.subscription.pausedandcustomer.subscription.resumed: sent when the status changes to and frompaused.
Why the user still has access
- The app only listens for
checkout.session.completed. Access is granted on purchase and never touched again. Every later cancellation and failed renewal is ignored. - It handles
customer.subscription.deletedbut notupdated. Cancellations are caught, but a move topast_dueorunpaidarrives ascustomer.subscription.updated, so customers whose card fails keep access until (and unless) the subscription is eventually canceled. - Your failed-payment setting is "leave past due". After the final retry, Stripe can cancel the subscription, mark it unpaid, or leave it
past_due(Stripe: Subscription status after retries). If it's leftpast_dueand your app treatspast_dueas paid, that customer keeps access with no end date. Nodeletedevent ever comes. - The app checks a boolean set once. An
is_proorplancolumn set at checkout, with no code path that sets it back, will never reflect a status change. - Events failed and were never resent. If your webhook endpoint was down or returning errors, the events that should have revoked access may have exhausted their retries. Check the endpoint's Event deliveries in the Dashboard. Our webhook secret guide covers finding and resending failed events.
- Events arrived out of order. Stripe doesn't guarantee event order. If your handler writes the status from each event's payload, an older
updatedevent processed after a newer one can put a canceled customer back toactive. Fetch the subscription's current status from the API when an event arrives instead. - The user is matched to the wrong subscription. A customer with an old canceled subscription and a new active one, or two Stripe customers with the same email, can confuse a lookup that takes the first result. See How to find Stripe customers who paid but don't have access for matching traps.
Two cases that are not bugs
- Canceled at period end. When a subscription is set to cancel at the end of the period, it stays
activeand the customer keeps access until then. They can also undo it before the period ends (Stripe: Cancel at end of period). The customer portal works this way too: after canceling there, customers can renew until the billing period ends (Stripe: Configure the customer portal). Show "your plan ends on …" rather than revoking early. Watch out forcanceled_at: Stripe's API reference says that for these subscriptions it holds the time of the cancellation request, not the end of the period (API: Subscription object), so it's set while the subscription is still active. - Payment collection is paused. Pausing payment collection is meant as a grace period: the subscription stays
active, invoices keep being generated, and Stripe's docs say the customer keeps access (Stripe: Pause payment collection).
Decide your past_due policy
Stripe leaves this one to you. Some founders keep access during retries so a customer with an expired card isn't locked out while they update it; others cut access on the first failure. Either is fine if it's deliberate. Look at your retry settings: Stripe's docs say Smart Retries can run for 1 week to 2 months, with a recommended default of 8 tries within 2 weeks, and then your setting decides whether the subscription becomes canceled, unpaid or stays past_due (Stripe: Smart Retries). If you keep access during past_due, make sure the setting after the last retry is cancel or mark unpaid, so the grace period actually ends.
A handler that follows the status
The safest pattern is to treat every relevant event as a prompt to re-read the subscription and set access from its current status, rather than trusting each event's contents. A sketch, in the style of Stripe's Node library:
// Decide access from the subscription's current status, fetched from Stripe.
// Adjust GRACE to your own policy for past_due.
const ACCESS = new Set(["active", "trialing"]);
const GRACE = new Set(["past_due"]); // remove this line to cut access as soon as a renewal fails
async function syncAccess(subscriptionId) {
const sub = await stripe.subscriptions.retrieve(subscriptionId);
const hasAccess = ACCESS.has(sub.status) || GRACE.has(sub.status);
await db.users.update({ stripeSubscriptionId: sub.id }, {
hasAccess,
stripeStatus: sub.status,
cancelAtPeriodEnd: sub.cancel_at_period_end, // show "ends on …", don't revoke yet
});
}
// Call syncAccess(sub.id) for customer.subscription.updated,
// customer.subscription.deleted, customer.subscription.paused / resumed,
// and for invoice.paid / invoice.payment_failed (use the invoice's subscription).Verify the webhook signature before processing, record processed event IDs so retries and resends are harmless, and return 200 quickly.
Find the customers it already affects
- Export subscriptions from Stripe. In the Dashboard's subscriptions list, filter by status:
Canceled,Past dueandUnpaid. Export each list with the customer email and status. With the API,status=allon List subscriptions includes canceled ones. - Export who has access in your app. Every user (or team) currently on a paid plan, with email and, if you store it, the Stripe customer or subscription ID.
- Compare. Anyone on a paid plan in your app whose only subscriptions are
canceledorunpaidis getting the product for free. Anyone whose subscription ispast_dueneeds a decision under your policy above. Before acting, check they don't also have a separate active subscription, a one-time purchase or a comp. - Fix the data, then the code. Downgrade the affected users (consider emailing
past_duecustomers first, since some just need to update a card), then fix the handler so it doesn't happen again.
PaidCheck's free report runs step 3 for you: with a read-only restricted key and your user list (CSV, Supabase function or HTTPS endpoint), it lists users who still have access while their subscription is canceled, unpaid or past_due, along with failed webhook events from the last 30 days. Paid plans run that check daily and send an alert email when a check finds a new issue.
Who still has access after canceling?
Run the free leak report to see canceled, unpaid and past-due customers who can still use your app. Read-only restricted key, nothing stored.
Run the free leak report →FAQ
Which Stripe subscription statuses should get access?
Stripe's docs say you can provision access for trialing and active subscriptions, and should revoke access when a subscription becomes canceled or unpaid. past_due is the in-between state: the latest invoice payment failed or wasn't attempted, and Stripe may still retry it. Whether past_due keeps access is your decision. incomplete, incomplete_expired and paused subscriptions haven't been paid for the current period.
My user canceled, but Stripe still shows the subscription as active. Is that a bug?
No. If the subscription was set to cancel at the end of the period (cancel_at_period_end=true), it stays active until the period ends, so the customer keeps what they already paid for. Stripe sends customer.subscription.updated when the cancellation is scheduled and customer.subscription.deleted when the subscription actually ends.
Why does canceled_at have a date while the subscription is still active?
Stripe's API reference says that for a subscription canceled with cancel_at_period_end, canceled_at reflects the time of the cancellation request, not the end of the period. So don't use canceled_at being set as a signal to remove access. Use status.
How long does a subscription stay past_due?
It depends on your retry settings. Stripe's docs say Smart Retries can retry a number of times within 1 week to 2 months, with a recommended default of 8 tries within 2 weeks. After the final retry, your Dashboard settings decide whether the subscription is canceled, marked unpaid or left past_due. If it's left past_due, it can stay that way indefinitely.
Does pausing payment collection remove access?
No. Stripe's docs say that when you pause payment collection, the subscription stays active, invoices keep being generated, and the customer keeps access. That's different from a subscription whose status is paused.
Can PaidCheck fix access in my app?
No. PaidCheck reads Stripe with a read-only restricted key and compares it with who has access in your app (from a CSV, a Supabase function or an HTTPS endpoint you provide). It lists users whose subscription is canceled, unpaid or past_due but who still have access, so you can decide what to change. It never writes to Stripe or your app.
Check your own account
PaidCheck's free leak report runs these comparisons with a read-only Stripe restricted key. Upload a CSV of your users or connect nothing for Stripe-only checks. Nothing is stored.
Run a free leak report →