Skip to main content
ADR 0005 · Accepted · Phase 6
Status: Accepted · Phase: 6 · Supersedes: none · Superseded by: none

Context

Payment APIs are retried: networks time out, clients crash after sending and before reading, and callers retry on 5xx. A retry that creates a second payout is the worst failure a payments API has.

Decision

Callers supply an Idempotency-Key. The gateway stores the key with a hash of the request and the response, scoped per tenant, and distinguishes three cases: Request signatures additionally carry a per-request nonce.

Consequences

Good. A retry is safe, and the client gets the original outcome rather than a second effect. Downstream idempotency reinforces it: the chain driver and the bank rails are both idempotent on the caller’s key, so even a partially completed saga does not double-broadcast or double-pay. The conflict case is the one that matters. Silently returning the first response when the body differs would hide a client bug behind a success, and the client would never learn its second, different request never happened. The nonce was not in the original design. Signatures covering only method, path, timestamp and body meant two identical requests in the same millisecond produced the same signature — so replay protection rejected a legitimate idempotent retry. Found by writing the retry test, not by reasoning about it. Costs. Keys need storage and expiry (24h). Clients that omit the key get no protection, and the API cannot force them.

Alternatives

Server-derived idempotency from a content hash alone — rejected. Two genuinely distinct transfers with identical parameters are legitimate; only the caller knows whether it means “again” or “the same one”. Returning the first response on a body mismatch — rejected as above: it converts a client bug into a silent wrong outcome.