Idempotency keys in payment flows
Payment providers retry. Networks partition. Your webhook endpoint will see the same event more than once.
The fix is not “make the provider stop retrying.” The fix is designing handlers where processing the same event twice produces the same outcome as processing it once.
Start with a stable idempotency key
Stripe sends an event ID on every webhook payload. Store it before you do any side effects:
IF event_id EXISTS IN processed_events THEN RETURN 200
ELSE INSERT event_id, BEGIN transaction, ...
The insert must be the first durable write. If you charge a card before recording the event ID, a crash between those two steps creates a double-charge risk.
Separate “received” from “completed”
A webhook handler should acknowledge quickly and defer heavy work to a queue when possible. Two states matter:
- Received — we’ve seen this event ID and won’t process it again.
- Completed — all downstream effects finished successfully.
Partial completion is the hard case. If step three of five fails, your retry must resume from step three, not restart from step one.
Test with deliberate duplication
In integration tests, POST the same payload twice and assert:
- No duplicate ledger entries
- No duplicate subscription activations
- HTTP 200 on both requests
Reliability in fintech is not about preventing failure. It’s about making failure recoverable without corrupting state.