# Idempotency

> How to retry a write on the watsi API without applying it twice using the Idempotency-Key header, which routes accept it and how long it is remembered.

Source: https://docs.watsi.ai/en/api/idempotency

If a write is cut off halfway (the network drops, your process restarts), you don’t know whether watsi applied it. The `Idempotency-Key` header lets you retry it safely: if the first one was already applied, the retry returns **its** result instead of writing again.

```bash
curl -X POST "https://api.watsi.ai/v1/actions/contacts/create" \
  -H "Authorization: Bearer $WATSI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed" \
  -d '{ "name": "María Pérez", "phone_number": "5215598765432" }'
```

## Which routes accept it

The writes that are not idempotent by nature. Reads, and writes that leave the same state however many times you repeat them (such as setting a conversation’s status), ignore it because they are already safe to retry. This list is generated from the contract:

| Method | Path | Scopes | Idempotency-Key |
| --- | --- | --- | --- |
| `POST` | `/v1/actions/agents/create` | `agents:write` | Yes |
| `POST` | `/v1/actions/agents/knowledge` | `knowledge:write` | Yes |
| `POST` | `/v1/actions/agents/knowledge/delete` | `knowledge:write` | Yes |
| `POST` | `/v1/actions/agents/update` | `agents:write` | Yes |
| `POST` | `/v1/actions/attachments/create-upload` | `agents:write` | Yes |
| `POST` | `/v1/actions/attachments/finalize` | `agents:write` | Yes |
| `POST` | `/v1/actions/attachments/import-url` | `agents:write` | Yes |
| `POST` | `/v1/actions/contacts/create` | `contacts:write` | Yes |
| `POST` | `/v1/actions/crm/tags` | `crm:write` | Yes |
| `POST` | `/v1/actions/crm/tags/remove` | `crm:write` | Yes |
| `POST` | `/v1/actions/lifecycle/attach-file` | `lifecycle:write` | Yes |
| `POST` | `/v1/actions/voice/config` | `agents:write` | Yes |

## How it behaves

| Case                                            | Response                                                       |
| ----------------------------------------------- | -------------------------------------------------------------- |
| First request with a key                        | Runs normally                                                  |
| Same key, same body, within **30 seconds**      | `200` with the first one’s stored response; nothing is written |
| Same key, **different** body, within 30 seconds | `409` `IDEMPOTENCY_KEY_REUSED`                                 |
| Same key after 30 seconds                       | Treated as a new request                                       |
| The first request **failed**                    | Nothing was stored: the retry runs again                       |

- A key is scoped **per workspace and per route**: the same key on `contacts/create` and on `crm/tags` are two different keys.
- The body comparison is by content: the order of JSON properties does not matter.
- The 30-second window is meant for the immediate retry of a request that was cut off, not for deduplicating jobs that span hours. For that, keep your own record of which operations you completed.

## Good practice

- **One key per logical operation**, generated before the first attempt (a UUID v4 works) and reused on every retry of that operation.
- **Never the same key for two different operations**: the second body gets a `409`.
- **If you get `409 IDEMPOTENCY_KEY_REUSED`**, it is a bug in your code (two operations shared a key), not something a retry fixes.

> **Note**

`contacts/create` is also an _upsert_: sending the same phone, `wa_id` or email updates the existing contact instead of duplicating it, even without an `Idempotency-Key` and outside the window. The response says so with `"created": false`.
