# Idempotency

> Retry a send or a creation safely with the Idempotency-Key header, without doing it twice.

Source: https://docs.maylee.app/idempotency

A network timeout does not mean a request failed: the email may have left, and only the response got lost. Retrying blindly would send it a second time. The `Idempotency-Key` header makes retries safe.

## Which requests accept it

Every `POST` that sends or creates something:

* `POST /drafts/send` (send an email)
* `POST /drafts`
* `POST /emails/bulk`
* `POST /labels`
* `POST /ai-label-rules`
* `POST /views`
* `POST /filters`
* `POST /auto-forward-rules`

The header is optional. Send it on every one of these requests, and **always** on `POST /drafts/send`.

```bash
curl -X POST https://api.maylee.app/api/v1/drafts/send \
  -H "Authorization: Bearer $MAYLEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"mailboxId":"cmbx1a2b3c4d5e6f7g8h9i0j","to":[{"email":"client@acme.com"}],"body":{"text":"Hello"}}'
```

## How it works

* The first successful response is stored for **24 hours**.
* Replaying the same key with the same body returns that response with the header `Idempotent-Replay: true`, **without running the operation again**.
* Only successful responses are stored: a failed call releases its key, so that you can retry once the request is fixed.
* The replay still counts as a request: it is subject to the per-minute rate limit and uses the same quota units as the original call.
* In the rare event the idempotency store is unavailable, requests go through without the guarantee rather than being refused.

## Guard rails

| Situation                                                 | Response                                                                                                            |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Same key, **different body**                              | `409 CONFLICT`. It is almost always a bug (a key generated once and reused in a loop), and replaying would hide it. |
| Same key while the original call is **still in progress** | `409 CONFLICT`. Retry in a moment.                                                                                  |
| Key longer than 255 characters                            | `400 VALIDATION_FAILED`                                                                                             |

## Choosing keys

Keys are scoped to the workspace. Use globally unique values such as UUIDs (`uuidgen`, `crypto.randomUUID()`, `uuid.uuid4()`), never a counter, so that two integrations sharing a workspace cannot collide. Generate a new key for every new operation, and reuse it only to retry that same operation.
