Guide
Idempotency and retries
A dropped response is not the same as a failed request. The Idempotency-Key header is how you tell the difference without paying for it twice.
Where the header is required
Every endpoint that reserves credits requires an Idempotency-Key. Sending one without it is a 400, naming the header — the default has to fail closed on a surface where a retry costs real money:
POST /v1/characters/:id/actions/train— Attach the uploaded selfies and start training. Returns 202.POST /v1/images— Start an image generation and return the job to poll.POST /v1/videos— Start a video generation and return the job to poll.
Everything else accepts the header and ignores it. Reads are naturally repeatable, presigning costs nothing, and approving an already-approved job returns that job — so a retry after a lost response is safe there without any help from you.
The value is 1 to 128 printable ASCII characters, sent at most once per request. A UUID per logical request is the easy answer. Only a hash of it is ever stored.
What a key identifies
A key is scoped to your API key, the HTTP method and the canonical route — and, on character training, the character it acts on. Three consequences follow:
- The same string on
/v1/imagesand/v1/videosis two different requests. - Reusing one key across two characters cannot silently return the first character’s training run for the second.
- Your keys never collide with anything the Musavir studio does on the same account.
Alongside the key, the server stores a digest of the request body. The body is canonicalized first, so object field order does not matter — a client serializing from a map is not punished for it. Array order does matter: refKeys in a different order is a different request.
The four outcomes
IDEMPOTENCY_KEY=$(uuidgen)
curl -i -X POST https://api.musavir.ai/v1/images \
-H "Authorization: Bearer $MUSAVIR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{"prompt":"A lighthouse in fog, long lens"}'1. New key. The request runs. You get 202 and a job.
# The identical call again — same key, same body.
curl -i -X POST https://api.musavir.ai/v1/images \
-H "Authorization: Bearer $MUSAVIR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{"prompt":"A lighthouse in fog, long lens"}'
# HTTP/1.1 202 Accepted
# Idempotent-Replayed: true
# → the SAME job id. Nothing was rendered twice, nothing was charged twice.2. Same key, same body. The original job is returned without rendering anything, and the response carries Idempotent-Replayed: true. That header is also how our own usage counters know not to count the job twice.
3. Same key, different body. A 409 idempotency_conflict. Replaying here would hand you a job for a prompt you never sent, and charge you nothing for the render you believe you started — so the mismatch is refused instead.
{
"error": {
"code": "idempotency_conflict",
"message": "This Idempotency-Key was already used with a different request body.",
"requestId": "0d1f…"
}
}4. Same key, still running. A 409 idempotency_in_progress with Retry-After: 1. The first request has not finished claiming its work yet. Wait and send the identical request again; you will get the original job.
Failures and lost responses
When admission fails before a job and its funding commit, the key is released. For example, retry a 402 with the same key after topping up. A timeout or 500 alone does not prove that admission failed: retry the identical request with the same key, not a new one.
Image and video admission save the job, reservation and replay record in one database transaction. If its response is lost after that transaction commits, the same-key retry returns the existing job without another reservation. This does not mean an unknown provider submission can be repeated safely.
An interrupted request without a completed replay record stays claimed. After about ten minutes it returns 409 idempotency_unresolved instead of 409 idempotency_in_progress; it does not become reusable automatically. Inspect your jobs and balance, and contact support if the outcome is unclear. A new key creates new work and may spend again.
A retry strategy that works
- Generate the key before the first attempt, and store it beside your own record of the work. A key generated inside the retry loop is a new key on every attempt, which is exactly the protection you were trying to buy.
- Retry the same key on a connection error, a timeout, a
500, a503, or a409 idempotency_in_progress. Back off between attempts; honourRetry-Afterwhen it is there. - Do not retry a
400,401,403,404or409 idempotency_conflictwithout changing something. For409 idempotency_unresolved, investigate the original outcome instead of automatically retrying or replacing the key. - Never reuse a key for different work. One logical request, one key, for its whole life.
- Do not put secrets in the key. It travels in a header and is hashed on arrival, but an internal order id is a better source of stability than a token.
Every code mentioned here is listed with its own fix on the errors page.