# Idempotency and retries (/guides/idempotency-and-retries)



A network blip should not double-bill a generation or fire a tool twice. Zumik gives every mutating
request an idempotency key and separates three distinct kinds of retry, each with its own rules. The
guiding principle is simple: a logical response can have many internal attempts, but its observable
side effects happen exactly once.

## Agent-Idempotency-Key [#agent-idempotency-key]

Send `Agent-Idempotency-Key` on a mutating request. If the same key arrives again, Zumik replays the
original result instead of executing a second time.

```bash
curl https://api.zumik.ai/v1/chat/completions \
  -H "Authorization: Bearer zk_live_..." \
  -H "Content-Type: application/json" \
  -H "Agent-Idempotency-Key: 5f3c9b2a-task-4821" \
  -d '{"model":"code.fast","messages":[{"role":"user","content":"Run the fix."}]}'
```

How it behaves:

* **First call** runs normally and the response is cached against the key. The response carries
  `Agent-Idempotent-Replay: false`.
* **Replay** of the same key returns the original response verbatim, with
  `Agent-Idempotent-Replay: true`. You are not charged again.
* **Scope** is the project plus the request path plus the key, so a key is local to one endpoint in one
  project and cannot collide across tenants.
* **Retention** of a completed result is 24 hours; an in-flight reservation lasts up to 60 seconds.
* **Concurrent duplicate** - the same key arriving while the first is still in flight - returns
  `409 idempotency_conflict`:

  ```json
  {
    "error": {
      "message": "A request with this Agent-Idempotency-Key is already in progress. Retry after it completes.",
      "type": "idempotency_conflict"
    }
  }
  ```

<Note>
  Only successful (2xx) responses are cached. A transient `4xx`/`5xx` releases the key so a genuine
  retry can proceed - you do not get stuck replaying a failure. Use a fresh, stable key per logical
  operation (a task id works well); reusing one key for two different operations replays the first.
</Note>

## The three retry types [#the-three-retry-types]

Not every retry is the same, and treating them alike is how you double-charge or duplicate a tool call.
Zumik distinguishes:

| Type                | When                                                                     | Rule                                                                                               |
| ------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `transport_retry`   | A transient network failure - the request may or may not have reached us | Safe **only** with an idempotency key. Without one, you risk a duplicate generation.               |
| `provider_failover` | A provider or region failed mid-request                                  | Handled internally. The platform tries another path and records a new attempt; you do not re-send. |
| `semantic_retry`    | You deliberately want a different generation                             | A brand-new request and a new response object. Use a new idempotency key.                          |

A `transport_retry` is the one you control as a client: attach the same `Agent-Idempotency-Key` and the
retry is safe. A `provider_failover` happens below your call - the broker fails over (and, only under
explicit policy, to the [OpenRouter emergency fallback](/execution/openrouter-fallback)) and records
the cause, provider, and timing per attempt. A `semantic_retry` is not a retry of the same operation at
all; it is a new operation that gets its own response.

<Warning>
  Do not auto-replay a generation after observable streamed output has already reached the user unless
  the path supports resumability. On a stream disconnect, retrieve the response status or resume from an
  event cursor rather than re-issuing the generation. See [Streaming](/guides/streaming).
</Warning>

## Tool-side-effect safety [#tool-side-effect-safety]

The hard part of retries is tools with side effects - creating a record, sending a message, charging a
card. A retried or failed-over request must never run those twice. The rules:

<Steps>
  <Step title="Give every tool call a stable call id">
    A retry reuses the same call id, so a downstream system can dedupe on it rather than acting twice.
  </Step>

  <Step title="Commit assistant output to history exactly once">
    A logical response may have several internal attempts, but it is written to the session event line
    only once - the [session](/guides/sessions-and-branching) never records a generation twice.
  </Step>

  <Step title="Require external commit for side-effecting tools">
    Express it in [Agent Hints](/concepts/agent-hints) with
    `safety.tool_side_effect_mode: "external_commit_required"` so an effect is committed by your system
    after confirmation, not implicitly during a retried attempt.
  </Step>

  <Step title="Declare retry safety">
    Set `safety.retry_safety: "idempotent_generation_only"` so the platform only ever retries a
    generation it can prove is safe to repeat.
  </Step>
</Steps>

A logical response is the unit of meaning; attempts are the unit of execution. Keep side effects bound
to the logical response and a retry stays invisible to everything downstream.

<CardGroup cols="2">
  <Card title="Idempotency reference" icon="rotate" href="/api-reference/idempotency">
    The header, scope, TTL, and conflict semantics in the API reference.
  </Card>

  <Card title="Agent Hints" icon="sliders" href="/concepts/agent-hints">
    Declare retry and tool-side-effect safety as vendor-neutral intent.
  </Card>
</CardGroup>
