Diagnostic guide

refunded customer still has access

Refunding a payment is a Stripe action. Ending access is a decision you have to make and code separately: the two are not the same event, and Stripe has no way to know which one you want without being told.

most likely causes, in order

01charge.refunded is not handled at all+

how it looks

Every refund you have ever issued, from the dashboard or through support, has left access untouched. Nobody notices, because a refund reads as a resolution, not a new problem.

how to check

Search the webhook handler for charge.refunded. If it is absent, nothing downstream of the refund button runs.

the fix

Add the event. Decide the policy first (below), then write it.

02No policy was ever decided, so nothing was written+

how it looks

There is no bug to find here: charge.refunded is unhandled on purpose, or by omission, and nobody has decided whether it should revoke access.

how to check

This is a decision, not a check: does a refund on this product mean the customer should lose access? A partial goodwill refund usually should not. A full refund on request, or a fraud refund, usually should.

the fix

Pick the rule. If refunds should end access, handle the event. If they should not, this is expected behavior; mark it as such rather than treating it as a bug.

03The refund is issued from the Stripe dashboard, not the app+

how it looks

Refunds you process yourself, by hand, in Stripe never show up in your own logs, because nothing in your app triggered them.

how to check

A manual refund in the dashboard fires the same charge.refunded event as one issued through the API. If access still is not revoked, the webhook is the problem, not the refund path.

the fix

Same fix as above: handle the event. It does not matter how the refund was created.

04A partial refund is treated the same as a full one, or the reverse+

how it looks

A customer who received a small partial credit lost all access instantly, or a customer who got a full refund kept everything.

how to check

The event includes amount_refunded alongside the charge's original amount. A handler that revokes access on any charge.refunded, regardless of size, cannot tell the two apart.

the fix

Compare amount_refunded to the charge's amount. Revoke only when they are equal, unless your policy says otherwise.

what this does not tell you

None of this shows up by querying your own database: a refund does not change any column here by default, and that is exactly the problem. The only way to find who was refunded and still has access is to compare against Stripe directly, customer by customer.

Read-only, both sides. Three findings free, no account.

related