agent replayed a stale webhook payload: refunded an invoice that was already disputed
Fixes refunds issued from replayed or stale webhook payloads. Use when a refund fires for an invoice that was already disputed, refunded, or otherwise resolved. Not for legitimate duplicate webhooks handled idempotently or provider-side double refunds.
Fixing "agent replayed a stale webhook payload: refunded an invoice that was already disputed"
TL;DR
Never act on a webhook without checking the invoice's current state first. Deduplicate by event id, verify the webhook signature, and read the live invoice status before refunding. If it is already disputed, refunded, or paid, the stale event does nothing.
agent replayed a stale webhook payload: refunded an invoice that was already disputedSteps
Add event-id deduplication: store every processed webhook event id and skip any you have seen. Expected: Replayed payloads are ignored on sight.
Verify the webhook signature on every delivery before parsing the body. Expected: Forged or corrupted payloads never reach the refund logic.
Before any refund, fetch the live invoice and payment state; only refund when the state actually calls for it. Expected: A disputed or already-refunded invoice fails this check.
Make refunds idempotent with a key derived from the invoice id and reason. Expected: Even a double-trigger produces one refund.
Prevention
Webhooks are at-least-once delivery: replays are normal, not exceptional. Design every handler as check-then-act against live state, with the event id as the dedupe key. A handler that trusts the payload's embedded state will eventually act on the past.
Detection
Log event ids and flag duplicates. Alert on any refund where the invoice was already in disputed, refunded, or voided status. Reconcile refunds against disputes weekly; a refund on a disputed payment is the signature of this bug.
Recovery
If the provider allows, reverse the duplicate refund; if the invoice is under dispute, let the dispute process run and document the extra refund as evidence. Reconcile the ledger, notify the customer if money moved twice, and fix the handler before the next event.
When this applies
- A refund fired for an invoice that was already disputed or refunded
- The agent acted on a replayed or redelivered webhook
- Refund records reference a stale event payload
When it doesn't
- Two genuinely separate refunds were intended (check the reasons and amounts)
- The provider issued the duplicate refund itself (take it up with provider support)
- The webhook was fresh and the state check passed (then the refund logic itself is wrong)
Compatibility
Stripe API (webhook event ids, signature verification, idempotent refunds), Adyen (notification signatures, hmac validation), PayPal REST API v2 (webhook verification, idempotency). Any provider with redeliverable webhooks.
stale webhook caused a duplicate refund
refund issued on an already disputed invoice
replayed payment event triggered a second refund
Why it happens
The handler reads the payload ("payment failed, refund it") and acts, without noticing the payload is a redelivery of an event from before the dispute was filed. The embedded state in the payload is a snapshot of the past; only a live read shows the dispute that happened since.
Edge cases
- Out-of-order delivery: a newer event can arrive before an older one; always resolve against live state, not event order
- Dispute won by the merchant after a stale refund: you may be able to recover via the dispute evidence process
- Test webhooks replayed into production handlers: separate endpoints and signing secrets per environment
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_MOCw-KL7LqULWXBgaXDwdw