service_account_idempotency_required 400: send an Idempotency-Key

Some service-account keys require an Idempotency-Key on every paid submit. The 400 means the header is missing; add a stable key per intent and resend.

4 min readSume
All posts

service_account_idempotency_required is a 400 with the message "Service account policy requires an Idempotency-Key header." It appears when the key's policy sets requireIdempotencyKey and a paid generation arrives without an Idempotency-Key header. Add one and resend. Nothing was reserved or billed for the refused call.

What a good key looks like

One key names one intent, such as one clip for one order line, and it is reused only when you retry that same intent. The jobs and results guide says it directly: do not submit a new paid job for the same intent, retry the submit with the same key so that it returns the existing job. The generation admission page uses the pattern avatar-batch-001-item-001, which is a batch id plus an item id.

  • Derive the key from your own stable ids, not from a timestamp or a random value created on each attempt.
  • Keep it under your control: store it with the order or task before you send.
  • Reuse it across network retries and process restarts.
  • Use a different key for a different request body. Sending the same key with a different body is a conflict, not a new job.

Where it sits among the policy checks

The order in the source is: key usable, model allowed, operation allowed, then idempotency, attribution metadata and callback domain. A missing key therefore surfaces only after the earlier checks passed. If you fix this and see a 403, the next gate is a different one. Each refusal is recorded as a policy denial before it is returned.

Add it once, in a wrapper

Generate the key outside the retry loop, not inside it. This sketch runs as is and shows the shape of the call without sending it:

import hashlib, json

def idem_key(order_id: str, line: int, model: str) -> str:
    raw = f"{order_id}:{line}:{model}".encode()
    return "ord-" + hashlib.sha256(raw).hexdigest()[:24]

def request_parts(order_id, line, model, body):
    headers = {
        "Authorization": "Bearer <SUME_API_KEY from env>",
        "Idempotency-Key": idem_key(order_id, line, model),
        "Content-Type": "application/json",
    }
    return headers, json.dumps(body)

h, _ = request_parts("A-1001", 2, "image-1.0", {"prompt": "test"})
print(h["Idempotency-Key"])

Test it before production

Send the same submit twice in staging with the same key and confirm that you get the same job id back, not two jobs. Then send it once without the header and confirm that you see this 400, so that your wrapper's error handling is proven on the failure path as well. Finally, kill the process between the submit and the response, restart it and resend, because that crash window is the case the key exists for.

What it does not do

The header makes a retry safe. It does not lift a cap or a rate limit, and a new key for the same work is a new intent that meets the same limits. If the key was already used by a request that is still starting, the API may answer idempotency_key_in_use with a short retry delay, which is the one 409 on these surfaces that is worth retrying.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume