Skip to content
MV2API docsStudioDownload for agentsGet an API key

Reference

Errors

All 21 codes the API can return, each with the status it always arrives with and the thing to do next. Codes are wire-stable — branch on error.code, never on the message.

The error envelope

Every failure has the same shape. data is absent; error is present.

Error response
{
  "error": {
    "code": "missing_scope",
    "message": "This API key is missing the \"images:generate\" scope.",
    "requestId": "0d1f…",
    "details": { "requiredScope": "images:generate" }
  }
}
  • code — stable and machine-readable. A code’s meaning never changes inside /v1; new codes may be added.
  • message — written for a human. Show it, log it, but never parse it.
  • requestId — the same value as the X-Request-Id response header. It is how a support conversation becomes a lookup.
  • details — present when there is something machine-usable to add: the field that failed, the scope that was missing, the credits that were short.

Every code

Public API error codes, statuses and retry advice
CodeStatusRetry?Meaning
invalid_request400Not without changing the requestThe request reached a real endpoint but did not satisfy its schema — a missing required field, a value outside the accepted range, a path parameter that is not an id, a cursor that no longer resolves, or a billable call sent without an Idempotency-Key.
unknown_field400Not without changing the requestA field the endpoint does not define. /v1 refuses unknown fields at every depth rather than ignoring them.
invalid_json400Not without changing the requestThe body did not parse as JSON.
body_too_large413Not without changing the requestThe JSON body is over the size limit.
unsupported_media_type415Not without changing the requestThe request had a body with a Content-Type that is not application/json (or none at all).
invalid_api_key401Not without changing the requestAuthentication failed. Missing, malformed, unknown, paused, revoked and expired keys all produce this identical response — telling you which one it was would confirm to whoever holds a stolen key that it was once real.
missing_scope403Not without changing the requestThe key authenticated, but it does not carry the scope this endpoint requires.
account_not_approved403Yes, with backoffThe Musavir account that owns the key is not approved for access yet.
account_disabled403Not without changing the requestThe owning account can no longer use the API.
subscription_required403Not without changing the requestThe API is a Musavir Pro feature and the owning account has no live subscription — it is on the free plan, or a paid period has ended. Only the SPENDING endpoints are closed: generation, upload and character requests. GET /v1/account/usage, GET /v1/account/limits and GET /v1/capabilities keep answering, so a lapse never leaves you unable to see your own balance.
not_found404Not without changing the requestNo such endpoint, or no such resource FOR THIS KEY. A resource that belongs to another account, and a job created in the Musavir studio rather than through the API, both answer 404 rather than 403 — a 403 would confirm the id names something real.
method_not_allowed405Not without changing the requestThe path exists, but not under this method.
insufficient_credits402Not without changing the requestThe credit balance could not cover the job. Nothing was started and nothing was charged.
api_key_cap_exceeded402Not without changing the requestThe KEY hit its own monthly credit cap — not the account's balance. Caps are set per key in the developer dashboard and reset on the first of each UTC calendar month. The ceiling counts credits RESERVED as well as credits already settled, so jobs still running count against it; that is what stops a burst of concurrent requests from crossing a cap they each individually respected.
idempotency_conflict409Not without changing the requestThis Idempotency-Key was already used with a DIFFERENT request body.
idempotency_in_progress409Yes, after Retry-AfterAn earlier request with this same Idempotency-Key is still running.
idempotency_unresolved409Not without changing the requestAn earlier request with this Idempotency-Key stopped before it finished, and whether it took effect is unknown.
conflict409Not without changing the requestThe request is well-formed but the resource is not in a state that allows it — casting a character that has not finished training is the case you will actually meet.
rate_limited429Yes, after Retry-AfterA per-minute budget was exhausted. Rate limiting is NOT enforced on this deployment today, so you should not see this code — the mechanism exists, and the ceilings are switched off.
internal_error500Yes, with backoffSomething failed on our side. The response describes nothing further on purpose.
provider_unavailable503Yes, with backoffA capability exists but is not currently available — most often video generation, which reports itself as videos.available: false in GET /v1/capabilities before you spend a request finding out.

Code by code

invalid_requestHTTP 400

The request reached a real endpoint but did not satisfy its schema — a missing required field, a value outside the accepted range, a path parameter that is not an id, a cursor that no longer resolves, or a billable call sent without an Idempotency-Key.

Read error.details.path: it names the exact field, prefixed by where it came from (body, query or params). error.details.issues lists up to ten problems at once so one round trip fixes several.

Retry: Not without changing the requestdetails.locationdetails.pathdetails.issues
unknown_fieldHTTP 400

A field the endpoint does not define. /v1 refuses unknown fields at every depth rather than ignoring them.

Check the spelling against the endpoint reference. This is deliberate: a silently dropped resoluton would return 200 for a render you did not ask for, so the typo is reported instead of absorbed.

