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 { "code": "UNAUTHORIZED", "message": "..." }
│ nothing queued, held, or charged
│
└─ 201 { taskId } ──▶ task runs ──┬─▶ completed items can still carry an error
└─▶ failed errors: [{ code, message? }]Handle both.
Request errors#
The body is flat, with code at the top level in SCREAMING_SNAKE_CASE:
{ "code": "INSUFFICIENT_BALANCE", "message": "Insufficient balance." }Branch on code, never on message. message is optional: TASK_NOT_FOUND,
API_KEY_NOT_FOUND, INVALID_TOKEN, and TOKEN_EXPIRED have none.
INSUFFICIENT_SCOPE and SCOPE_ESCALATION add a scopes object.
| Status | Code | Means | Do |
|---|---|---|---|
| 400 | VALIDATION_ERROR | A check that runs after the schema passed. See below | Fix what message names |
| 400 | API_KEY_LIMIT_REACHED | The account already has 100 live keys | Delete a key first |
| 400 | WEBHOOK_API_KEY_NOT_FOUND | apiKeyIds names a key you don't have | Drop the unknown id |
| 400 | DESTINATIONS_INVALID_PROVIDER | A destination update changed provider.id | Create a new destination |
| 401 | UNAUTHORIZED | No usable bearer header, or an unknown or deleted key | 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 |
| 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 path | Check the URL. There is no /v1 prefix |
| 409 | DESTINATION_NAME_TAKEN | You already have a destination with that name | Pick another name |
| 413 | VALIDATION_ERROR | POST /image/upload body over 100 MB | Upload fewer or smaller files |
| 422 | INSUFFICIENT_BALANCE | Available balance is below the task's estimate | Top up. Nothing was queued |
| 429 | SPENDING_LIMIT_EXCEEDED | The key's cap for the current day, week, or month is used up | Wait for the period, or raise the cap |
| 500 | INTERNAL_SERVER_ERROR | Our fault | Retry, after checking GET /tasks |
| 500 | UNKNOWN_ERROR | Storing or reading a destination secret failed | Retry |
| 502 | DESTINATION_TEST_FAILED | Your storage rejected the test upload. message is the provider's | Fix the credentials or path |
VALIDATION_ERROR covers the checks that run after the schema passed:
metadataover 2048 bytes- a
destinationname you don't own inputsor options the chosen model cannot take- an upload with no files, more than 10 files, a file that is not JPEG, PNG, or WEBP, or a file under 1 KB or over 10 MB
A 429 other than SPENDING_LIMIT_EXCEEDED means back off and retry. There
is no Retry-After header. Rate limits has the
rest.
Schema rejections use a different shape#
A body that fails the schema never reaches the handler, so it gets no code.
The validator answers 400 with its own shape, which depends on the endpoint:
| Endpoints | 400 body |
|---|---|
/image/*, /video/* | { "data": <your body>, "error": [issues], "success": false } |
/tasks, /api-key, /webhook, /destinations | { "success": false, "errors": [issues] } |
| Any body that is not valid JSON | Malformed JSON in request body as plain text |
{
"data": { "model": "auto", "count": 1 },
"error": [
{
"path": ["prompt"],
"code": "required",
"expected": "a string",
"actual": "missing",
"message": "prompt must be a string (was missing)"
}
],
"success": false
}Read path and message on each issue. count outside 1 to 20, an unknown
model id, and a missing prompt all fail here. data echoes the request
body, so keep these responses out of logs you share.
Generation bodies drop fields they do not recognize instead of rejecting them. {"promt": "..."}
fails on the missing prompt, but {"prompt": "...", "widht": 512} succeeds and ignores
widht. Check the task's echoed request when a setting seems to have no effect.
Task failures#
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.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 (a
_4k size on a model without 4k fails with CAPABILITY_NOT_SUPPORTED),
queueing the task, or the daily sweep that expires a task stuck on pending.
Codes#
| 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 |
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.
What is safe to retry#
400,401,403,404,409,413, and422fail the same way every time. Fix the request.429 SPENDING_LIMIT_EXCEEDEDclears when the key's period rolls over.502 DESTINATION_TEST_FAILEDis your storage rejecting the upload. Fix the destination.500and 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, checkGET /tasksbefore sending it again.- A create that fails while queueing marks its task
failedwithENQUEUE_FAILED, releases the hold, and answers500. Retrying that one is safe. - A settled task never runs again. Retrying a task failure means a new request. Failed items are not charged.
In the SDK and CLI#
The SDK throws MynthAPIError for a request error, with status and code,
and TaskAsync* errors while waiting on a task.
SDK errors covers both, including why a schema
rejection leaves code undefined.
The CLI prints the message to stderr and exits with a code scripts can branch
on: 2 usage and VALIDATION_ERROR, 3 auth, 4 balance or spending
limit, 5 RESTRICTED_CONTENT, 6 another 429, 1 anything else.
CLI commands has the table.
Next steps#
- Tasks: statuses, partial failure, and attempts.
- Authentication: the auth codes in context.
- Webhooks: the same codes, pushed to you.