# Errors

> The per-`code` list and the typed `error.details` shapes are in the published OpenAPI schema — browse them in **[ReDoc](https://api.genomicintelligence.ai/redoc)** or fetch [`/v1/openapi.json`](https://api.genomicintelligence.ai/v1/openapi.json). This page covers the envelope, the discriminate-on-`code` rule, the handling strategy, and a few codes whose behavior the schema can't fully express.

Every non-2xx response uses the unified envelope:

```json
{
  "error": {
    "code": "<machine_readable_snake_case>",
    "message": "<human-readable summary>",
    "request_id": "<server-assigned id>",
    "details": "<optional, code-specific>"
  }
}
```

`error.code` is the **authoritative discriminator**. Never parse `error.message`. The same `request_id` appears on the `X-Request-Id` response header and on every server log line for the request — quote it in support tickets.

> The `X-Request-Id` **header** is set on every response, including any error whose body omits `error.request_id`. Read `error.request_id or X-Request-Id` so you always have a correlation id to quote. The bundled client does this for you.

## The code list

`code` is a closed enum in the schema — see the `ErrorBody.code` definition in [ReDoc](https://api.genomicintelligence.ai/redoc) or [`/v1/openapi.json`](https://api.genomicintelligence.ai/v1/openapi.json) for the current set.

A new code can ship before your client is regenerated — so **treat an unlisted value as a generic failure, not a parse error**. (The schema says so explicitly; a generated client that rejects unknown enum members will break on the next release, not on a bug.)

## Reading `error.details`

`details` is an `anyOf` over `ValidationFailedDetails` | `TaskNotSupportedByModelDetails` | `ModelNotFoundDetails` | `SyncTooLargeDetails` | `GenericDetails` | `null`, keyed on the sibling `code`. Switch on `code` first, then read `details`; codes without a typed shape carry only the human-readable `message` and should be treated as opaque.

> One trap, still live — plus one shape note:
>
> 1. **Shape.** For `validation_failed`, `details` is *usually* `{"errors": [...]}`, matching the schema; each entry has the standard FastAPI `{loc, msg, type}` shape. Older releases emitted the bare per-field array, so defensive clients that accept either stay correct. **One `validation_failed` breaks that shape**: the splice response-size cap sends `{task, record_count, maximum_records, sequence_length, threshold}` and no `errors` key (see below). Read `details.get("errors", [])` rather than indexing it.
> 2. **`loc`.** Some checks report where you'd expect — a sequence bound at `["body","sequence"]`, an unknown option key at `["body","options","<key>"]` — but expression's `tss_index` checks come from a whole-model validator and report at **`["body"]`**. There is no `["body","tss_index"]` to match on, and matching on it will silently never fire. Match on `error.code`; use `message` for display only.

## Notable codes

Most codes are self-explanatory from the OpenAPI schema. A few have behavior worth noting:

- **`504` (no envelope):** a sync request past the upstream read timeout is terminated by the edge proxy with the proxy's body, not the unified `{error: {...}}` envelope (see [Limits](./limits.md)). Retry with `Prefer: respond-async`.
- **`404 not_found` on an unknown task:** an unrecognised `{task}` segment — on either `POST /v1/tasks/{task}/predict` or `GET /v1/tasks/{task}/models` — is a `404`, not a `422`. Only the six published tasks exist.
- **`404 not_found` on cross-account access:** accessing another caller's job returns `404`, not `403`, to prevent id enumeration.
- **The two `413`s are different failures.** `payload_too_large` is the **16 MiB raw-body cap**, enforced before parsing — split the request. `sync_too_large` is an operation refusing **synchronous JSON** delivery above its published cap, with `details = {sequence_length, threshold}` — resubmit the identical body with `Prefer: respond-async`. Two operations have such a cap: the composite `find-genes-and-predict-expression` workflow above 50,000 bp and `POST /v1/tasks/annotation/predict` above 200,000 bp. The other five predict endpoints never emit it, and neither does an annotation request for `?format=bed` or `?format=gff3`, which stays synchronous at any accepted length. See [Limits](./limits.md).
- **A too-long `sequence` is *not* a `413`.** Above 500,000 bp — or below the task's floor — is `422 validation_failed` at `loc` `["body","sequence"]`, with the bound and your submitted length in the message.
- **`415 unsupported_format`:** an unsupported `?format=` (or `Accept`) value is rejected outright, never silently downgraded to JSON; the message names the supported set. Formats differ per task — promoter `json|bed|bedgraph`, splice `json|bed|gff3`, enhancer `json|bedgraph`, chromatin `json|bed`, annotation `json|bed|gff3`, expression JSON only. Text formats are synchronous-only: pairing one with `Prefer: respond-async` is a `400`.
- **`422 validation_failed` on a closed `options`:** every task's `options` object rejects unknown keys (`type: "extra_forbidden"`, `loc` `["body","options","<key>"]`). Notably `model` belongs at the **top level** of the body, not inside `options`. See [Tasks](../tasks.md#options).
- **`422 validation_failed` on the splice response-size cap:** `POST /v1/tasks/splice/predict` refuses a request whose response would carry more than **20,000 sites** — reachable only by setting `options.threshold` very low (notably `0`) on a long sequence, where the site count tracks sequence length rather than biology. `details` is `{task, record_count, maximum_records, sequence_length, threshold}` — **not** the `{errors: [...]}` array every other `validation_failed` carries, and not something the OpenAPI schema declares (error responses are typed generically, so this page is the only place it is written down). The remedy is a higher `options.threshold`, never a shorter sequence; use `1e-3` rather than `0` if you want everything. See [Limits](./limits.md#response-size-the-splice-site-cap).
- **`422 validation_failed` on `expression`:** four checks are specific to `POST /v1/tasks/expression/predict` — sequence below the 9,198 bp floor, sequence above 500,000 bp, `tss_index` missing on a sequence that is not exactly 9,198 bp, and `tss_index` outside `[4599, len(sequence) - 4599]`. There is no flag, header, or query parameter that relaxes any of them, and the `tss_index` pair reports at `loc` `["body"]` (see the warning above). See [Limits](./limits.md#expression-the-9198-bp-window-and-tss_index).
- **`429 too_many_requests`:** comes from three paths — the per-key concurrency cap, the per-key rate cap, or the edge per-IP cap. All emit `Retry-After`; the application paths also emit `RateLimit-*` headers. On the edge path, `error.request_id` is an edge-assigned identifier (32 hex chars) rather than a UUID, but still correlates with the edge access log. See [Limits](./limits.md).

## Handling errors

Three buckets:

1. **Retryable transient** (`too_many_requests`, `rate_limited`, `model_loading`, `service_unavailable`, `timeout`, `insufficient_memory`): honor `Retry-After`; if absent, exponential backoff capped at ~30 s. For `timeout` and `insufficient_memory`, resubmit async or with a shorter sequence rather than hammering the same request.
2. **Permanent** (other `4xx`): surface `error.message` to the user; do not retry. `422 validation_failed` messages are usually deterministic and safe to echo verbatim.
3. **Server bug** (`5xx` other than `503` and `504`): capture `request_id` and [contact us](../resources/support.md). One retry is fine; tight loops are not.

`504` is the exception to bucket 3: it carries no envelope and means the sync request outran the proxy window, so retry it with `Prefer: respond-async` rather than treating it as a server bug (see the note above).

---

Back to the workflow: [REST API guide](../rest-api.md) · [Tasks](../tasks.md).
