---
title: Idempotency
description: Why every run needs an idempotencyKey, what replays and what conflicts, and how a job run's digest differs from a direct run's.
---

Every `POST /v1/runs` and MCP [`run`](/mcp-tools/run) call requires `idempotencyKey`. It is what makes retrying a
run safe.

## Same key, same body: replay

Retry with the same key and the identical request, for this workspace, against this endpoint, and
you get the original run back, whatever its status (including one still in flight), stamped
`replayed: true`. Nothing runs a second time, and nothing charges twice.

Send it as `idempotencyKey` in the body, or as the `Idempotency-Key` header; sending both with
different values is a 400. The CLI makes one for you when you do not pass one (`looot-<uuid>`)
and prints it to stderr:

```txt
idempotency-key: looot-5b1c...
```

If a request drops mid-flight, retry it with the exact same key and body. If the first one did
reach the gateway, you get that run back and no new run starts.

## Same key, different body: conflict

Reuse a key with a different `endpointId` or `input`, and you get `409 idempotency_conflict`;
no new run starts. looot never silently applies a changed body to an old run. Use a fresh key
per logical call.

## After `insufficient_balance`, use a new key

A run refused for [`insufficient_balance`](/errors/rest-errors#insufficient_balance) is not retried with the same key. Top up, then run again
with a new `idempotencyKey`. The old key stays attached to the refused run.

## Job run digests

For a `job:<id>` run, the digest that decides what counts as "the same call" is built from the job
id, the input as sent, `prefer`, `exclude` and the fallback settings. It never includes which
provider ends up picked. So the same key against the same job always replays the first run, even
if a fresh run would now pick a different provider. The same key against a different job id, or
against a direct endpoint id, is [`idempotency_conflict`](/errors/rest-errors#idempotency_conflict); a job run and a direct run never replay
each other, in either direction.

A direct run's digest is unchanged: `endpointId` plus `input` as sent.

## Worked example

```json
{
  "endpointId": "job:people.email.verify",
  "input": { "email": "jane.doe@example.com" },
  "idempotencyKey": "verify-jane-doe-1"
}
```

Send it twice with the same key and input: the second call returns the first run, `replayed:
true`, no new hold. Send it again with `{"email": "jane.doe@example.org"}` under the same key:
`409 idempotency_conflict`.

<Related />
