Idempotency
Every POST to a mutating endpoint carries an Idempotency-Key header. Replay the same key + body to safely retry after a network glitch or timeout. The second call returns the original response and never re-applies the side effect.
When it's required
On every POST against /api/v1/*. If PUT endpoints are added in future, the same requirement applies. GET and DELETEare not idempotency-tracked, since they either don't mutate or are idempotent in the HTTP-spec sense.
A missing header on a tracked verb returns 400 missing_idempotency_key.
Key conventions
- UUIDv4 is the canonical choice. Any string up to 255 bytes works.
- Generate one key per logical operation, not per HTTP call. Retries reuse the same key; new operations get a new one.
- Don't derive the key from the request body. That defeats the point of the body-mismatch check below.
- Records expire after 24 hours. Reuse the key beyond that window and the next call is treated as a fresh operation.
Replay semantics
First call: the API runs the handler, persists the response body and status, and returns it. Subsequent calls with the same key + same body: the API skips the handler entirely and returns the persisted response with an extra header:
The replay returns the original request_id, not a fresh one. That is deliberate. It lets you correlate logs across retry attempts.
Body mismatch (409)
We hash the request body and store it alongside the key. If a later request reuses the key but the body hash differs, we don't silently re-run anything. You get back 409 key_reused_with_different_body:
Common pitfall: timestamps in the body
ordered_at: new Date().toISOString() regenerated on each retry), every retry will look like a different body. Either freeze that value at retry-loop creation time, or omit it from the body and let the server stamp it.Worked example
Errors are cached too
Replay caching applies to any response, including 4xx and 5xx. If your first call returned 500 upsert_failed, the replay returns that same 500. Use a new key when you want to actually retry.
This is intentional. Replays exist to keep side effects from happening twice, not to mask past failures.
