---
title: "Accept idempotency keys on POST · Agent Wranglers"
url: https://agentwranglers.se/playbook/api-design/reliability/idempotency-keys
updated: 2026-10-20
licence: "All rights reserved"
---

# Accept idempotency keys on POST

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Reliability](https://agentwranglers.se/playbook/api-design/reliability)
- Rule: Let clients send an `Idempotency-Key` header on POST and PATCH, and return the first result for a repeated key instead of acting twice.
- How: Scope each key to the caller. Store it with a hash of the request body and the response, for at least 24 hours. A repeat with the same body gets the stored response. A repeat with a different body gets 422, and a repeat while the first is still running gets 409. Require the key on payments and orders, and answer 400 when it is missing. The header comes from the IETF draft `draft-ietf-httpapi-idempotency-key-header`, so name it and your expiry in the docs. See also [save each form only once](https://agentwranglers.se/playbook/app-ui/forms/single-submit).
- Check: Send the same POST with the same key twice. One order exists, and both answers are identical. The same key with a changed body gets 422.

Example:

```http
POST /payments HTTP/1.1
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324
Content-Type: application/json

{"orderId":"ord_8f2k","amount":12950,"currency":"SEK"}
```

```js
async function withIdempotency(callerId, key, body, handle) {
  const bodyHash = sha256(canonicalJson(body))
  const claimed = await store.claim({ callerId, key, bodyHash, ttlHours: 24 })
  if (!claimed.isNew) {
    if (claimed.bodyHash !== bodyHash) return problem(422, 'idempotency-key-reused')
    if (!claimed.response) return problem(409, 'request-in-progress')
    return claimed.response
  }
  const response = await handle(body)
  await store.saveResponse({ callerId, key, response })
  return response
}
```

```python
def with_idempotency(caller_id, key, body, handle):
    body_hash = sha256(canonical_json(body))
    claimed = store.claim(caller_id=caller_id, key=key, body_hash=body_hash, ttl_hours=24)
    if not claimed.is_new:
        if claimed.body_hash != body_hash:
            return problem(422, "idempotency-key-reused")
        if claimed.response is None:
            return problem(409, "request-in-progress")
        return claimed.response
    response = handle(body)
    store.save_response(caller_id=caller_id, key=key, response=response)
    return response
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers api design playbook: https://agentwranglers.se/playbook/api-design/reliability/idempotency-keys`.

- Next: [Stop lost updates with ETags](https://agentwranglers.se/playbook/api-design/reliability/optimistic-concurrency)
- [Reliability](https://agentwranglers.se/playbook/api-design/reliability)
