Skip to content

Errors

Errors use the envelope’s error variant:

{
"error": {
"code": "bad_request",
"message": "...",
"details": { ... }
},
"meta": {
"api_version": "v1",
"request_id": "req_01H..."
}
}

There is no data field on error responses. HTTP status follows RFC 9110.

Status conventions

StatusWhen
400 Bad RequestMalformed request, missing required field, bound exceeds hard maximum, missing confirm: true on DELETE.
401 UnauthorizedMissing or invalid bearer token on a token-required endpoint.
403 ForbiddenToken supplied via query parameter (always rejected); internal-metrics call without internal bearer.
404 Not FoundResource id (course, credential, source reference) does not exist in current index.
409 Conflictexpected_state_version mismatch on a state mutation.
415 Unsupported Media TypeContent-Type is not application/json on a POST/PATCH/PUT/DELETE.
422 Unprocessable EntityBody shape valid but semantically invalid (catalog version mismatch, unknown course id, etc.).
429 Too Many RequestsPer-IP rate limit exceeded. Retry-After header set.
500 Internal Server ErrorUnhandled server fault. Includes request_id for debugging; never leaks stack traces.
503 Service UnavailableServer is restarting (index publication swap) or shutting down. Retry-After set.

Canonical error codes

These are stable enough that frontends can switch on them.

General

  • bad_request — generic 400.
  • unsupported_media_type — wrong Content-Type.
  • payload_too_large — request body exceeds limit.
  • internal_error — unhandled fault.
  • service_unavailable — restart in progress.

Authentication

  • missing_token — Authorization header absent on a token-required endpoint.
  • unauthorized — bearer doesn’t match any stored verifier.
  • token_in_query — token supplied via ?token= (always rejected).

Catalog

  • course_not_found, credential_not_found, source_reference_not_found.

Query and Advisory

  • unknown_target — the requested target is not in the current index.
  • evaluation_problem_invalidwhat-if body’s hypothetical changes are malformed.
  • engine_native_status_leaked — internal sanity check; should never appear externally. Filing this as a bug helps.

Graph views

  • bound_exceeds_hard_max — request bound is over the hard maximum for that endpoint.
  • unknown_view/graph/views/<name> is not one of the seven supported views.

State

  • missing_confirmDELETE /state/current without {"confirm": true}.
  • state_version_conflictexpected_state_version mismatch.
  • catalog_version_mismatch — body’s catalog_version_id doesn’t match persisted state.

Rate limiting

  • rate_limit_exceeded — 429. Response includes Retry-After and hints which limiter fired.

What error responses never contain

  • The raw bearer token (even if the request supplied a malformed one).
  • Stack traces or internal exception messages.
  • File paths from the server filesystem.
  • Engine-native debug logs.

Per ADR 0018, engine-native statuses (UNKNOWN, UNSAT, infeasible, timeout) never appear as HTTP errors — they show up as academic unknown or conflict statuses inside successful responses.

Distinguishing transport failures from academic unknowns

This split matters a lot in client code:

If the client sees…It’s…
HTTP 4xx/5xx with error.codeA transport / contract error. Bug or load.
HTTP 200 with unknowns in the envelopeAcademic uncertainty. A feature, not a bug.
HTTP 200 with warnings[].code == "graph_view_truncated"A bounded response. Caller asked for too much; server gave back a partial answer.

The frontend’s EnvelopeIssues surface renders the first kind; the status badges render the second; the inspector’s truncation banner renders the third.

Want more?