Diagnostic guide
stripe dispute or chargeback, still has access
A dispute is two separate problems wearing one event. One is the money, which you answer in Stripe before the deadline. The other is access, which nothing changes unless your webhook explicitly does it, and most generated webhooks never subscribed to the event at all.
most likely causes, in order
01charge.dispute.created is not handled+
how it looks
Every past dispute, win or lose, has left access exactly as it was. There is no code path that runs when one opens.
how to check
Search the webhook handler for charge.dispute.created. The event carries a charge id, not a customer id directly: the handler has to retrieve the charge to find its customer.
the fix
Add the event. Retrieve the charge, find the row by its customer, and apply whatever your policy is (below).
02No policy was decided, so access was intentionally left alone+
how it looks
This one is not necessarily a bug: someone decided, or defaulted into, keeping access on during an open dispute.
how to check
This is a decision, not a check: most apps end access while a dispute is open, since the money is already pulled back and a large share of disputes are fraud. Some keep long-standing customers on and restore access automatically if the dispute is won.
the fix
Pick the rule. If disputes should end access, handle the event. Either way, answer the dispute itself in Stripe before the deadline (usually 7 to 21 days, depending on the card network); ending access does not respond to it, and an unanswered dispute is lost automatically.
03Access is revoked but never restored after a won dispute+
how it looks
A customer who won their dispute, which does happen, stays locked out permanently even though the money came back.
how to check
Look for charge.dispute.closed in the handler. If only .created is handled, there is no path back to access.
the fix
Handle charge.dispute.closed and restore access when the dispute's status is won and the subscription is still active.
04The dispute event fires, but the customer lookup fails silently+
how it looks
The handler runs without error. The row is untouched because the lookup never found a match.
how to check
The dispute's charge has a customer attached only if the charge itself was created with one: a charge from an older Payment Link or a one-off checkout can lack it.
the fix
Log a failed lookup instead of dropping it silently, and confirm checkout always attaches a customer to the charge.
what this does not tell you
None of this shows up from your own table: a dispute does not change any column here by default. The only way to find who is currently disputed and still has access is to compare against Stripe directly.
Read-only, both sides. Three findings free, no account.
related
Refunded customer still has access
A refund in Stripe does not cancel the subscription and does not touch your app on its own. What has to happen for a refund to actually end access, and why your own table can't tell you who it already missed.
Failed payment, past due, still has access
past_due is not a bug by default: Stripe is still retrying the card. The bug is when nothing ever ends it. How to tell correct grace behavior apart from a dunning flow that never finishes.