# 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.

```text
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.

## Request errors

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:

```json
{
  "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](https://mynth.io/docs/concepts/rate-limits.md) has the rest.

### Validation errors

`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:

```json
{
  "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`:

- a `size` the pinned model cannot produce, such as `16:9_4k` on a model without
  4k. The message names the size to use instead
- `inputs` the pinned model cannot take, or too many of them
- a video resolution tier, `duration`, or `audio` the model does not offer. The
  message lists the values it does
- `metadata` over 2048 bytes
- a `destination` name you don't own
- 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. The message names the file

With `model: "auto"`, input and size checks wait until the worker picks a model;
see [task failures](#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`.

### Malformed requests

Before the body is validated, it has to be JSON, sent as JSON:

```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."
  }
}
```

```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`.

## 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.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.

```json
{
  "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`.

### 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         |
| `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](https://mynth.io/docs/concepts/tasks.md#retries-and-timeouts). What `restricted_content`
means is on [prompts and images](https://mynth.io/docs/concepts/prompts.md).

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`, 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.
- A create that fails while queueing marks its task `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.
- 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`, `code`,
`message`, and `issues`, and `TaskAsync*` errors while waiting on a task.
[SDK errors](https://mynth.io/docs/sdks/typescript/errors.md) 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](https://mynth.io/docs/sdks/cli/commands.md#exit-codes) has the table.

## Next steps

- [Tasks](https://mynth.io/docs/concepts/tasks.md): statuses, partial failure, and attempts.
- [Authentication](https://mynth.io/docs/api-reference/authentication.md): the auth codes in context.
- [Webhooks](https://mynth.io/docs/concepts/webhooks.md): the same codes, pushed to you.
