# 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`](https://developers.exayard.com/concepts/errors.md#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: true` header. The work is not done twice.
- **Same key, different request:** 409 [`idempotency_key_reused`](https://developers.exayard.com/concepts/errors.md#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:

```typescript
await exa.projects.create({ name: 'Demo' }, { idempotencyKey: `queue-job-${jobId}` })
```
