# Idempotency

> Retry any POST or DELETE safely with an Idempotency-Key, and resend chat messages safely with client_message_id.

Networks fail. A request can time out after the server has already done the work, and a plain retry would then create a second session, a second message or a second answer. K-Agent gives you two tools to make retries safe.

## `Idempotency-Key`

Send a unique key with any `POST` or `DELETE`. If you retry with the same key, you get the stored result instead of a second action:

**curl**

```bash
curl https://api.k-agent.kerneltics.com/v1/sessions \
  -H "Authorization: Bearer $KAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a0e-8d3b-4c55-9a77-1e2f3d4c5b6a" \
  -d '{"agent": "store-assistant", "external_id": "order-8812", "input": "Where is my order?"}'
```

**JavaScript**

```js
const key = crypto.randomUUID(); // create once, reuse for every retry of this request
const RETRY_409 = new Set(['idempotency_in_progress', 'session_busy', 'session_queue_full']);

async function createSessionWithRetry(body, attempts = 4) {
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch('https://api.k-agent.kerneltics.com/v1/sessions', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.KAGENT_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': key,
        },
        body: JSON.stringify(body),
      });
      if (res.status === 409) {
        const { error } = await res.clone().json();
        if (!RETRY_409.has(error.code)) return res;
      } else if (res.status < 500 && res.status !== 429) {
        return res;
      }
    } catch {
      // network error: retry with the same key
    }
    await new Promise((r) => setTimeout(r, 500 * 2 ** i));
  }
  throw new Error('Gave up after retries');
}
```

**Python**

```python
import os, time, uuid, requests

key = str(uuid.uuid4())  # create once, reuse for every retry of this request
RETRY_409 = {"idempotency_in_progress", "session_busy", "session_queue_full"}

def create_session_with_retry(body: dict, attempts: int = 4) -> requests.Response:
    for i in range(attempts):
        try:
            r = requests.post(
                "https://api.k-agent.kerneltics.com/v1/sessions",
                headers={"Authorization": f"Bearer {os.environ['KAGENT_API_KEY']}",
                         "Idempotency-Key": key},
                json=body, timeout=120,
            )
            if r.status_code == 409:
                if r.json()["error"]["code"] not in RETRY_409:
                    return r
            elif r.status_code < 500 and r.status_code != 429:
                return r
        except requests.ConnectionError:
            pass  # network error: retry with the same key
        time.sleep(0.5 * 2 ** i)
    raise RuntimeError("Gave up after retries")
```

### The rules

- **Scope.** A key is scoped to your project, the credential that sent it, and the route (method and path pattern). The same key on another route is a different key.
- **Length.** 1 to 255 characters (`400 invalid_idempotency_key` otherwise). A UUID v4 is ideal.
- **Lifetime.** Results are kept for **24 hours**. After that, the key can be used again.
- **Replays** return the original status and body, with the header `Idempotent-Replayed: true`.
- **Same key, different request** — another body or another path ID — returns `422 idempotency_key_reused`. The comparison uses the resolved session, so addressing it by `sess_…` or by `external_id` counts as the same request.
- **Still running.** If the first request hasn't finished, a retry returns `409 idempotency_in_progress`. Wait and retry with the same key.
- **Server errors are not stored.** A `5xx` that happens before any work started can be retried with the same key and will run again.

### Requests that start a run

For `ask`, `POST /v1/sessions` with `input`, messages and `submit_tool_outputs`, the key is tied to the **run** as soon as it exists:

- a retry returns the run's **current** state in the endpoint's normal response shape — if it has finished since, you get the finished run;
- a retry of a streaming request re-attaches to that run's stream **from the start**;
- `409 idempotency_in_progress` is only possible in the brief moment before the run exists.

### Requests that return a secret

Creating or rolling an API key, minting a client token, starting a widget session, creating a webhook endpoint or rotating its secret, and reading the tool signing secret all return a secret **once**. Their replays return the same resource with the secret set to `null` and `"secret_redacted": true` — the secret itself is never stored for replay.

## `client_message_id`

For chat messages there is a second, simpler mechanism: give each message your own ID.

```bash
curl https://api.k-agent.kerneltics.com/v1/sessions/order-8812/messages \
  -H "Authorization: Bearer $KAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": "Thanks!", "client_message_id": "wa-msg-77121"}'
```

- The same `client_message_id` in the same session returns the original `{session, message, run}` with `200` and `Idempotent-Replayed: true` — no second message, no second answer.
- The same ID with different text returns `409 client_message_id_conflict`.
- It never expires while the session exists, which makes it the right tool for channels that redeliver messages hours later (webhooks from messaging platforms, mobile apps that resend after reconnecting).

Use **both** when you can: `client_message_id` to deduplicate the message itself, and `Idempotency-Key` to make the HTTP request safe to retry.
