> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.rray.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.rray.app/_mcp/server.

# Idempotency

Network timeouts happen. To retry `POST /v1/messages` without risking a
duplicate send, pass an `Idempotency-Key` header — any unique string that
identifies the *operation* (an order id, a job id):

```bash
curl -X POST https://api.rray.app/v1/messages \
  -H "Authorization: Bearer $RRAY_API_KEY" \
  -H "Idempotency-Key: order-1042-receipt" \
  -H "Content-Type: application/json" \
  -d '{"from": "receipts@yourdomain.com", "to": ["customer@example.com"], "subject": "Your receipt", "text": "Thanks!"}'
```

`POST /v1/messages/batch` accepts the same header; there the stored response is
the whole positional batch result.

## Semantics

* The **first** request with a key performs the send. The key is claimed in the
  same database transaction as the message, so a request that fails before
  committing leaves no claim behind and the key stays usable.
* A **retry** with the same key and the same body replays the stored
  response — the same message `id`, no second send.
* A **concurrent** retry while the original is still in flight serializes
  behind it: it waits for the original to commit, then replays that response.
* The same key with a **different body** returns
  `409 idempotency.body_mismatch` — a key identifies one exact operation.
* A claim that exists but cannot be replayed returns
  `409 idempotency.conflict`. Retry shortly; if it persists, quote the
  `request_id`.

Claims are scoped to the workspace and retained for **24 hours**, after which
the key is free to use again.

## Replay in practice

```bash
# first call
POST /v1/messages  Idempotency-Key: order-1042-receipt
202 {"id": "msg_01M4ANAVD7EYMHY0PWSH5PBDJD"}

# identical retry — same id, nothing sent
POST /v1/messages  Idempotency-Key: order-1042-receipt
202 {"id": "msg_01M4ANAVD7EYMHY0PWSH5PBDJD"}

# same key, changed subject
POST /v1/messages  Idempotency-Key: order-1042-receipt
409 {"code": "idempotency.body_mismatch", ...}
```

## Practical guidance

* Generate the key where the business operation is born (e.g.
  `invoice-{id}-receipt`), not per HTTP attempt — that's the whole point.
* Safe to adopt gradually: requests without the header behave exactly as
  before (each attempt sends).
* Replay matching is **semantic, not textual**: the key is matched against a
  hash of the parsed request, so whitespace and field order don't matter, and
  an omitted optional field is the same as an explicit `null`. Changing any
  actual value — a recipient, the subject, the body — is a different operation
  and trips `idempotency.body_mismatch`.