Retry: Not without changing the requestdetails.locationdetails.pathdetails.fields
invalid_jsonHTTP 400

The body did not parse as JSON.

Serialize with a JSON encoder rather than string concatenation, and confirm nothing truncated the payload in transit. The parser message is deliberately not echoed back — it would quote your own body into our logs.

Retry: Not without changing the request
body_too_largeHTTP 413

The JSON body is over the size limit.

Media never travels through this API. Presign an upload, PUT the bytes straight to storage, and send the returned uploadKey — a request carrying a prompt and a handful of keys is kilobytes.

Retry: Not without changing the requestdetails.maxBytes
unsupported_media_typeHTTP 415

The request had a body with a Content-Type that is not application/json (or none at all).

Send Content-Type: application/json. A charset parameter is fine; a different media type is not.

Retry: Not without changing the requestdetails.received
invalid_api_keyHTTP 401

Authentication failed. Missing, malformed, unknown, paused, revoked and expired keys all produce this identical response — telling you which one it was would confirm to whoever holds a stolen key that it was once real.

Check that the header reads Authorization: Bearer mk_live_… with the complete secret. If you cannot account for the key, revoke it and issue a new one; there is no way to re-reveal a key after creation.

Retry: Not without changing the request
missing_scopeHTTP 403

The key authenticated, but it does not carry the scope this endpoint requires.

Add the scope to the key in the developer dashboard, or use a key that has it — error.details.requiredScope names it exactly. Scope changes take effect on the next request.

Retry: Not without changing the requestdetails.requiredScope
account_not_approvedHTTP 403

The Musavir account that owns the key is not approved for access yet.

Approval is an account-level state, not a key-level one — issuing another key changes nothing. Approval takes effect on the next request once granted.

Retry: Yes, with backoff
account_disabledHTTP 403

The owning account can no longer use the API.

Contact support. This is checked live on every request, so a resolution applies immediately without reissuing keys.

Retry: Not without changing the request
subscription_requiredHTTP 403

The API is a Musavir Pro feature and the owning account has no live subscription — it is on the free plan, or a paid period has ended. Only the SPENDING endpoints are closed: generation, upload and character requests. GET /v1/account/usage, GET /v1/account/limits and GET /v1/capabilities keep answering, so a lapse never leaves you unable to see your own balance.

Subscribe to (or renew) Musavir Pro on the Billing page of the dashboard — error.details.plan names the plan the refusal was made against. A trial counts as subscribed: it requires a card and converts automatically, so evaluating the API does not require paying first. Access resumes on the next request; keys do not need reissuing.

Retry: Not without changing the requestdetails.plan
not_foundHTTP 404

No such endpoint, or no such resource FOR THIS KEY. A resource that belongs to another account, and a job created in the Musavir studio rather than through the API, both answer 404 rather than 403 — a 403 would confirm the id names something real.

Check the method and path against the endpoint reference, and confirm the id came from a response to one of your own API calls.

Retry: Not without changing the request
method_not_allowedHTTP 405

The path exists, but not under this method.

The Allow response header lists the methods that path does accept.

Retry: Not without changing the requestdetails.allow
insufficient_creditsHTTP 402

The credit balance could not cover the job. Nothing was started and nothing was charged.

Read error.details.reason to choose the remedy: subscribe means a lapsed subscription froze the granted balance, buy_credits means the account is simply out. requiredCredits and availableCredits are decimal strings. If the account has automatic top-ups switched on, GET /v1/account/usage reports why the last one did not land under autoReload.lastError — a declined card and a month that has reached its ceiling both surface there, and both look like this error from the outside.

Retry: Not without changing the requestdetails.reasondetails.requiredCreditsdetails.availableCredits
api_key_cap_exceededHTTP 402

The KEY hit its own monthly credit cap — not the account's balance. Caps are set per key in the developer dashboard and reset on the first of each UTC calendar month. The ceiling counts credits RESERVED as well as credits already settled, so jobs still running count against it; that is what stops a burst of concurrent requests from crossing a cap they each individually respected.

Raise or clear the cap on that key in the developer dashboard, or send the request with a key that has room. Buying credits does not help — the account balance was never the constraint. monthlyCreditCap, usedCredits, remainingCredits and requiredCredits are decimal strings.

Retry: Not without changing the requestdetails.monthlyCreditCapdetails.usedCreditsdetails.remainingCreditsdetails.requiredCredits
idempotency_conflictHTTP 409

This Idempotency-Key was already used with a DIFFERENT request body.

Use one key per distinct request. Replaying the earlier job here would hand you a render you never asked for, so the mismatch is refused instead. Field order does not count as a difference — the body is canonicalized before it is compared.

Retry: Not without changing the request
idempotency_in_progressHTTP 409

An earlier request with this same Idempotency-Key is still running.

Wait for Retry-After (one second) and send the identical request again; you will get the original job back.

Retry: Yes, after Retry-After
idempotency_unresolvedHTTP 409

