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

# Idempotency and replay

Write operations take an `Idempotency-Key` header of 1 to 255 characters. Draft and tool validation,
campaign recipient checks, `POST /calls/search` and `POST /usage/query` do not change a resource and
take no key.

## The rules

* **One key per intended action.** Generate a new key, for example a UUID, for each action you mean
  to take. Persist it before you send the request.

* **Same key, same request: the original answer.** Sending the same key with the same request again
  returns the original status code and body, with the response header `Idempotent-Replayed: true`.
  Nothing happens a second time: a replayed `createCall` does not place a second call.

* **Same key, different request: refused.** A changed method, path, body or original `If-Match`
  under a used key answers `409 idempotency_key_reused`. Fix the caller; do not retry a changed
  intent with the old key.

* **Still running: wait.** If the first request with the key is still in progress, a duplicate
  answers `409 idempotency_in_progress` with `Retry-After` at once; it does not wait for the first
  request. Wait the stated seconds, then resend the identical request with the same key: it then
  replays the first answer. This holds for every keyed mutation.

* **24 hours.** Keys are scoped to your workspace and to the operation, and are kept for 24 hours.
  After that, read the resource to find out what happened before you act again. This is not
  permanent deduplication.

* **Authorization is checked again.** A replay still requires a key that is valid now.

## What a replay returns

A replay returns the stored receipt of the first attempt: it describes that attempt, not the
resource as it is now. The server looks the key up before it checks the current revision, so a
replay of an edit returns its original answer even if the resource has changed since.

| Operations                                                                           | What is stored                                                                                                                                                                 | What a replay answers                                                                                       |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| Agent, draft, variable, publish, rollback, tool and MCP server writes                | The full public answer and its `ETag`                                                                                                                                          | The original status, `ETag` and body, even after the resource was changed or detached, or its agent removed |
| `createCall`                                                                         | The accepted call: its ID, the executed version, the state, the source number ID and the poll hint. The dialled number, the variables and the metadata are never stored in it. | The original body, naming the same call. The number is never dialled twice.                                 |
| `endCall`                                                                            | The full answer                                                                                                                                                                | The original body                                                                                           |
| Phone-number assignment and provisioning                                             | The full answer (the number, or the number with its setup state)                                                                                                               | The original body with its original `ETag`                                                                  |
| Hosted share-link issue and rotation                                                 | Safe metadata without the bearer token                                                                                                                                         | Metadata with `token: null`; the original token is revealed once                                            |
| `createSecret`, `rotateSecret`, `revokeSecretVersion`, `deleteSecret`                | The metadata answer (for `deleteSecret`, the `204`)                                                                                                                            | The original status and body; the secret is not read again                                                  |
| Reads, draft/tool validation, campaign recipient checks, call search and usage query | Nothing: they take no key                                                                                                                                                      | Not applicable                                                                                              |

No receipt holds a credential value. The rule behind the table: a replay returns the original body,
except that a body carrying personal data that can be erased (phone numbers, variables, transcripts)
is kept as identifiers only and read again, because erasure must not leave that data in a 24-hour
receipt. The answers above are designed not to carry such data, which is why they can be stored
whole.

### Refusals

A refused request can also be stored, and then replays as the same refusal for the key's 24 hours.
Two cases differ:

* `createCall` refused with `503 at_capacity` or `429 quota_exceeded`: wait `Retry-After`, then send
  the identical request with the same key. It is admitted afresh.
* The secret operations store nothing when they refuse. After you fix the cause, the same request
  with the same key runs.

## A safe retry

```bash
KEY=$(uuidgen)
curl -sS -X POST "$RASEN_API_BASE/agents" \
  -H "Authorization: Bearer $RASEN_API_KEY" -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Support line", "resource_key": "support-line"}'
# The connection dropped before the answer arrived? Send the identical request again with the same $KEY.
```

For `internal_error`, `provider_unavailable`, `unavailable`, `at_capacity` and
`call_orchestrator_unavailable`, a retry with the same key is safe: if the first attempt took effect,
the stored outcome is replayed; if it had no effect, the retry performs the operation once. Never
retry with a new key.

## Keys are not resource keys

An `Idempotency-Key` protects one request. A `resource_key` names a piece of configuration across
script runs, and is not an idempotency key. See [Resource keys](/concepts/resource-keys).

## Key rotation on the server

If the platform rotates its master key material, idempotency keys used in the previous 24 hours
answer `idempotency_key_expired`. Read the resource to reconcile, then send a new request with a new
key.