Errors
The shapes an error takes, every request error code and task failure code, how a failed task differs from a rejected request, and what is safe to retry.
Two different things fail, and they look nothing alike. A request error
rejects the call before anything is queued, with a 4xx or 5xx right away.
A task failure happens after a 201: the task runs and the work fails
seconds or minutes later.
POST /image/generate
│
├─ rejected 4xx / 5xx { "error": { "code": "unauthorized", "message": "..." } }
│ nothing queued, held, or charged
│
└─ 201 { task_id } ──▶ task runs ──┬─▶ completed items can still carry an error
└─▶ failed errors: [{ code, message? }]Handle both.
Every request error has the same body: an error object with a code in
lowercase snake_case and a message that says what to change:
{
"error": {
"code": "insufficient_balance",
"message": "This request needs $0.12, which is more than your available balance. Top up at https://mynth.io/dashboard/wallet, or lower the cost (fewer images, a cheaper model). Nothing was queued or charged."
}
}Every error has this envelope, including a 404 for an unknown path, a
415, and a 500. No error response has data, and no success response has
error.
Branch on code, never on message. The message is written for people and
may change. It is optional: invalid_token and token_expired have none. validation_error can add an
issues array, insufficient_scope and scope_escalation add a scopes
object, and public_access_token_failed adds task_id, all inside error.
| Status | Code | Means | Do |
|---|---|---|---|
| 400 | validation_error | The body, query, or path failed validation. See below | Fix what message names |
| 400 | invalid_json | The body is not valid JSON. message includes the parser's position | Fix the JSON |
| 400 | api_key_limit_reached | The account already has 100 live keys | Delete a key first |
| 400 | webhook_api_key_not_found | api_key_ids names a key you don't have | Drop the unknown id |
| 400 | destination_invalid_provider | A destination update changed provider.id | Create a new destination |
| 401 | unauthorized | No usable bearer header, or an unknown or deleted key. message says which | Fix the credential |
| 401 | invalid_token | A pat_ that is malformed or not signed by Mynth | Get a new token from your server |
| 401 | token_expired | A pat_ past its hour | Poll from the server, or use a webhook |
| 402 | insufficient_balance | Available balance is below the task's estimate. message states the estimate | Top up. Nothing was queued |
| 402 | spending_limit_exceeded | The key's cap for the current day, week, or month is used up. message states the cap, what is used, and what the request needs | Wait for the period, or raise the cap |
| 403 | insufficient_scope | Valid key, missing scope. Adds scopes.required and scopes.current | Use a key with the scope, or add it |
| 403 | scope_escalation | An API key asked for manage or keys. Adds scopes.requested, scopes.allowed | Do it in the dashboard |
| 404 | task_not_found | Unknown task, another account's task, or a pat_ for a different task | Check the id and the credential |
| 404 | api_key_not_found | Unknown key id | Check the id |
| 404 | destination_not_found | Unknown destination id | Check the id |
| 404 | webhook_not_found | Unknown webhook id | Check the id |
| 404 | not_found | No route matches the method and path. message names both | Check the URL |
| 409 | destination_name_taken | You already have a destination with that name | Pick another name |
| 413 | payload_too_large | POST /image/upload body over 100 MB | Upload fewer or smaller files |
| 415 | unsupported_media_type | A JSON endpoint without Content-Type: application/json, or an upload without multipart/form-data | Set the Content-Type header |
| 500 | internal_server_error | Our fault, including a failure to store or read a destination secret | Retry, after checking GET /tasks |
| 500 | public_access_token_failed | The task was created and will run, but its pat_ could not be generated. Adds task_id | Follow task_id. Don't resend |
| 502 | destination_test_failed | Your storage rejected the test upload. message is the provider's | Fix the credentials or path |
A 429 means back off and retry. There is no Retry-After header.
Rate limits has the rest.
validation_error covers two kinds of check, with one shape.
A field that fails the schema: a missing prompt, a count outside 1 to 20
or not a whole number, an unknown model id, a size that is not a size, or a
field the endpoint does not define. These add issues, one entry per invalid
field, and message states all of them:
{
"error": {
"code": "validation_error",
"message": "model: \"flux.2-pro\" is not supported. Did you mean \"black-forest-labs/flux.2-pro\"? See https://mynth.io/models, or GET /models, for the model IDs. count: Too big: expected number to be <=20.",
"issues": [
{
"path": ["model"],
"message": "\"flux.2-pro\" is not supported. Did you mean \"black-forest-labs/flux.2-pro\"? See https://mynth.io/models, or GET /models, for the model IDs."
},
{ "path": ["count"], "message": "Too big: expected number to be <=20" }
]
}
}path points into the body, query, or path parameters: ["inputs", 0, "source"]
is inputs[0].source, and an empty path means the body as a whole. Map
issues onto your own form fields, or show message as it is. The response
does not echo your request.
Each issue in message starts with its field path, such as count: or
inputs[0].source:. An issue about the whole body has no path prefix.
Use code and issues[].path to handle errors in code, since the wording of
message can change.
A request that fits the schema but not the model or your account has no
issues, only message:
size the pinned model cannot produce, such as 16:9_4k on a model without
4k. The message names the size to use insteadinputs the pinned model cannot take, or too many of themduration, or audio the model does not offer. The
message lists the values it doesmetadata over 2048 bytesdestination name you don't ownWith model: "auto", input and size checks wait until the worker picks a model;
see task failures.
Every request body rejects fields it does not define, at any depth. A typo
such as {"prompt": "...", "widht": 512} answers 400 with an issue at
["widht"] instead of running without it. A camelCase name, such as
aspectRatio, gets a message saying field names are snake_case.
Before the body is validated, it has to be JSON, sent as JSON:
{
"error": {
"code": "unsupported_media_type",
"message": "The request has no Content-Type header. Send the body as application/json and set Content-Type: application/json."
}
}{
"error": {
"code": "invalid_json",
"message": "The request body is not valid JSON (Expected double-quoted property name in JSON at position 17 (line 1 column 18)). Check for trailing commas, unquoted keys, or a truncated body."
}
}POST /image/upload is the exception: it takes multipart/form-data, and a
JSON body there answers 415 unsupported_media_type.
An accepted task settles as completed or failed, and both can carry error
codes. Where the code lands depends on the task type:
| Task type | A failure appears in |
|---|---|
image.generate | result.images[].error, task stays completed |
video.generate | result.videos[].error, task stays completed |
image.remove_background, image.upscale, image.rate, image.alt, image.review | errors[], task is failed |
So a refused prompt on POST /image/generate gives you 201 at creation and a
completed task whose only image failed. Checking status === "completed" is
not enough. Walk the array.
{
"status": "completed",
"result": {
"model": "black-forest-labs/flux.2-pro",
"images": [
{
"status": "failed",
"error": {
"code": "restricted_content",
"message": "The request was blocked by content moderation."
}
}
]
}
}An image.generate or video.generate task reaches failed only when the
work around generation broke: resolving the model, the inputs, or the size,
queueing the task, or the daily sweep that expires a task stuck on pending.
With a pinned model, inputs and sizes it cannot serve are rejected at create
with 400 validation_error. With model: "auto" they are only known once a
model is picked, so a _4k size or an edit that lands on a model without
support fails the task with capability_not_supported.
| Code | Means | Retry helps |
|---|---|---|
restricted_content | The provider or the model refused the content. Mynth did not | Mynth does not retry. You may resend |
invalid_prompt | The prompt was empty or too long for the model | No |
invalid_input | An input image could not be fetched or read | No. Check the URL is publicly reachable |
capability_not_supported | The model does not support an option you sent | No. Use another model or option |
output_too_large | image.upscale would produce more than 4096x4096 pixels | No. Send a smaller image or factor |
providers_busy | No provider for that model has capacity right now | Yes, later |
rate_limited | Providers kept rate-limiting the request | Yes, shortly |
timeout | The provider did not answer in time | Yes |
provider_error | The provider failed for another reason | Yes |
fetch_failed | image.rate, image.alt, or image.review could not fetch url | Only if the URL was flaky |
rating_failed | image.rate ran but produced nothing usable | Yes |
alt_generation_failed | image.alt ran but produced nothing usable | Yes |
review_failed | image.review ran but produced nothing usable | Yes |
task_expired | Still pending 24 hours after its last update, then swept | Yes. Something broke on our side |
enqueue_failed | Queueing failed right after creation. The create answered 500 | Yes |
unknown_error | Nothing else matched | Once |
Before a retryable code reaches you, Mynth has already retried across
providers: up to four attempts per image and three per video. See
tasks. What restricted_content
means is on prompts and images.
Webhooks carry the same values: .completed events carry result, and
.failed events carry errors.
400, 401, 403, 404, 409, 413, and 415 fail the same way every
time. Fix the request.402 insufficient_balance clears when you top up.
402 spending_limit_exceeded clears when the key's period rolls over or you
raise the cap.502 destination_test_failed is your storage rejecting the upload. Fix the
destination.500 and connection failures are worth retrying, with one catch: there are
no idempotency keys. Every create mints a new task and holds cost again,
so resending a create that actually succeeded runs and charges it twice. If
a create times out, check GET /tasks before sending it again.failed with
enqueue_failed, releases the hold, and answers 500. Retrying that one is
safe.500 public_access_token_failed is the opposite: the task was created and
will run. Follow error.task_id with your API key instead of resending.The SDK throws MynthAPIError for a request error, with status, code,
message, and issues, and TaskAsync* errors while waiting on a task.
SDK errors covers both.
The CLI prints the message to stderr and exits with a code scripts can branch
on: 2 usage, validation_error, invalid_json, and
unsupported_media_type, 3 auth, 4 balance or spending limit, 5
restricted_content, 6 a 429, 1 anything else.
CLI commands has the table.