Skip to content

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

  1. In the Stripe Dashboard, open a real (or test) payment and click Refund. Issue the refund.
  2. 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.
  3. 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 (or refund.created) is not there, nothing in your app ever hears about refunds.

The fix

  1. 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.
  2. 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.
  3. To automate it, handle the refund webhook. Add charge.refunded to your Stripe webhook endpoint. In your handler, find the customer from the event's customer or metadata, and set them back to unpaid, the exact reverse of what your payment handler does.
  4. 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.
  5. 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.refunded from 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.