API Idempotency Keys
debt(d8/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7), +1 because the absence of idempotency key support is an architectural omission rather than a code pattern easily flagged. While semgrep is listed as a potential tool, detection_hints.automated is explicitly 'no' — the real detection often happens only in production when duplicate charges or duplicate orders appear after retries, pushing this toward d9 'silent in production until users hit it'. Settled on d8 as semgrep rules could theoretically flag POST payment endpoints lacking the header check, but this requires custom configuration and is not a default catch.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes accepting an Idempotency-Key header, storing keys, and returning cached responses — this requires adding middleware or a service layer for key storage (likely a new database table or cache layer), modifying POST endpoint handlers, and implementing atomic check-and-store logic. It's more than a one-line fix but typically contained within the API layer rather than being a full architectural rework.
Closest to 'persistent productivity tax' (b5). Once implemented, idempotency key infrastructure becomes a cross-cutting concern for all state-changing POST endpoints. Every new payment or order endpoint must integrate with the key storage system. The applies_to contexts (web, api) and the tags (api-design, reliability, payments) indicate it touches a wide surface area. It's not quite b7 since it doesn't reshape every change in the system, but it does impose an ongoing tax on API development workflows.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception is precisely that developers believe retrying with the same payload is inherently safe because 'the server checks for duplicates' — this is a deeply held wrong assumption. Common mistakes reinforce this: using request body hash as a key (seems logical but fails for different users with same data), not making storage atomic (race conditions are non-obvious), and applying keys to GET requests (misunderstanding idempotency itself). The concept name 'idempotency' suggests mathematical idempotence, but the implementation requirements (client-generated unique keys, atomic storage, TTL expiration) are significantly more nuanced than most developers expect.
Also Known As
TL;DR
Explanation
POST requests are not idempotent — retrying a failed payment POST could charge twice. Idempotency keys solve this: client generates a UUID per operation, sends it as Idempotency-Key header, server stores (key → response) and on retry returns the cached response without re-executing. Keys typically expire after 24 hours. Implementation: check key in Redis before processing, store result atomically with key, return stored result on duplicate. Stripe, PayPal, and all major payment APIs implement this pattern.
Diagram
sequenceDiagram
participant C as Client
participant S as Server
participant R as Redis
C->>S: POST /payments<br/>Idempotency-Key: uuid-123
S->>R: GET idempotency:uuid-123
R-->>S: null - not seen before
S->>S: Process payment
S->>R: SET idempotency:uuid-123 = response
S-->>C: 200 OK - charged
Note over C,S: Network timeout - client retries
C->>S: POST /payments<br/>Idempotency-Key: uuid-123
S->>R: GET idempotency:uuid-123
R-->>S: cached response
S-->>C: 200 OK - same response, not charged again
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Using request body hash as idempotency key — two different users making the same purchase would share a key.
- Not expiring idempotency keys — storing responses forever wastes storage.
- Not making the key storage and operation atomic — a race condition between checking and storing creates duplicates.
- Idempotency keys on GET requests — GET is already idempotent; keys are only needed for state-changing operations.
Avoid When
- Do not use idempotency keys as a substitute for proper transaction handling — the key prevents duplicate requests, not concurrent ones.
- Avoid short key expiry windows for payment flows where network timeouts can cause retries hours later.
When To Use
- Use idempotency keys for any non-idempotent POST operation that could be safely retried: payments, order creation, message sending.
- Implement server-side key storage for the duration clients may reasonably retry (typically 24 hours to 7 days).
- Return the cached response immediately when a duplicate key is received — do not re-execute the operation.
Code Examples
// No idempotency — double-charge on retry:
POST /api/payments
{ "amount": 1000, "card": "tok_visa" }
// Network timeout after 5s — did it succeed?
// Retry:
POST /api/payments
{ "amount": 1000, "card": "tok_visa" }
// Result: charged twice
// Idempotency key — safe retry:
POST /api/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{ "amount": 1000, "card": "tok_visa" }
// Network timeout...
// Retry with SAME key:
POST /api/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
// Server: key exists, return cached 200 response
// Result: charged once, client has the receipt
// PHP server implementation:
$cached = $redis->get('idempotency:' . $key);
if ($cached) return json_decode($cached); // Return stored response
$result = $paymentGateway->charge($amount, $card);
$redis->setex('idempotency:' . $key, 86400, json_encode($result));
return $result;