# Idempotency

Networks fail at the worst moments: you POST a send, the connection drops, and now you don't know whether the email went out. Idempotency lets you retry that request safely. Send the same `Idempotency-Key` header again and Bird replays the original response instead of processing the request a second time.

## How it works

Idempotency is **opt-in**. Add an `Idempotency-Key` header to any `POST`, `PATCH`, or `DELETE` request; requests without the header are processed normally with no deduplication. `GET` requests are naturally idempotent and ignore the header.

```bash
curl -X POST https://us1.platform.bird.com/v1/email/messages \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-user/usr_abc123" \
  -d '{
    "from": "hello@example.com",
    "to": ["user@example.com"],
    "subject": "Welcome!",
    "html": "<p>Thanks for signing up.</p>"
  }'
```

A key is any non-empty string up to 255 characters. The recommended format is a deterministic key derived from your own entities, `<event-type>/<entity-id>` (for example `welcome-user/usr_abc123`), so retries across process restarts share a key; a random UUID per logical operation works too. The [Bird SDKs](/docs/sdks/concepts) generate a UUID key automatically for every mutating request and reuse it across their internal retries, so SDK users get safe retries without doing anything.

Keys are scoped to your workspace (to your organization on organization-level endpoints), so two workspaces can use the same key string without colliding. A completed response is kept for **3 hours**; a retry after that window is processed as a fresh request. The window comfortably covers typical retry schedules, but it is not a permanent dedupe log.

## Replays

When Bird sees a key it has already completed, it returns the cached response, same status code, same body, without re-running the request. Replayed responses carry one extra header so you can tell them apart from fresh processing:

```http
HTTP/1.1 202 Accepted
Idempotency-Replay: true
```

Successful responses are what replay protects: the case where your first attempt worked but you never saw the answer. Requests that fail don't burn the key. A validation or business-rule rejection releases it, so a corrected retry under the same key is processed fresh rather than replaying the old error.

## Failure modes

| Scenario                                         | Response                                                 |
| ------------------------------------------------ | -------------------------------------------------------- |
| Same key, same request, original completed       | Cached response replayed with `Idempotency-Replay: true` |
| Same key, **different** request body or endpoint | `409`, `E01005 IdempotencyKeyReuse`                      |
| Same key, original request still in flight       | `409`, `E01004 RequestInProgress`                        |
| Key longer than 255 characters                   | `400`, `E01002 InvalidRequest`                           |

Reusing a completed key with a different request is treated as a client bug: Bird returns `409` immediately rather than silently handing you a response that doesn't match what you sent. Generate a new key for the new request. The comparison covers the method, endpoint, path and query parameters, and the raw request body, so even a whitespace change in the body counts as a different request.

`RequestInProgress` means a concurrent request with the same key hasn't finished yet, typically an aggressive client-side timeout retrying while the first attempt is still processing. The in-flight lock expires within 30 seconds, so wait briefly and retry. See [Errors](/docs/guides/errors) for the envelope these come wrapped in.

## What's not cached

**`5xx` responses are never cached.** A server error means Bird doesn't know whether the request took effect, and caching it would permanently block the retry from ever succeeding. Instead the key unlocks, and your retry is processed fresh. This is exactly the case idempotency exists for: retry `5xx` responses and timeouts with the same key, and you'll either get the original successful response (if the first attempt actually completed) or a clean new attempt.

Deduplication is also a protective layer, not a lock on your throughput: if the store behind it is briefly unavailable, requests proceed without deduplication rather than failing. Design your keys so a rare duplicate is tolerable, not catastrophic.

## Practical guidance

- Generate one key per logical operation, not per HTTP attempt; the whole point is that retries of the same operation share the key.
- Retry on network errors, timeouts, and `5xx` with exponential backoff, reusing the same key each time.
- Treat `409 IdempotencyKeyReuse` as a bug in your key generation, not something to retry.
- Skip keys on `GET` requests and on operations that are naturally idempotent in your domain; the mechanism is there for the cases where a duplicate would hurt.

## Next steps

- [Idempotency API reference](/docs/api/idempotency): header and response-header schemas
- [SDK concepts](/docs/sdks/concepts): automatic key generation and retry behavior in the SDKs
- [Errors](/docs/guides/errors): the error envelope and code catalog
- [Sending email](/docs/guides/email/sending-email): send and batch endpoints