← Back to blog

You refunded a customer three months ago. They still have access.

September 11, 2026

Issuing a refund feels like a closed loop — support clicks refund, Stripe sends the money back, done. Nothing about that action tells your application anything. Refunding a charge and revoking a customer’s access are two entirely separate operations in Stripe’s model, and most integrations only ever built one of them, because in testing, the two normally happen together — someone cancels, gets refunded, loses access, everything lines up. The gap only shows up in the case nobody tests: a refund issued without canceling the subscription underneath it.

Why a refund doesn’t touch the subscription

charge.refunded fires on the charge object — it says money moved back to the customer. It says nothing about the subscription that charge happened to be paying for, because Stripe has no way of knowing whether you meant “refund this one payment” or “this customer should no longer have the product.” Those are different business decisions, and Stripe correctly leaves that decision to you. Support teams refund charges constantly for reasons that have nothing to do with access — a duplicate charge, a goodwill gesture, a billing dispute resolved in the customer’s favor for one specific month. If your access logic isn’t listening for this at all, none of those refunds do anything to what the customer can still do in your product, including the ones where they very much should lose access.

The dispute case is worse, not better

A chargeback starts with charge.dispute.created, and this one is genuinely urgent — not just because the customer is about to get their money back regardless of whether you agree, but because Stripe charges you a dispute fee on top of the reversed payment, and every additional day the customer keeps using the product is value delivered on a transaction you’re about to lose money on twice over. A dispute is also frequently a sign of first-party fraud (a customer who has no intention of paying, disputing the charge instead of canceling normally) rather than a legitimate billing complaint — exactly the case where continued access costs the most and helps the least.

Two-minute check: Stripe Dashboard → Payments → filter by Refunded, or Disputes tab directly. Pick five recent refunds and disputes and check whether the customer tied to each one currently has an active subscription and active access in your app. If the subscription is still active and nothing in your logs shows an access change around the refund date, you’ve found this gap.

Why the fix isn’t just “cancel on refund”

The tempting fix — automatically cancel the subscription the moment any refund happens — is wrong often enough to cause its own problems. A support agent refunding one month as a retention gesture for an otherwise happy, currently-paying customer does not want that customer’s active subscription canceled as a side effect. The correct behavior depends on why the refund happened, which Stripe’s event doesn’t tell you — it only tells you that money moved. That distinction has to live in your own application: a refund reason your support tool captures, or a simple rule like “a refund for the customer’s most recent invoice, issued within N days of that invoice, revokes access; anything else is reviewed manually.”

What actually needs building

What we built this for

I built Venwai after running into this same shape of gap from several different angles — a webhook that never configured in live mode, a subscription that never fired the event it was supposed to. Venwai connects to Stripe with a read-only key, polls one endpoint you expose on your own app once a day, and diffs who Stripe currently says has a valid, paid subscription against who your app grants access to — including customers whose payment has since been refunded or disputed, regardless of which specific webhook was or wasn’t wired up to catch it.

Free while it’s in beta, no card required: venwai.com.