Loading documentation

Access required

Your account doesn't have access to the documentation. Contact your administrator.

Sign out
Skip to main content

Error responses

Status: stable Category: reference Added: 2026-04-20 Last reviewed: 2026-04-20

Summary​

Every non-2xx response uses a uniform envelope:

{
"timestamp": "2026-04-20T17:32:10.812Z",
"error": "ValidationError",
"message": "Field 'depth' must be one of basic, contextual, full",
"request_id": "8f2c3b1d-5e4a-4a9c-9b1e-2a7f1d3e4c5b",
"field": "depth"
}
  • error is a stable machine-readable code. Branch your client on this.
  • message is human-readable. Display it; do not parse it.
  • request_id is the correlation ID. Quote it in support tickets.
  • Extra keys (field, retry_after, etc.) appear on specific error codes and are documented below.

401 responses are the one exception — they use a smaller envelope shape. See authentication.md.

Envelope fields​

FieldTypeAlways presentDescription
timestampstring (ISO 8601 UTC)YesServer time when the response was emitted. Millisecond precision.
errorstringYesStable machine-readable error code. See the taxonomy below.
messagestringYesHuman-readable description. May change wording between releases; do not parse it.
request_idstring (UUID)Almost alwaysCorrelation ID. Present for every /api/* response. Absent only for errors raised before per-request logging begins — in practice, the unauthenticated /health probe is the only such path.
extrasvariesPer error codeAdditional fields documented per error below.

Error taxonomy​

The table lists every value error can take, the HTTP status that accompanies it, and the extras that may appear.

errorStatusExtrasMeaning
ValidationError400field (when known)Request was syntactically malformed or violated a field-level rule.
DeIdentificationViolation400field (always)Request body contained an identifier-shaped value or blocked field name.
Unauthenticated401noneMissing or invalid apikey. Uses the 401 envelope shape described in authentication.md.
PaymentRequired402noneOut of credits, or a per-key / account budget is exhausted. See credits.md.
NotFound404nonePath does not exist or method is wrong.
PayloadTooLarge413noneRequest body exceeded the size limit (default 512KB).
UnsupportedMediaType415noneContent-Type is not application/json.
UnprocessableEntity422variesWell-formed request whose field combination cannot be processed. Rare — see below.
RateLimited429retry_after (seconds)Rate limit exceeded. Retry-After response header is also set.
InternalError500noneUnexpected server-side failure. Quote request_id in a support ticket.
UpstreamError502noneAn upstream dependency returned an unusable response.
ServiceUnavailable503noneA required dependency is temporarily unhealthy. Retry with backoff.
UpstreamTimeout504noneAn upstream dependency exceeded its timeout.

Per-error detail​

ValidationError — 400​

Fires when the request body or parameters fail a shape or value check: malformed JSON, unknown fields (on endpoints that reject unknowns), out-of-range values, missing required fields. Examples:

{
"timestamp": "2026-04-20T17:32:10.812Z",
"error": "ValidationError",
"message": "Field 'depth' must be one of basic, contextual, full",
"request_id": "...",
"field": "depth"
}
{
"timestamp": "2026-04-20T17:32:10.812Z",
"error": "ValidationError",
"message": "Request body is not valid JSON",
"request_id": "..."
}

When the offending field can be identified, field names it using a dotted path (features.accident_month, demographics.age_bracket) or a bracketed index (answers[2].text). field is absent when the error applies to the request as a whole (e.g. malformed JSON at the root).

field is the reliable signal, not the message. A 400 comes in two flavours and they are distinguished by whether field is present:

  • Shape or range failure — a missing field, a wrong type, or a value outside its documented bounds. Carries field, and the message names the constraint: "features.accident_month: Input should be less than or equal to 12". Fix the named field.
  • Vocabulary or business-rule rejection — the shape was fine but a value is not in an accepted set. Carries no field; the message quotes the offending value: "unknown industry_title: 'Underwater Basket Weaving'". Fetch the accepted values from the endpoint's discovery route.

Branch on the presence of field, never on message text.

The rejected value is never echoed on a shape or range failure. The message describes the constraint that was violated, not the input that violated it. Values are quoted only for vocabulary rejections, where the value is a category name you supplied from a published list.

DeIdentificationViolation — 400​

Fires before any business logic. The request body is walked and rejected on the first hit of:

  • a blocked field name: name, first_name, last_name, full_name, ssn, dob, date_of_birth, email, phone, mrn, patient_id, member_id.
  • a value matching an identifier pattern: SSN shape (\d{3}-\d{2}-\d{4}), email shape, phone shape, or a past-dated ISO date (day-resolution date of birth).

Example:

{
"timestamp": "2026-04-20T17:32:10.812Z",
"error": "DeIdentificationViolation",
"message": "demographics.email appears to contain an email address",
"request_id": "...",
"field": "demographics.email"
}

field is always present and names the offending path. The offending value is never echoed. It is not logged and not returned — surfacing the value anywhere would defeat the purpose of the check.

Strip the identifier client-side and retry. For age, send demographics.age_bracket (for example "35-44") instead of a date of birth. See conventions.md for the full de-identification contract.

Unauthenticated — 401​

The apikey header is missing, empty, or not a known key. 401 bodies use a smaller envelope than the standard error shape — see authentication.md.

PaymentRequired — 402​

The call could not be funded. Fires before the endpoint does any work, so an unfunded call never partially executes:

{
"timestamp": "2026-06-22T19:04:20.118Z",
"error": "PaymentRequired",
"message": "Insufficient credits",
"request_id": "..."
}

The message distinguishes a depleted balance (Insufficient credits) from an exhausted per-key allocation (Key credit allocation exhausted) or account-wide cap (Account credit allocation exhausted). A 402 is not retriable — it clears only when you add funds or raise a budget in the portal, not by retrying. See credits.md for the credit model, the X-Credits-* headers, and budgets.

NotFound — 404​

The path and method combination has no handler. This covers unknown paths (/api/does-not-exist) and wrong methods on known paths (GET /api/cdri/compute when only POST is registered).

{
"timestamp": "2026-04-20T17:32:10.812Z",
"error": "NotFound",
"message": "Route not found",
"request_id": "..."
}

PayloadTooLarge — 413​

The request body exceeds the server-side limit. The default is 512KB. No endpoint requires a body close to this limit; hitting 413 almost always indicates an accidental upload of raw data that should have been summarized client-side.

UnsupportedMediaType — 415​

The Content-Type header is not application/json. Always set Content-Type: application/json on any request with a body.

UnprocessableEntity — 422​

Reserved for a request that is well-formed and whose fields are each individually valid, but whose combination cannot be processed.

In practice you should not expect to see it. Anything a caller can fix by changing one field — including a value outside a documented range, a wrong type, or a value not in an accepted vocabulary — is a 400 ValidationError, because a 400 tells you which field to fix. A request naming an instrument that is not in the catalog, for example, returns 400, not 422.

Handle 422 as non-retriable and read the message, but do not build logic that expects it.

RateLimited — 429​

Too many requests for your consumer within the active window. The envelope carries a retry_after extra (seconds to wait) and the response sets a Retry-After header:

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{
"timestamp": "2026-04-20T17:32:10.812Z",
"error": "RateLimited",
"message": "Too many requests",
"request_id": "...",
"retry_after": 12
}

See rate-limits.md for the response headers and backoff guidance.

InternalError — 500​

An unexpected server-side failure. The client did nothing wrong. The response intentionally carries a generic message ("An unexpected error occurred") — details are logged server-side, keyed by request_id, and never leaked to the envelope.

What to do:

  1. Retry once with a short delay (1–2 seconds).
  2. If it persists, open a support ticket quoting the request_id.

UpstreamError — 502​

An upstream dependency returned an unusable response. Your request could not be completed because one of its downstream services is misbehaving. Retry with backoff.

ServiceUnavailable — 503​

A required dependency is temporarily unhealthy. This is also the shape GET /api/health/check uses when a probe fails. For non-health endpoints, a 503 means the request cannot be serviced right now — retry with backoff. If it persists for more than a few minutes, check the status page.

UpstreamTimeout — 504​

An upstream dependency exceeded its timeout. Distinct from UpstreamError in that the upstream may still be processing; the request was abandoned. Retry with backoff; if the same request reliably times out, contact support with a minimal reproducer.

Debugging flow​

  1. Capture request_id. Present on every error response. Quote it in tickets.
  2. Branch on error, not message. Messages are human-readable and may be reworded in a release.
  3. Inspect the extras. field on ValidationError and DeIdentificationViolation pinpoints the path. retry_after on RateLimited is the wait in seconds.
  4. Retry on the retriable statuses. 429, 500, 502, 503, 504 are retriable with exponential backoff. 400, 401, 402, 404, 413, 415, 422 are not — retrying without changing the request (or, for 402, your funding) is a waste.
  5. Check the request again at the boundary. DeIdentificationViolation is always a client-side fix — a field name or value in the body needs to be stripped or restructured.

Change history​

DateChange
2026-04-20Initial publication.
2026-06-22Added 402 PaymentRequired (credit metering).
2026-08-07Documented the two 400 ValidationError flavours and how to tell them apart. Corrected the 422 section: a field-level failure is a 400, and the previously-listed unknown-instrument example returns 400.
2026-10-14The engine assigns every request ID; an X-Request-ID request header is not used.