How Do I Refund a Stripe Payment, and Does It Take Back the Customer's Access?
6 min read
Refunding a payment in Stripe and removing the customer's access are two separate actions. Refunding sends the money back. It does not touch your app. Unless you have wired up something to revoke access when a refund happens, the customer gets their money back and keeps everything they paid for.
Why this happens
When a customer paid, something in your app flipped a switch: it set a flag like
is_pro = true, or created a subscription row, or added them to a "paid" group.
That switch was flipped by your checkout or your webhook on the payment
event. Nothing flips it back on its own.
A refund is a different event entirely. Stripe processes the money on its side
and, if you're listening, fires a charge.refunded event. But an AI agent that
built "let users pay to unlock Pro" almost never builds the reverse. You asked
for the unlock, so it built the unlock. You never asked "and take it back if I
refund them," so that half doesn't exist. The demo worked, the money moved, and
the revoke path was never written.
How to check
- In the Stripe Dashboard, open a real (or test) payment and click Refund. Issue the refund.
- Now log in to your app as that customer. Can they still see the paid features? If yes, refunding does not revoke access in your app. That is the default.
- Check whether you even listen for the event. In the Stripe Dashboard go to
Developers, Webhooks, open your endpoint, and look at the event list. If
charge.refunded(orrefund.created) is not there, nothing in your app ever hears about refunds.
The fix
- Decide the two actions are separate, and own both. A clean refund means: money back in Stripe, and access removed in your app. Do them together every time.
- For one-off, manual refunds, revoke by hand for now. After you click
Refund in Stripe, go into your own database or admin and set the customer back
to unpaid (
is_pro = false, or remove them from the paid group). Slow, but correct, and fine while you have a handful of users. - To automate it, handle the refund webhook. Add
charge.refundedto your Stripe webhook endpoint. In your handler, find the customer from the event'scustomerormetadata, and set them back to unpaid, the exact reverse of what your payment handler does. - For subscriptions, cancel the subscription too. A refund of one invoice
does not stop the next charge. If they're on a plan, also cancel the
subscription in Stripe (or via
customer.subscription.deleted) so they aren't billed again next month. - Verify the whole loop in test mode. Use a test card, pay, refund, and
confirm access is gone and no further charges are scheduled. Trigger the event
with
stripe trigger charge.refundedfrom the Stripe CLI if you want to test the handler directly.
The trap to avoid
Do not assume refunding in Stripe "cancels everything." It cancels the money, not the access, and for subscriptions it does not even stop the next charge. The opposite trap is just as common: revoking access in your app but forgetting to actually refund the money in Stripe, so the customer is locked out and still charged. Both halves, every time, or you have an angry customer either way.
Where this fits
Refunds are a normal part of having real users, and the revoke path is exactly the kind of thing that never shows up in a demo because you don't refund yourself. The free Readiness Report checks whether your payment flow has a reverse gear, and whether refunds and cancellations actually change what a user can see. If you'd rather have the handlers written and tested for you, that's what the Finishing Pass covers. It's the same gap behind a webhook that never fired and a canceled subscription that still has access: the money moved, but your app never heard about it.