Errors
The per-code list and the typed error.details shapes are in the published OpenAPI schema — browse them in ReDoc or fetch /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:
{
"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.
request_id from the header if the body omits itThe 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 or /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.
details defensively — and never branch on locOne trap, still live — plus one shape note:
- Shape. For
validation_failed,detailsis 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. Onevalidation_failedbreaks that shape: the splice response-size cap sends{task, record_count, maximum_records, sequence_length, threshold}and noerrorskey (see below). Readdetails.get("errors", [])rather than indexing it. loc. Some checks report where you'd expect — a sequence bound at["body","sequence"], an unknown option key at["body","options","<key>"]— but expression'stss_indexchecks 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 onerror.code; usemessagefor 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). Retry withPrefer: respond-async.404 not_foundon an unknown task: an unrecognised{task}segment — on eitherPOST /v1/tasks/{task}/predictorGET /v1/tasks/{task}/models— is a404, not a422. Only the six published tasks exist.404 not_foundon cross-account access: accessing another caller's job returns404, not403, to prevent id enumeration.- The two
413s are different failures.payload_too_largeis the 16 MiB raw-body cap, enforced before parsing — split the request.sync_too_largeis an operation refusing synchronous JSON delivery above its published cap, withdetails = {sequence_length, threshold}— resubmit the identical body withPrefer: respond-async. Two operations have such a cap: the compositefind-genes-and-predict-expressionworkflow above 50,000 bp andPOST /v1/tasks/annotation/predictabove 200,000 bp. The other five predict endpoints never emit it, and neither does an annotation request for?format=bedor?format=gff3, which stays synchronous at any accepted length. See Limits. - A too-long
sequenceis not a413. Above 500,000 bp — or below the task's floor — is422 validation_failedatloc["body","sequence"], with the bound and your submitted length in the message. 415 unsupported_format: an unsupported?format=(orAccept) value is rejected outright, never silently downgraded to JSON; the message names the supported set. Formats differ per task — promoterjson|bed|bedgraph, splicejson|bed|gff3, enhancerjson|bedgraph, chromatinjson|bed, annotationjson|bed|gff3, expression JSON only. Text formats are synchronous-only: pairing one withPrefer: respond-asyncis a400.422 validation_failedon a closedoptions: every task'soptionsobject rejects unknown keys (type: "extra_forbidden",loc["body","options","<key>"]). Notablymodelbelongs at the top level of the body, not insideoptions. See Tasks.422 validation_failedon the splice response-size cap:POST /v1/tasks/splice/predictrefuses a request whose response would carry more than 20,000 sites — reachable only by settingoptions.thresholdvery low (notably0) on a long sequence, where the site count tracks sequence length rather than biology.detailsis{task, record_count, maximum_records, sequence_length, threshold}— not the{errors: [...]}array every othervalidation_failedcarries, 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 higheroptions.threshold, never a shorter sequence; use1e-3rather than0if you want everything. See Limits.422 validation_failedonexpression: four checks are specific toPOST /v1/tasks/expression/predict— sequence below the 9,198 bp floor, sequence above 500,000 bp,tss_indexmissing on a sequence that is not exactly 9,198 bp, andtss_indexoutside[4599, len(sequence) - 4599]. There is no flag, header, or query parameter that relaxes any of them, and thetss_indexpair reports atloc["body"](see the warning above). See Limits.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 emitRetry-After; the application paths also emitRateLimit-*headers. On the edge path,error.request_idis an edge-assigned identifier (32 hex chars) rather than a UUID, but still correlates with the edge access log. See Limits.
Handling errors
Three buckets:
- Retryable transient (
too_many_requests,rate_limited,model_loading,service_unavailable,timeout,insufficient_memory): honorRetry-After; if absent, exponential backoff capped at ~30 s. Fortimeoutandinsufficient_memory, resubmit async or with a shorter sequence rather than hammering the same request. - Permanent (other
4xx): surfaceerror.messageto the user; do not retry.422 validation_failedmessages are usually deterministic and safe to echo verbatim. - Server bug (
5xxother than503and504): capturerequest_idand contact us. 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 · Tasks.