Skip to content
MV2API docsStudioDownload for agentsGet an API key

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/images and /v1/videos is 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

First call
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.

Retry with the same key and the same body
# 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.

409 idempotency_conflict
{
  "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

  1. 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.
  2. Retry the same key on a connection error, a timeout, a 500, a 503, or a 409 idempotency_in_progress. Back off between attempts; honour Retry-After when it is there.
  3. Do not retry a 400, 401, 403, 404 or 409 idempotency_conflict without changing something. For 409 idempotency_unresolved, investigate the original outcome instead of automatically retrying or replacing the key.
  4. Never reuse a key for different work. One logical request, one key, for its whole life.
  5. 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.