# Idempotency

> Retry email sends safely with the Idempotency-Key header. Which endpoints support it, key format and scope, the 24-hour replay window and its errors.

Network errors and timeouts leave you unsure whether a send went through. An idempotency key makes the send safe to retry: if Emailit already accepted a request with the same key, it returns the original response instead of sending the email again. This page is the reference for the `Idempotency-Key` header. For a walkthrough, see [Idempotent sends](/docs/email-api/idempotency/).

## Supported endpoints

| Endpoint | |
| --- | --- |
| `POST /emails` | [Send an email](/docs/api-reference/emails/send/) |
| `POST /emails/{id}/forward` | [Forward an email](/docs/api-reference/emails/forward/) |

Other endpoints ignore the header. Creating a domain, API key, audience or contact is already safe to retry: a second request with the same name or email returns `409` and the `existing` object instead of creating a duplicate.

## Send a key

Add an `Idempotency-Key` header with a value that's unique to the email you're sending, such as a UUID or an ID from your own system:

**cURL**

```bash
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirmation" \
  -d '{
    "from": "Acme <orders@acme.com>",
    "to": "ada@example.com",
    "subject": "Your order #1042",
    "html": "<p>Thanks for your order.</p>"
  }'
```

**Node.js**

```javascript
const response = await fetch('https://api.emailit.com/v2/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `order-${order.id}-confirmation`,
  },
  body: JSON.stringify({
    from: 'Acme <orders@acme.com>',
    to: order.email,
    subject: `Your order #${order.id}`,
    html: '<p>Thanks for your order.</p>',
  }),
});
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.emailit.com/v2/emails",
    headers={
        "Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}",
        "Idempotency-Key": f"order-{order['id']}-confirmation",
    },
    json={
        "from": "Acme <orders@acme.com>",
        "to": order["email"],
        "subject": f"Your order #{order['id']}",
        "html": "<p>Thanks for your order.</p>",
    },
    timeout=30,
)
```

Generate the key once per email, before the first attempt, and send the same key on every retry of that email.

## How keys work

| Rule | Details |
| --- | --- |
| Format | 1 to 256 characters. Letters, digits, hyphens (`-`) and underscores (`_`) only. |
| Scope | Per workspace. Keys from different workspaces never collide, but both endpoints share one namespace, so don't reuse a send key for a forward. |
| Lifetime | The response to a successful request is kept for 24 hours after it completes. |
| Replays | A request with a stored key returns the stored response with status `200`. No email is sent, no credits are used, and the sending limits aren't counted. |
| Matching | Only the key is compared, not the request body. A different request with a used key returns the first response. |
| Failures | If a request fails with any error, nothing is stored and the key is released, so you can fix the request and retry with the same key. |
| Concurrency | While a request with a key is still running, another request with the same key returns `409`. The lock is released when the first request finishes, and it expires after 15 minutes at most. |

A replayed request still goes through authentication and the per-second and daily [sending limits](/docs/api-reference/rate-limits/) check, so it can return `401` or `429`. Forwards also count against the hourly forward limit, even when the response is replayed.

## Errors

**400**

```json
{
  "error": "Invalid Idempotency-Key"
}
```

**409**

```json
{
  "error": "Idempotency key in progress",
  "message": "A request with this Idempotency-Key is already being processed. Retry shortly with the same key."
}
```

**503**

```json
{
  "error": "Idempotency unavailable",
  "message": "Unable to process Idempotency-Key right now. Retry the request with the same key."
}
```

| Status | `error` | Cause | What to do |
| --- | --- | --- | --- |
| `400` | `Invalid Idempotency-Key` | The key is empty, longer than 256 characters, or has other characters than letters, digits, `-` and `_`. | Use a UUID or another safe value. |
| `409` | `Idempotency key in progress` | A request with the same key is still being processed. | Wait a second and retry with the same key. You'll get the stored response once the first request finishes. |
| `503` | `Idempotency unavailable` | Emailit can't check the key right now. The request wasn't processed. | Retry with the same key after a short delay. |

Emailit never sends a request without its idempotency check: if the key can't be checked, the request fails with `503` rather than risk a duplicate.

## Choose good keys

- Derive the key from the thing you're notifying about, such as `order-1042-confirmation` or `password-reset-<token-id>`, so a retry from another worker or after a restart uses the same key.
- Use a fresh UUID only when the email has no natural ID, and store it with the job before the first attempt.
- Don't reuse a key for a different email within 24 hours. You'd get the first email's response and the second email wouldn't be sent.

## Related

  - [Idempotent sends](/docs/email-api/idempotency/): A guide to retry-safe sending.
  - [Rate limits](/docs/api-reference/rate-limits/): Back off and retry with example code.

---
Source: https://emailit.com/docs/api-reference/idempotency/
