Idempotency keys

Make retries safe with the Idempotency-Key header.

Send an Idempotency-Key header on any non-GET request you might retry. If the network drops a reply, you resend the same request with the same key and get the stored response back instead of a second write.

The key is fingerprinted against the method, path and body. Same key, same request, the api replays the stored response. Same key, different body, it answers 422 idempotency_key_reused.

A retryable create
curl -X POST http://127.0.0.1/api/conversations \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer fk_<key>' \
  -H 'idempotency-key: 9a1c2b3e-4d5f-4a6b-8c7d-9e0f1a2b3c4d' \
  -d '{"channel_id":"<channel-id>","body":"Where is my order?"}'

What the header does

SituationAnswer
Same key, same fingerprint, already completedThe stored response body and status, replayed.
Same key, different body422 idempotency_key_reused.
Same key, first request still running409 idempotency_key_in_flight with Retry-After: 1.
The request answered 5xxThe reservation is released, so a retry runs the work again.

The reservation commits in its own transaction, so a concurrent retry sees the in-flight row rather than both requests winning. Keys are scoped to the tenant and the caller, so two callers who pick the same value do not collide. A completed row expires after 24 hours and the sweep reaps it.