agent double-refunded a customer: refund script ran twice on retry
Stops refund scripts from charging back a customer twice when a retry re-executes. Use when a refund job reran or an operator reports two refunds for one payment. Not for first-time refund failures or refund status webhook handling.
agent double-refunded a customer: refund script ran twice on retry
TL;DR
Wrap every refund in an idempotency key derived from the payment plus the refund reason, and check for an existing refund before creating a new one. If a double refund already happened, issue no third refund; instead record the over-refund and arrange to re-collect the difference. Most refund endpoints dedupe on the key, so a crashed-and-retried script stops being a double-charge vector.
agent double-refunded a customer: refund script ran twice on retryFix it
- Derive one stable idempotency key per logical refund. Compose it from payment id + refund reason + date, e.g.
refund-[payment_id]-partial-[yyyymmdd]. Success check: re-running the script with the same inputs returns the original refund object instead of creating a new one. - Query before you create. List refunds for the charge and skip any charge that already has a refund matching your reason. Success check: a pre-flight list returns 0 unmatched charges before the create call.
- Persist a processed list. Record each completed refund (charge id, refund id, timestamp) in durable storage, and read it at startup. Success check: after a mid-run crash, the restarted script skips all recorded rows.
- Recover an existing double refund. Confirm both refunds posted, void nothing automatically; log the overage, notify ops, and re-invoice the extra amount rather than attempting a third adjustment. Success check: the customer ledger shows exactly one net refund after reconciliation.
When this applies
- A refund script, cron, or agent retried and a customer shows two refunds for one payment.
- You are building an agent that issues refunds and want retry safety.
When it doesn't
- The refund failed the first time and never posted; that is a create-retry problem, not a double refund.
- You are reconciling refund webhooks, not issuing refunds from a script.
Compatibility
- Pattern: any agent harness (scheduled job, retry loop, multi-step checkout agent) calling Stripe Refunds, Adyen refunds, or PayPal refunds endpoints.
- Works on Stripe API 2024-06 and later (idempotency keys supported on POST /v1/refunds).
Variant: refund posted twice after a network timeout
The create call timed out, the agent retried, and both calls actually landed. Same fix: the idempotency key turns the retry into a no-op replay of the first call.
Variant: batch refund job restarted from zero
The whole batch re-ran because the first run crashed. Same fix plus a checkpoint file: record progress after every N refunds so a restart resumes mid-list.
Why it happens
Refund create calls are not safe to retry bare. If the first call succeeded but its response was lost (timeout, crash, deploy), the retry is indistinguishable from a brand-new refund request, so the provider happily creates a second one.
Edge cases
- Partial refunds: a second refund for a DIFFERENT amount is legitimate; scope your pre-flight check to the same amount and reason, not just the same charge.
- A refund that failed validation may still be retried with a corrected amount; use a new key when the amount changes, never reuse the old one.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_BuVSScCWJrGECnhbJvw9OQ