An earlier request with this Idempotency-Key stopped before it finished, and whether it took effect is unknown.

Do NOT resend with the same key — it will keep returning this. Check GET /v1/jobs and your balance to see whether the original request landed, then retry with a NEW Idempotency-Key if it did not.

Retry: Not without changing the request
conflictHTTP 409

The request is well-formed but the resource is not in a state that allows it — casting a character that has not finished training is the case you will actually meet.

Read the resource first and act on its current status; details.characterReady is false when a cast member was the problem. The message names the state that blocked the call.

Retry: Not without changing the requestdetails.characterReady
rate_limitedHTTP 429

A per-minute budget was exhausted. Rate limiting is NOT enforced on this deployment today, so you should not see this code — the mechanism exists, and the ceilings are switched off.

Honour Retry-After and back off. When limiting is enabled, every authenticated response also carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; until it is, those headers are deliberately absent rather than advertising a budget nothing enforces.

Retry: Yes, after Retry-Afterdetails.limitdetails.remainingdetails.resetSeconds
internal_errorHTTP 500

Something failed on our side. The response describes nothing further on purpose.

Retry with the same Idempotency-Key — that is exactly the case the header exists for. If it persists, quote error.requestId to support; it resolves the full server-side record.

Retry: Yes, with backoff
provider_unavailableHTTP 503

A capability exists but is not currently available — most often video generation, which reports itself as videos.available: false in GET /v1/capabilities before you spend a request finding out.

Check GET /v1/capabilities first, and retry later. Nothing about your request was wrong, and nothing was charged.

Retry: Yes, with backoff

When a job fails

A job that reaches failed is a different kind of failure: the request that started it succeeded with a 202, and the render failed later. Read failure.code on the job — it is its own append-only vocabulary, and the raw provider text is never included.

content_blockedfailure.code

Safety and moderation refused the prompt, a reference, or a face.

Change the prompt or the references. Retrying the identical request is refused the same way.

Retry: Not without changing the request
insufficient_creditsfailure.code

The balance ran out mid-render.

Nothing was charged for the failed job. Top up and start it again.

Retry: Not without changing the request
character_unavailablefailure.code

A cast member could not be used for this render — its portrait is not ready, or was rejected.

Read the character and check its status. The message says whether this is temporary.

Retry: Not without changing the request
timed_outfailure.code

The provider took too long and the job was abandoned.

Start it again with a NEW idempotency key. Consider a shorter or simpler render.

Retry: Yes, with backoff
provider_unavailablefailure.code

Our provider account cannot serve this render — a model that is not enabled, for instance.

Not something your request can fix. Retry later, and check GET /v1/capabilities for what is currently available.

Retry: Yes, with backoff
provider_errorfailure.code

The provider rejected or abandoned the render for its own reason.

Usually transient — start the job again. If it repeats for one prompt, treat that prompt as blocked.

Retry: Yes, with backoff
generation_failedfailure.code

Something else went wrong. The message carries whatever detail is safe to share.

Retry once; if it persists, quote the job id to support.

Retry: Yes, with backoff

A failed character carries its own smaller set on the same field: insufficient_credits when the balance ran out mid-training (you were not charged) and training_failed when the face could not be built — see characters.

Running out of credits

A 402 insufficient_credits is the one failure that is not about the request. Nothing was started and nothing was charged — the balance simply could not cover the job. GET /v1/account/usage is where the whole picture is: the two credit buckets, what this key and the account have spent over the period, and an autoReload block.

Automatic top-ups charge the account owner’s saved card when the balance falls below a figure they chose, up to a ceiling per UTC calendar month. That is why a balance can go up with no purchase in your logs, and why it can suddenly stop doing so:

  • enabled, threshold, amount and monthlyCap — the policy, as decimal strings. All four are null/false when it was never set up.
  • monthToDate — credits added automatically since the first of the month. Compare it with monthlyCap: once they meet, no further top-up runs until the month rolls over, however low the balance goes.
  • lastError and lastAt — why the last attempt stopped. A declined or expired card reads here first; it does not otherwise announce itself to your integration.

Debugging a call

  1. Log X-Request-Id on every response, success or failure. It is on all of them, and it is the only handle support has.
  2. Read error.details.path first on a 400. It names the exact field, prefixed with where it came from — body.refKeys[2], query.limit, params.id.
  3. Check for a typo before checking the docs. unknown_field means the field does not exist at all; invalid_request means it exists and your value is wrong. They are separate codes precisely so a misspelled optional field does not look like a missing required one.
  4. A 404 is often a scope or origin problem, not a wrong id. Jobs created in the Musavir studio are invisible to API keys by design, and another account’s resources answer the same way.
  5. A 401 on a key that used to work means it was paused, revoked or expired — all three read the same on the wire. Check it in the dashboard.

For retry mechanics — which failures are safe to repeat, and with which key — see idempotency and retries.