Idempotency
Any POST, PATCH, PUT or DELETE on /v1 can carry an Idempotency-Key header:
POST /v1/projects
Idempotency-Key: create-demo-project-2026-10-05
The key is 1 to 255 characters: letters, digits, underscore and hyphen. A malformed key is refused with 400
invalid_idempotency_key.
What happens
- First call: we run it and keep the response for 24 hours.
- Retry with the same key and the same request: we send the kept response again, with an extra
Idempotent-Replayed: trueheader. The work is not done twice. - Same key, different request: 409
idempotency_key_reused. Pick a new key. This stops a client that mixed up its keys from applying the wrong change.
"The same request" means the same method, path and body. The query string is not part of it.
Whose keys they are
Keys are kept apart per caller. For an app, that is per app, per key and per company the call works in. Two apps, two
keys of one app, or one key in two companies never share a kept response, even with the same Idempotency-Key.
What is never kept
- Server errors (5xx). Retry them with the same key; the next call runs again.
- 400 responses. Fix the request and retry with the same key.
- Streams (server-sent events) and chunked responses.
- Response bodies over 256 KiB.
There is no lock while a call is running. If two calls with the same new key arrive at the same moment, both run. Send a retry only after the first call has answered or timed out.
SDK support
@exayard/sdk sends an Idempotency-Key on every POST, PATCH, PUT and DELETE by itself, so its own retries
never do the work twice. Pin your own key when you retry from a persistent queue, so a retry after a restart uses the
same key:
await exa.projects.create({ name: 'Demo' }, { idempotencyKey: `queue-job-${jobId}` })