code field — a machine-readable string that never changes once published. This page lists every code the API can return, along with the high-level type SDKs branch on and the HTTP status code each maps to.
For the shape of the error envelope itself (type, code, message, doc_url, request_id, etc.), see Error format.
Branching guidance
- Branch on
type, not status code alone. Status codes collapse distinct failure modes;typeseparatesconflictfromidempotency_conflicteven though both surface as409. - Treat
codeas the narrow key. Use it for telemetry, runbook lookups, and per-error UI copy. Never displaymessageto end users — it is developer-facing and may change. - Honor the envelope’s
retryablefield. Codes with an explicit retry contract emitretryableon every error response:falsemeans the same request can never succeed (fix the input or state first),truemeans transient — retry with backoff. Where the column below shows —, retryability is context-dependent: the envelope carriesretryableon the responses whose cause settles it (e.g.source_url_unreachableistruefor a timeout andfalsefor a 404) and omits it otherwise; when it is absent, retry onlyupstream_errorandrate_limited. - A file link that could not be created is a retry, not a failure. When a route that returns a signed URL (
GET /v1/drive/files/{id}/download, a signed upload URL) cannot create one through a brief outage on our side, it answerstemporarily_unavailable(503,upstream_error,retryable: true) withRetry-Afterandretry_after_ms. The file is unaffected: send the same request again after the wait. - Always log
request_id. It is the single field that lets Moda support trace a specific failure across logs and Sentry.
Billing refusals
insufficient_credits and paid_plan_required (both 402) carry the remedy in details:
A member who reached their own monthly credit limit gets
client only: a workspace admin has to raise the limit, and no billing page clears it.
Voyager credits are prepaid: a Voyager-billed request its credits cannot cover is refused, never billed later. insufficient_credits then carries details.reason credits_exhausted and details.auto_reload (enabled, armed, paused, paused_reason, cap_reached); read armed to know whether auto-reload will buy more. While the workspace’s auto-reload is buying a credit pack, the refusal is credits_reloading (402, retryable, with retry_after_ms and Retry-After): retry once the pack lands, within seconds. Agent chat billed in arrears is not available on prepaid Voyager credits: chat_unavailable (402, not retryable).
chat_not_enabled and chat_cap_reached (both 402) refuse Voyager agent chat, which is billed on its own meter apart from plan credits: a workspace admin turned chat off (it is on by default, with a $50 monthly limit), or this month’s chat spend reached the workspace’s limit. They carry the same details, with billing_product voyager, remedy enable_chat or raise_cap, and upgrade_url opening the Voyager dashboard’s chat settings; chat_cap_reached adds the limit and this month’s spend as monthly_cap_cents and month_to_date_cents. A workspace admin clears them; the limit also resets on the 1st (UTC). payment_failed (402) refuses Voyager agent chat while the workspace’s last Voyager payment has failed and Stripe is retrying it, so no more charges land on a failing card; plan credits stay usable meanwhile. Its details carry remedy update_payment and an upgrade_url opening the Voyager dashboard to update the payment method.
Once agent chat is included in Voyager plans, chat draws the plan’s credits like media: past them it gets insufficient_credits, and there is no separate chat spending limit (chat_cap_reached is not returned). model_not_in_plan (402) refuses a chat model the workspace’s Voyager plan does not include: details carry remedy upgrade, required_plan (starter, creator or ultra) and an upgrade_url opening the Voyager dashboard’s plan picker with that plan selected. Pick a model on the plan from GET /v1/inference/models instead, or upgrade. The provider proxy returns the same model_not_in_plan for a media model above the plan: Seedance 2.5 (every bytedance/seedance-2.5/... endpoint) needs Voyager Creator or higher, so a Voyager-billed Starter workspace is refused before any credits are held.
model_access_required (403) refuses a generation with a model that also needs the workspace’s approval (Wan 3.0 Spicy and Wan 2.7 Spicy: Voyager Creator or higher, and an approved request). details are {access_key, access, model_name}, access being the workspace’s status: none (never asked), requested (waiting for review) or denied. Ask with POST /v1/model-access/{access_key}/request; GET /v1/credits reports the status as model_access. Below the plan the model needs, model_not_in_plan answers first, with details.access_key. Before anything is held or sent, a Spicy request’s prompt and images are checked by Moda: content with anyone under 18 (in any mature, suggestive or nude scene), real or recognizable people, explicit sexual content (sex acts, explicit genitalia), extreme gore or other illegal content is refused (suggestive scenes and partial, non-explicit nudity of fictional adults, and action violence, blood and horror, are allowed) with provider_content_policy_violation (422, not retryable; the check is automated, so its message is hedged and its reason is the check’s best reading, see Provider refusals), details being {target, reason, checked_by: "voyager"} (target prompt or reference_images; reason minors, real_person, explicit_sexual, extreme_gore or prohibited, the last also for other illegal content). A Spicy request takes only its model’s inputs: Wan 3.0 Spicy (every edition) prompt (required for text-to-video), duration (2–30 s), resolution (480p, 720p or 1080p; Pro and Prime Pro 1080p, 2k or 4k), aspect_ratio (adaptive, 16:9, 4:3, 1:1, 3:4, 9:16), generate_audio and seed, plus image_url (required) and end_image_url for image-to-video, or reference_image_urls (1–10, required) for reference-to-video; Wan 2.7 Spicy prompt (required), image_url (required), negative_prompt (at most 2,000 characters), duration (2–15 s), resolution (720p or 1080p), generate_audio and seed. A prompt runs to at most 20,000 characters (Wan 2.7: 12,000), every image is a file uploaded through POST /v1/provider-proxy file upload (the HTTPS URL that upload returns; no inline data: images), there are no reference videos yet, and no prompt expansion (enable_prompt_expansion is always sent false). Anything else is refused with unprocessable (422, details {field, reason}) before the check. content_check_unavailable (503, retryable) means the check could not answer; nothing was held, and the same request can be sent again.
model_not_found (404) answers a model endpoint ID that is not in the model catalog (a POST /v1/provider-proxy submission, before any plan check or credit hold, and Voyager remote admission). The message names the closest real models, and details is {model, suggestions}, closest first; suggestions is empty when the search found nothing close. An ID missing its owner (qwen3-tts-1.7b-hosted for .../qwen3-tts/...) or carrying a scheme prefix (local:qwen3-tts-1.7b; endpoint IDs have none) is answered this way too. Copy one exactly; never construct or guess an ID or version.
input_too_long (422, not retryable) also refuses an input clip longer than the model takes (Wan 2.2 VACE: 1–15 s), before anything is held or sent. param names the clip input and details carry {field, limit, min, length, unit} with unit seconds; the message states the clip’s length and the accepted range. A clip whose length cannot be read is not refused here.
no_voyager_seat (402) refuses a Voyager-billed request from a user with no seat on the workspace’s Voyager plan. The workspace has a plan, so a workspace admin seats the user: details carry billing_product voyager, remedy seat and an upgrade_url opening the Voyager dashboard’s seats (intent=seat).
billing_unavailable (503) means the credit balance could not be verified and nothing was sent to the provider. It is safe to retry after retry_after_ms.
not_available_on_voyager_billing (403) refuses a Voyager-billed request for an operation or model that only Moda credits can pay for, such as a hosted copy of an ElevenLabs audio model. details.billing_product is voyager, and the message names what to use instead where there is an alternative (the direct audio models, ElevenLabs and Fish Audio, listed in details.use_instead.models, for generate-audio). No billing page clears it.
Provider refusals
When the model itself, or Moda’s own pre-check, refuses a generation, the answer is a4xx with retryable: false and never a 5xx: nothing ran, nothing is charged, and any credit hold is released. Besides the legacy code, message, details and retryable, every refusal on the Voyager app route (/api/v1/voyager/generations) carries error, the same typed object the Voyager API answers on /v1/jobs, /v1/estimate, a job’s error, MCP tool results and webhooks:
input_too_large comes from 3D rigging (POST /v1/media/generate-3d rigging, the Voyager API’s rig) when the input mesh has more triangles than rigging takes, whether the provider refuses it at once or fails the task; nothing is charged. Remesh the model first (the remesh operation with target_polycount 100000 or less; on the Voyager API, POST /v1/jobs with "operation": "remesh" and the source model as input.input_task_id (its job) or input.mesh (formerly input.model_url, still accepted)), then rig the remeshed one. An image-to-3D job meant for rigging can ask for the lower count up front with should_remesh: true and target_polycount: 100000.
invalid_parameter (422, retryable: false) comes from POST /v1/media/remove-background when the model refuses the image itself: it has more pixels or a larger file than the model reads, it is too small or its aspect ratio too extreme, the model cannot load it (a corrupt or unsupported image), or the cutout comes back fully transparent because the model found no subject to keep. The same image gets the same answer, and the message says what to change. Nothing is charged. Send a different image. With an idempotency_key, the same key is refused while its claim is held, so use a new one for the new image. The same code and retryable: false also answer an image Moda cannot read before it reaches the model (a format it cannot decode, a damaged file, or an SVG it cannot rasterize; use a PNG, JPEG or WebP). Nothing ran, so that key is released and can be sent again.
A content-policy reason is an automated check’s best reading, never a confirmed cause: checks are sometimes wrong, so the message says so and never accuses the requester. Agents should treat it as “this request was declined; try rewording or another model”, not as a judgement of the user or of their other requests.
reason: public_figure means the model’s check may have read the request as showing or naming a well-known person, which that model doesn’t allow (likeness is for a model that refuses any real person). Another model often allows it, and running the same model again doesn’t help. details.suggested_models lists up to three models that usually allow public figures for the same operation and that the workspace’s plan runs, cheapest first, each with its model_id and display name model; the message names them too. It is absent when none fits. Like reason, it comes from automated checks that can be wrong. A public_figure refusal, like every content refusal, is charged nothing.
When another video model declines a text-to-video or image-to-video request for its content, Voyager may run its own automated check on that request. If the check finds nothing Wan 3.0 Spicy refuses (anyone under 18, a real or recognizable person, explicit sexual content, extreme gore or other illegal content), the refusal adds details.alternative: model (Wan 3.0 Spicy), model_id (wan-3.0-spicy; the legacy proxy names it endpoint), operation, access_key and requires. requires is none when the workspace can run it now, access when the workspace needs an approved access request first, or plan when it needs Creator or higher, with details.upgrade_url. Like reason, this offer comes from an automated check that may be inexact. The refusal’s code, status, message and reason are unchanged. Offer it to the user and run it only with their consent. A Spicy run takes only Spicy’s own inputs: send the prompt, its options and the images, uploaded as Voyager files, in Spicy’s form (other fields don’t carry over). Spicy checks the new request again before it runs.
When the model’s provider is down, refuses our own account or credentials, or our balance with it is spent, the answer is provider_model_unavailable (503, upstream_error, retryable: true, retry_after_ms; canonical upstream_unavailable), whichever provider runs the model: our outage, never the request’s fault, and nothing is charged. Retry shortly or choose another model. model_temporarily_unavailable (503, retryable; canonical model_unavailable) means only that Moda paused the model after repeated upstream failures; it clears on its own.
A cancel that can’t stop the job
Some running jobs can’t be stopped once their model has started them. Their cancel answers409 with retryable: false and the message “This job can’t be stopped now, so it runs to its normal end and is charged as usual. Keep checking its status to collect its result.” The canonical code is cancel_unsupported (type conflict); on the provider proxy’s legacy answers the released code stays invalid_operation and details.canonical_code is cancel_unsupported. Don’t send the cancel again and don’t stop polling: keep reading the job’s status, then collect its result. A job that fails is not charged.
A refusal after the image job started
OnPOST /v1/media/generate-image, /v1/media/edit-image, /v1/media/upscale and /v1/media/outpaint, a refusal can arrive after the model already accepted the job, for example a refused credential while the job runs or a content-policy verdict on its result. Its status, code and retryable are unchanged. The accepted job may still run, so Moda keeps the request’s idempotency_key claimed rather than letting a retry start a second job. A retry with the same key answers 409 idempotency_in_flight until the claim lapses (at least 30 minutes). The refusal itself then has no Retry-After, and its message says to send a new idempotency_key for another attempt. A refusal before the model accepted the job releases the key, as before.
Canonical codes
An opt-in model request ("schema": "voyager"), the Voyager app route’s error, and the Voyager API answer every Studio code with one canonical code. Each body carries one retryable: the raise site’s when it knows (a cause it knows is final reads retryable: false, and its message never says “try again”), else the canonical code’s. A job’s error, its webhook and MCP get_job keep the same answer: model_unavailable for a model Voyager can’t run this request on yet is retryable: false, while a model Moda paused (model_temporarily_unavailable) clears on its own and stays retryable: true. A Studio-written refusal also keeps its original code, details and status in details.refusal, except a content refusal: content_policy carries only its canonical hedged message and details (reason, target), never the check’s own wording or tokens.
A finished job’s
outcome_unknown (GET /v1/jobs/{id}, its webhook, MCP get_job) is retryable: false: the job is over and a new submission may be charged again, so check your usage before submitting it again. The synchronous outcome_unknown answer stays retryable: true: ask for the job’s status again.
Any other code in the catalog below is answered as itself, except a Studio code at a 5xx: it keeps its name only when the catalog marks it not retryable at that status (media_render_unrecoverable: the render is over, and a repeat may be charged again), and is otherwise answered by its status. The Voyager API’s own codes above (outcome_unknown, upstream_unavailable, …) always keep theirs. A code missing from the catalog is answered by its status: internal_error for a 500, upstream_unavailable for another 5xx, forbidden, not_found, conflict, rate_limited and so on for a 4xx, and invalid_request only for a 400 or 422.
Model catalog refusals
GET /v1/catalog, GET /v1/catalog/models/{id} and GET /v1/catalog/me keep the flat body released apps read, {"error": "<code>", "message": "..."}, where error is a string rather than the object above. Beside it, every refusal carries request_id and canonical_error, the typed error object (type, code, message, request_id, retryable, doc_url):
canonical_error.code and canonical_error.retryable. The flat codes are model_unknown (404: the catalog lists no model with this ID), revision_not_found (404: ?revision= names a revision no longer served; read the current catalog), not_acceptable (406: Accept-Encoding refuses both gzip and identity), permission (403: /me takes a Voyager credential) and not_found (404: the credential has no workspace).
Catalog
Every code the API returns, generated from the registry (backend/app/api/public/error_codes.yaml). The Voyager API (api.voyager.so) answers model and job errors in its own canonical codes, listed in its OpenAPI document’s x-voyager-errors; Canonical codes says which canonical code each Studio code is answered with.
Type reference
Thetype field is a closed set. SDKs may translate types to typed exceptions; new code values may be added to an existing type without notice, so always include a default branch.