← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

API Idempotency Keys

API Design PHP 7.0+ Intermediate
debt(d8/e5/b5/t7)
d8 Detectability Operational debt — how invisible misuse is to your safety net

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.

e5 Effort Remediation debt — work required to fix once spotted

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.

b5 Burden Structural debt — long-term weight of choosing wrong

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.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

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.

About DEBT scoring →

Also Known As

idempotency key Idempotency-Key header duplicate request prevention

TL;DR

A client-generated unique key sent with non-idempotent requests — the server stores the response and returns it unchanged if the same key is received again, preventing duplicate operations.

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

If two requests with the same idempotency key carry different payloads, you must return an error — silently accepting the second request with different data breaks the contract and causes subtle bugs.

Common Misconception

Retrying with the same payload is safe because the server checks for duplicates — without idempotency keys the server has no way to know if a request is a retry or a new operation with identical data.

Why It Matters

A network timeout on a payment POST — did it succeed or not? Without idempotency keys, retrying risks charging twice; with them, the retry is safe and returns the original result.

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

💡 Note
The bad example processes every POST independently; a network timeout causes the client to retry and the user is charged twice — the fix deduplicates on the server using the client-supplied key.
✗ Vulnerable
// 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
✓ Fixed
// 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;

Added 16 Mar 2026
Edited 5 Apr 2026
Views 53
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 4 pings S 0 pings M 2 pings T 1 ping W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W
No pings yet today
PetalBot 1
Amazonbot 9 Perplexity 6 SEMrush 5 Scrapy 5 Ahrefs 4 Google 3 Claude 3 Unknown AI 2 ChatGPT 1 Bing 1 PetalBot 1
crawler 38 crawler_json 2
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Medium
⚡ Quick Fix
Accept an Idempotency-Key header on POST endpoints — store the key and response; return the cached response for duplicate requests with the same key within a 24-hour window
📦 Applies To
PHP 7.0+ any web api
🔗 Prerequisites
🔍 Detection Hints
POST endpoint for payment or order creation without idempotency key support; client retries causing duplicate charges
Auto-detectable: ✗ No semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Medium ✗ Manual fix Fix: Medium Context: Function Tests: Update


✓ schema.org compliant