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:

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

Why the user still has access

Two cases that are not bugs

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

  1. Export subscriptions from Stripe. In the Dashboard's subscriptions list, filter by status: Canceled, Past due and Unpaid. Export each list with the customer email and status. With the API, status=all on List subscriptions includes canceled ones.
  2. 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.
  3. Compare. Anyone on a paid plan in your app whose only subscriptions are canceled or unpaid is getting the product for free. Anyone whose subscription is past_due needs 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.
  4. Fix the data, then the code. Downgrade the affected users (consider emailing past_due customers 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.

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.