Idempotency
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 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:
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 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; 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
{
"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.