Short answer
Open the charge and check its refunds. If the customer is already refunded, mark the order refunded in your database and stop. Then make refunds safe to repeat: check the charge first, send an idempotency key, and update your records from the refund.created or charge.refunded webhook, comparing amount_refunded with the charge amount before marking the order fully refunded.
Rather not fix it yourself? Stripe Billing Fix and Setup handles it at a fixed price, agreed before work starts.
Why the refund ran twice, and the fix
01
Someone refunded it in the Dashboard first
“Sent when a charge is refunded, including partial refunds.” Stripe, Refunds (charge.refunded event)
A refund made by hand in the Dashboard doesn’t update your database unless you listen for it. Your code then tries again. Handle refund.created or charge.refunded so both paths agree, and remember charge.refunded also fires for partial refunds.
02
The refund request was sent twice
“The charge you’re attempting to refund has already been refunded.” Stripe, Error codes (charge_already_refunded)
A double click, a retry after a timeout, or a webhook delivered twice sends the same refund again. Pass an idempotency key for each refund attempt, such as the order ID plus a refund request ID, so a repeat returns the first result. Keys expire after 24 hours.
await stripe.refunds.create( { payment_intent: order.paymentIntentId }, { idempotencyKey: `refund-${order.id}-${refundRequest.id}` }, );03
The code refunds the wrong charge
“Use a different charge ID only if you selected the wrong charge and intended to refund another charge.” Stripe, Error codes (charge_already_refunded)
A stored charge ID that points at an older payment refunds the wrong one, or one that was already refunded. Look up the charge from the order’s PaymentIntent before refunding.
04
Partial refunds add up
Several partial refunds can reach the full amount. Check amount_refunded on the charge before you refund more, and show staff what is left.
Checked against the vendor’s docs on : Stripe docs: Error codes; Stripe docs: Refunds; Stripe API reference: Idempotent requests.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- $950Stripe Billing Fix and SetupSubscriptions, webhooks, customer portal and failed payments set up or fixed, tested in Stripe test mode.See scope and start
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.See scope and start
“Working with Simon makes everything run so smoothly and better than I could ask for it. Amazingly communicative, extremely knowledgeable of coding and the backend processes. I strongly recommend.”
Related: Stripe idempotency key in use · Stripe webhook 200 but database not updated · Stripe PaymentIntent already succeeded · Stripe card_not_supported