# Idempotency (/guides/idempotency)

Retry a request and you've sent the same email twice. Let a checkout form double-submit and the customer gets two receipts. The fix is the `Idempotency-Key` header, which guarantees a request runs **at most once**.

When Bitelio sees a key for the first time, the request goes through as usual. When the same key shows up again within your project, the request is **refused** with a `409` instead of being executed a second time.

The header is supported on both public API write endpoints:

* `POST /v1/track`
* `POST /v1/send`

## Sending a key

Any string of 1–255 printable ASCII characters works as a key. What matters is that each logical operation gets its own — either a UUID, or something you derive from your own records, such as `receipt-order-1234`.

import {Tab, Tabs} from 'fumadocs-ui/components/tabs';

<Tabs items={['cURL', 'JavaScript', 'Python']}>
  <Tab value="cURL">
    ```bash
    curl -X POST https://api.bitelio.com/v1/send \
      -H "Authorization: Bearer sk_your_secret_key" \
      -H "Idempotency-Key: receipt-order-1234" \
      -H "Content-Type: application/json" \
      -d '{
        "to": "customer@example.com",
        "subject": "Your receipt",
        "body": "<p>Thanks for your order!</p>"
      }'
    ```
  </Tab>

  <Tab value="JavaScript">
    ```javascript
    await fetch('https://api.bitelio.com/v1/send', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer sk_your_secret_key',
        'Idempotency-Key': 'receipt-order-1234',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        to: 'customer@example.com',
        subject: 'Your receipt',
        body: '<p>Thanks for your order!</p>',
      }),
    });
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import requests

    requests.post(
        "https://api.bitelio.com/v1/send",
        headers={
            "Authorization": "Bearer sk_your_secret_key",
            "Idempotency-Key": "receipt-order-1234",
        },
        json={
            "to": "customer@example.com",
            "subject": "Your receipt",
            "body": "<p>Thanks for your order!</p>",
        },
    )
    ```
  </Tab>
</Tabs>

## What happens on reuse

A repeated key comes back as a `409` carrying the error code `IDEMPOTENCY_KEY_REUSED`. Inside `details` you'll find information about the request that claimed the key first:

```json
{
  "success": false,
  "error": {
    "code": "IDEMPOTENCY_KEY_REUSED",
    "message": "Idempotency-Key \"receipt-order-1234\" has already been used",
    "statusCode": 409,
    "details": {
      "key": "receipt-order-1234",
      "originalRequest": "POST /v1/send",
      "originalRequestAt": "2025-01-15T10:30:00.000Z",
      "originalStatusCode": 200
    }
  }
}
```

If `originalStatusCode` is `null`, the first request hasn't finished yet — both requests carrying the key arrived simultaneously and this one lost the race.

<Callout type="warn">
  On a reused key, Bitelio **refuses** — it does not replay the first response. The `409` guarantees nothing ran twice, but it won't hand you the original email or event ID. Capture that ID yourself when the first request succeeds if you'll need it later.
</Callout>

## Which failures free the key

A failed request doesn't always consume the key.

| Outcome of the first request | Key is       | Why                                                                                                                                         |
| ---------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `2xx` success                | **Kept**     | The operation happened. A retry would duplicate it.                                                                                         |
| `4xx` client error           | **Released** | Validation and permission failures are rejected before any write occurs, so correcting the request and retrying under the same key is safe. |
| `5xx` server error           | **Kept**     | Part of the work may already have happened. Blocking the retry is exactly what the key is for.                                              |

If a `5xx` consumed a key but you're certain the operation never took effect, retry under a fresh key.

## Scope and expiry

A key belongs to your **project** as a whole, not to a single endpoint — once spent on `/v1/track`, it can't be replayed against `/v1/send`. Separate projects, on the other hand, can each use the same key string without interfering.

Claimed keys expire after **24 hours** and can be used again from that point.

## Multiple recipients

When you pass an array of recipients to `POST /v1/send`, they're processed sequentially — but the idempotency key protects the request as a unit, not recipient by recipient. Should the request die partway with a `5xx`, some recipients may already have received their email; the key stays claimed for exactly that reason, so a naive retry can't hit them twice.

If you need control at the recipient level, issue one request per recipient, each carrying its own key.
