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

SituationResult
Missing header on a non-GET400 idempotency_key_required (param: "Idempotency-Key"). Nothing is changed.
Same token, same key, same request, within 24 hThe 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 tokenNot a replay — a brand-new request, even if the method, path and body are identical.
Same key, different request, within 24 h409 idempotency_conflict. The key was already used for a different method, path or body.
Same key while the first request is still running409 idempotency_conflict ("still in progress") — retry shortly with the same key.
The first attempt failed with 5xx, 401, 403 or 429Nothing is stored: retrying with the same key runs the request again.
Key older than 24 hTreated 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.