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. Twovalidation_faileds break that shape: the splice response-size cap sends{task, record_count, maximum_records, sequence_length, threshold}, and the expression refusal for too little real sequence sends{invalid_characters: [], invalid_positions: [], valid_characters: [...]}with empty lists; neither has anerrorskey (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: five checks are specific toPOST /v1/tasks/expression/predict, the same for both expression models — sequence below 9,198 bp, sequence above 500,000 bp,tss_indexmissing on a sequence that is not exactly 9,198 bp,tss_indexoutside[4599, len(sequence) - 4599](the TSS must be at least 4,599 bp from each end), and too little real sequence on one side of the TSS. The last one fires on windows dominated byNor low-complexity repeats: as a rule of thumbg0-expressionneeds roughly 2 kb of real (non-N) sequence upstream of the TSS and 2.6 kb downstream,g0-expression-8192roughly 3.3 kb upstream and 2.6 kb downstream; themessagesays which side fell short. The fix is real sequence in place of theNor repeat run; forg0-expression-8192, more real flank also helps. 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.