# Errors

Every non-2xx response is `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).
One shape, on every endpoint:

```json
{
  "type": "https://docs.genuineai.app/errors/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "Validation error",
  "instance": "/api/v1/agents",
  "code": "validation_failed",
  "errorId": "8f14e45f"
}
```

| Field | |
|---|---|
| `type` | URI identifying the kind of error. |
| `title` | Short summary of the kind. Prose; do not branch on it. |
| `status` | The HTTP status, repeated in the body. |
| `detail` | What went wrong with *this* request. Prose. |
| `instance` | The path that produced it. |
| `code` | **Branch on this.** A value from the closed list below. |
| `errorId` | Correlates with our logs. Quote it when asking us about a request. |

## Branch on `code`

`code` comes from a fixed vocabulary, and the list is enforced closed: a code minted by
a provider we call (a payment processor, a telephony provider) never appears here. It
travels in `upstreamCode` instead, so `code` always means something we defined.

<CodeTabs syncKey="lang">

```js title="JavaScript"
if (err.code === "insufficient_credits") promptTopUp();
else if (err.code === "rate_limited")    retryAfter(res.headers.get("Retry-After"));
else if (err.code === "validation_failed") showFieldErrors(err.invalidFields);
```

```python title="Python"
if err["code"] == "insufficient_credits":
    prompt_top_up()
elif err["code"] == "rate_limited":
    retry_after(res.headers.get("Retry-After"))
elif err["code"] == "validation_failed":
    show_field_errors(err["invalidFields"])
```

</CodeTabs>

Adding a code is a contract change. Removing or renaming one is a breaking change and is
subject to the deprecation policy.

## Codes by status

| Status | Default code | Means |
|---|---|---|
| 400 | `bad_request` | Malformed request. |
| 401 | `unauthorized` | Missing or invalid credentials. |
| 402 | `payment_required` | Billing action needed. |
| 403 | `forbidden` | Authenticated, not permitted. |
| 404 | `resource_not_found` | No such resource, or not visible to you. |
| 409 | `conflict` | Conflicts with current state. |
| 410 | `resource_gone` | Existed, deliberately removed. |
| 413 | `payload_too_large` | Body over the limit (see below). |
| 415 | `unsupported_media_type` | Send JSON. |
| 422 | `validation_failed` | Failed validation; see `invalidFields`. |
| 429 | `rate_limited` | Slow down. See [rate limits](/rate-limits). |
| 500 | `internal_error` | Our fault. `errorId` is the thing to quote. |
| 502 | `upstream_error` | A service we depend on failed. |
| 503 | `service_unavailable` | Temporary. [Check status](https://status.genuineai.app), then retry. |

Several codes are narrower than their status, because each calls for a different
response:

- **`license_required`** (403): your workspace does not have this module, and a
  permission change will not fix it. The response names the module in `feature`, so you
  can match on that rather than parse the message:

  ```json
  {
    "status": 403,
    "code": "license_required",
    "detail": "This workspace does not have the \"module-design\" license feature. See https://docs.genuineai.app/plans-and-modules",
    "feature": "module-design"
  }
  ```

  See [plans and modules](/plans-and-modules) for what each module covers.
- **`insufficient_credits`** (403): the workspace is out of credits. Top up rather than
  retry.
- **`maintenance`** (503): planned downtime, as against `service_unavailable` for an
  unplanned failure. Every endpoint answers this for the length of the window, `detail`
  carries the message written for it, and `Retry-After` estimates how long it has left.
  Nothing you sent was applied, so the request is safe to replay in full. See
  [maintenance windows](/maintenance).
- **`business_rule`** (varies): a deliberate refusal rather than a fault. The `detail`
  explains it and is written to be shown to a person.

## The rest of the vocabulary

The remaining codes are narrower still. Each names a situation a client can do something
specific about, and most appear on one area of the API only.

| Code | Status | Where | Means |
|---|---|---|---|
| `daily_cap_reached` | 429 | Chat widgets | The widget's daily credit cap for this workspace is spent. Resets with the day. |
| `session_limit_reached` | 429 | Chat widgets | This visitor has started as many widget sessions today as the widget allows. |
| `session_expired` | 401 | Chat widgets | The widget session token is no longer valid. Start a new session. |
| `assistant_busy` | 409 | Chat widgets | This conversation already has a reply in flight. Wait for it rather than sending again. |
| `server_busy` | 429 | Chat widgets | The shared AI queue is saturated. Back off and retry. |
| `turnstile_failed` | 403 | Chat widgets | The bot check did not pass. |
| `agent_not_public` | 410 | Chat widgets | The agent behind the widget is no longer published. |
| `origin_not_allowed` | 403 | Browser calls | The request came from a web origin that is not on the workspace's allowlist. |
| `design_locked` | 403 | Designs | The design is locked and cannot be edited until it is unlocked. |
| `design_is_template` | 403 | Designs | The operation applies to a design, and this one is a template. Create a design from it first. |
| `template_missing` | 400 | Designs | The request needs a template and named none. |
| `template_gone` | 404 | Designs | The template the request named no longer exists. |
| `fields_missing` | 400 | Designs | The template declares no fields the request could fill. |
| `text_fields_missing` | 400 | Designs | The template declares no text fields the request could write into. |
| `export_too_large` | 422 | Designs | The export exceeds the pixel budget. Lower the resolution or the size. |
| `ai_generation_failed` | 502 | Generation | The model produced no usable result. Retrying is reasonable; the request itself was fine. |
| `file_not_recoverable` | 409 | Files | The file is past the point where it can be restored from the trash. |
| `db_pool_timeout` | 503 | Any | The database is under load. Back off and retry. |

Eight more codes are reserved in the vocabulary for request-parsing and platform failures
and are not answered today: `malformed_json`, `malformed_body`, `unsupported_encoding`,
`unsupported_charset`, `request_aborted`, `not_implemented`, `gateway_timeout` and
`geo_restricted`. A client that branches on `code` needs no special handling for them; an
unknown code falls through to the handling for its status.

## Validation errors

A 422 adds `invalidFields`, with one entry per field that failed:

```json
{
  "status": 422,
  "code": "validation_failed",
  "invalidFields": [
    { "path": "agent_audience", "location": "body", "msg": "core:validation.enum" }
  ]
}
```

`msg` is a translation key rather than an English sentence. Resolve it or show your own
copy.

## 4xx and 5xx

The distinction is meaningful. A 4xx reports a problem with what you sent and returns a
usable `detail`. A 5xx reports only that something failed on our side; it carries
`errorId` and is investigated by us.

If 5xx responses are arriving in numbers rather than one at a time, the
[status page](https://status.genuineai.app) is the fastest way to tell a platform incident
from something specific to your integration, and to see when it is resolved without
retrying to find out. Quote the `errorId` if what you are seeing is not on it.

## Request body size

Request bodies are capped at **10 MB**. Exceeding the cap returns 413 with the limit
named in `detail`. This does not apply to file uploads, which go directly to storage
through signed URLs and never pass through the API.
