Designing Idempotent APIs for Payments
How idempotency keys, deduplication, and clear status models prevent double charges and support safe retries.
Payment APIs fail in the real world: timeouts, client retries, and mobile apps that submit twice. Idempotency is how you make “pay once” a guarantee, not a hope.
What idempotency means
The same request, sent multiple times with the same idempotency key, produces one financial effect. The client can safely retry on network errors.
Implementation pattern
- Client sends
Idempotency-Key: <uuid>header (or body field) onPOST /payments. - Server checks a store (Redis or DB table): key + route + tenant.
- First time: process payment, persist result, store key → response
201. - Duplicate: return the same stored response without re-charging.
Store at least: key, status, response body hash, created_at, expiry (e.g. 24h).
Status model
Use explicit states: pending, succeeded, failed, refunded. Never infer from HTTP code alone. Webhooks and polling should read the same canonical record.
Edge cases
- Concurrent duplicates — Use DB unique constraint on idempotency key or distributed lock during processing.
- Partial failure — If charge succeeded but response failed, retry must return success with same transaction id.
- Refunds — New idempotency key per refund attempt.
What to log
Log idempotency key, payment id, and outcome—never full PAN. Makes dispute resolution faster.
Takeaway
Idempotent payment APIs are non-negotiable for fintech. Design them on day one; retrofitting is painful and expensive. Pair with webhook signatures and reconciliation jobs for a complete story.