Idempotency
Safely retry mutating Management API requests with idempotency keys.
Provisioning code retries — after a timeout, a dropped connection, a redeployed runner. On the Inference API a retry costs tokens; on the Management API a retry creates resources. A duplicate project or a second live key is worse than a failed request, so idempotency here is required, not optional.
The header
Every POST, PATCH, PUT and DELETE must carry an Idempotency-Key
header. GET requests do not need one.
Idempotency-Key: 5f0c1c4e-2b7a-4d38-9a51-7d2e6c0b9f13- The value is a client-generated string. Use a V4 UUID, or a value that
is unique per logical operation — for example your CI run id plus the step
name (
run-48213-create-key). - At most 255 characters. Keys are scoped to the personal access token that sent them — the same key value sent by a different token (even the same person's, even for the same body) is a new request, not a replay.
- Stored responses are kept for 24 hours, then pruned.
- Do not put secrets or personal data in the key.
Behaviour
| Situation | Result |
|---|---|
| Missing header on a non-GET | 400 idempotency_key_required (param: "Idempotency-Key"). Nothing is changed. |
| Same token, same key, same request, within 24 h | The original status and body are replayed, with the header Idempotent-Replayed: true. The first call's 201 is the second call's 201, with the same object id. Nothing is changed again. |
| Same key value, different personal access token | Not a replay — a brand-new request, even if the method, path and body are identical. |
| Same key, different request, within 24 h | 409 idempotency_conflict. The key was already used for a different method, path or body. |
| Same key while the first request is still running | 409 idempotency_conflict ("still in progress") — retry shortly with the same key. |
The first attempt failed with 5xx, 401, 403 or 429 | Nothing is stored: retrying with the same key runs the request again. |
| Key older than 24 h | Treated as new. |
Every other outcome is stored and replayed — including a 4xx such as
400 or 409. That is why a corrected request needs a new key (below).
"Same request" means the same method, path and body. The most common
integration bug is a constant key reused for every call — the
idempotency_conflict error exists to name it.
curl -X POST https://portal.<domain>/api/v1/mgmt/projects \
-H "Authorization: Bearer $TF_PAT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "payments"}'Tip
Generate the key before the first attempt and reuse it for every retry of
that operation. After you change a rejected request (for example, fixing a
400), send it with a new key.
Secrets in replayed responses
Two operations return a secret: creating a key and rotating a key. A replay of either returns the same secret as the first response — which is what makes a retry after a lost response safe: you get the key you created, not a second one.
A replay of either is never handed back blindly: the route's scope and your
current project and key access are checked again before the cached
response is served. If you could see the key when you created or rotated it
but have since lost access to the project, the replay is 404 not_found —
the same as if the key did not exist — and the secret is not returned again.
If the secret is not available for replay, the replay fails with
409 idempotency_replay_unavailable instead of a secret: the secret was delivered once, on the original response.
If you lost it, retry with a new Idempotency-Key and revoke the unused
key.
The stored response is protected like every other secret, and each successful replay is recorded separately in the
audit log (key_create_replayed,
key_rotate_replayed), so you can see when a secret was delivered twice.
Stored responses — including any stored secret — are kept for 24 hours and then deleted; after that a repeated key is treated as new.