Idempotency
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
Section titled “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 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?"}'const key = crypto.randomUUID(); // create once, reuse for every retry of this requestconst 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');}import os, time, uuid, requests
key = str(uuid.uuid4()) # create once, reuse for every retry of this requestRETRY_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
Section titled “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_keyotherwise). 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 bysess_…or byexternal_idcounts 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
5xxthat happens before any work started can be retried with the same key and will run again.
Requests that start a run
Section titled “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_progressis only possible in the brief moment before the run exists.
Requests that return a secret
Section titled “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
Section titled “client_message_id”For chat messages there is a second, simpler mechanism: give each message your own ID.
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_idin the same session returns the original{session, message, run}with200andIdempotent-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.