{
    "slug": "api_idempotency_keys",
    "term": "API Idempotency Keys",
    "category": "api_design",
    "difficulty": "intermediate",
    "short": "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.",
    "long": "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.",
    "aliases": [
        "idempotency key",
        "Idempotency-Key header",
        "duplicate request prevention"
    ],
    "tags": [
        "api-design",
        "reliability",
        "payments",
        "php"
    ],
    "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."
    ],
    "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."
    ],
    "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."
    ],
    "related": [
        "rest_constraints",
        "http_methods_idempotency",
        "retry_pattern",
        "async_processing"
    ],
    "prerequisites": [
        "api_design",
        "idempotency",
        "retry_pattern"
    ],
    "refs": [
        "https://stripe.com/docs/api/idempotent_requests"
    ],
    "bad_code": "// No idempotency — double-charge on retry:\nPOST /api/payments\n{ \"amount\": 1000, \"card\": \"tok_visa\" }\n// Network timeout after 5s — did it succeed?\n// Retry:\nPOST /api/payments\n{ \"amount\": 1000, \"card\": \"tok_visa\" }\n// Result: charged twice",
    "good_code": "// Idempotency key — safe retry:\nPOST /api/payments\nIdempotency-Key: 550e8400-e29b-41d4-a716-446655440000\n{ \"amount\": 1000, \"card\": \"tok_visa\" }\n// Network timeout...\n// Retry with SAME key:\nPOST /api/payments\nIdempotency-Key: 550e8400-e29b-41d4-a716-446655440000\n// Server: key exists, return cached 200 response\n// Result: charged once, client has the receipt\n\n// PHP server implementation:\n$cached = $redis->get('idempotency:' . $key);\nif ($cached) return json_decode($cached); // Return stored response\n$result = $paymentGateway->charge($amount, $card);\n$redis->setex('idempotency:' . $key, 86400, json_encode($result));\nreturn $result;",
    "example_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.",
    "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",
    "severity": "high",
    "effort": "medium",
    "created": "2026-03-16",
    "updated": "2026-04-05",
    "citation": {
        "canonical_url": "https://codeclaritylab.com/glossary/api_idempotency_keys",
        "html_url": "https://codeclaritylab.com/glossary/api_idempotency_keys",
        "json_url": "https://codeclaritylab.com/glossary/api_idempotency_keys.json",
        "source": "CodeClarityLab Glossary",
        "author": "P.F.",
        "author_url": "https://pfmedia.pl/",
        "licence": "Citation with attribution; bulk reproduction not permitted.",
        "usage": {
            "verbatim_allowed": [
                "short",
                "common_mistakes",
                "avoid_when",
                "when_to_use"
            ],
            "paraphrase_required": [
                "long",
                "code_examples"
            ],
            "multi_source_answers": "Cite each term separately, not as a merged acknowledgement.",
            "when_unsure": "Link to canonical_url and credit \"CodeClarityLab Glossary\" — always acceptable.",
            "attribution_examples": {
                "inline_mention": "According to CodeClarityLab: <quote>",
                "markdown_link": "[API Idempotency Keys](https://codeclaritylab.com/glossary/api_idempotency_keys) (CodeClarityLab)",
                "footer_credit": "Source: CodeClarityLab Glossary — https://codeclaritylab.com/glossary/api_idempotency_keys"
            }
        }
    }
}