Error responses
Status:
stableCategory:referenceAdded:2026-04-20Last 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"
}
erroris a stable machine-readable code. Branch your client on this.messageis human-readable. Display it; do not parse it.request_idis 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
| Field | Type | Always present | Description |
|---|---|---|---|
timestamp | string (ISO 8601 UTC) | Yes | Server time when the response was emitted. Millisecond precision. |
error | string | Yes | Stable machine-readable error code. See the taxonomy below. |
message | string | Yes | Human-readable description. May change wording between releases; do not parse it. |
request_id | string (UUID or client-supplied) | Almost always | Correlation 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. |
| extras | varies | Per error code | Additional 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.
error | Status | Extras | Meaning |
|---|---|---|---|
ValidationError | 400 | field (when known) | Request was syntactically malformed or violated a field-level rule. |
DeIdentificationViolation | 400 | field (always) | Request body contained an identifier-shaped value or blocked field name. |
Unauthenticated | 401 | none | Missing or invalid apikey. Uses the 401 envelope shape described in authentication.md. |
PaymentRequired | 402 | none | Out of credits, or a per-key / account budget is exhausted. See credits.md. |
NotFound | 404 | none | Path does not exist or method is wrong. |
PayloadTooLarge | 413 | none | Request body exceeded the size limit (default 512KB). |
UnsupportedMediaType | 415 | none | Content-Type is not application/json. |
UnprocessableEntity | 422 | varies | Body parsed but a business rule rejected it. |
RateLimited | 429 | retry_after (seconds) | Rate limit exceeded. Retry-After response header is also set. |
InternalError | 500 | none | Unexpected server-side failure. Quote request_id in a support ticket. |
UpstreamError | 502 | none | An upstream dependency returned an unusable response. |
ServiceUnavailable | 503 | none | A required dependency is temporarily unhealthy. Retry with backoff. |
UpstreamTimeout | 504 | none | An 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 (demographics.age_bracket) or a bracketed index
(responses.item_3). field is absent when the error applies to the
request as a whole (e.g. malformed JSON at the root).
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
The body parsed as valid JSON but a higher-level rule rejected it. Used for semantically-invalid input that passed the first-pass shape check. A concrete example:
POST /api/cdri/computebody that references an instrument name not registered in the catalog. The body was valid JSON and the top-level shape was correct, but the named instrument does not exist, so the request cannot be processed as written.
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:
- Retry once with a short delay (1–2 seconds).
- 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
- Capture
request_id. Present on every error response. Quote it in tickets. - Branch on
error, notmessage. Messages are human-readable and may be reworded in a release. - Inspect the extras.
fieldonValidationErrorandDeIdentificationViolationpinpoints the path.retry_afteronRateLimitedis the wait in seconds. - Retry on the retriable statuses.
429,500,502,503,504are retriable with exponential backoff.400,401,402,404,413,415,422are not — retrying without changing the request (or, for402, your funding) is a waste. - Check the request again at the boundary.
DeIdentificationViolationis always a client-side fix — a field name or value in the body needs to be stripped or restructured.
Related
- authentication.md — the
apikeyflow and the401body shape. - rate-limits.md —
429headers and backoff. - credits.md — the credit model behind
402, theX-Credits-*headers, and budgets. - conventions.md — success envelope, timestamps, de-identification rules.
Change history
| Date | Change |
|---|---|
| 2026-04-20 | Initial publication. |
| 2026-06-22 | Added 402 PaymentRequired (credit metering). |