Skip to main content
Every non-2xx response body is one shape:
Guaranteed members: type, code, message, doc_url, request_id. Everything else is code-specific context, documented on each code’s page. message is prose for humans and may change; program against code, which is stable API surface. doc_url is always https://docs.fiveninelabs.com/errors/<code> — the code’s own page, one URL shape for every code. Errors scoped to a run will additionally carry console_url — a deep link to that run’s dashboard detail view, the same evidence we debug with. (Not yet emitted — don’t depend on it until the changelog announces it.) request_id also arrives as the fn-request-id header on every response (success and error) and appears in the dashboard’s API log — include it in support requests.

Types → status codes

Two request-shape divergences, stated honestly until fixed:
  • Malformed JSON is currently misreported. A body that fails to parse (or exceeds the 1 MB body cap) is treated as empty, so you get 400 invalid_param naming the first missing required field (usually query) — not a parse error. “query is required” while you’re sending a query means: check your JSON and your body size. A dedicated parse-failure code is planned. Details on invalid_param.
  • A wrong HTTP method on a known path currently returns 404 not_found, not 405. If a route you’re sure exists 404s, check the verb before the path.

The code catalog

The catalog is an open enum: new codes may appear (the additive rule). Handle unknown codes by type.

Declining to spend — a $0 abstained run, not an HTTP error

When the parse decides spending on a query could not honestly deliver, production behaves like this:
  1. Pre-spend signal: POST /v1/sessions/estimate returns would_run: false with the parsed intent, what’s missing, and the reason — free, before any commitment. This is the gate to build on.
  2. If you create anyway: POST /v1/sessions returns 201 with a running session, and within seconds the run ends with an abstain event (reason attached) → status abstained, $0 settled, reservation released.
An earlier draft of this contract described a synchronous 422 declined_to_spend; the live API does not emit it, and clients must not gate on it — see declined_to_spend. Rehearse the real ending with the FN_TEST_DECLINE sentinel.

Failures that are NOT errors

A run that starts and then ends badly is a status, not an HTTP error: abstained, error, and stopped arrive as terminal events on the stream and as status on the session object, with the ledger settled only for signed receipts and the remaining reservation released. HTTP errors are reserved for “this request did not start a run.” Rehearse the run-level endings with the test-mode sentinels (FN_TEST_ABSTAIN, FN_TEST_ERROR, FN_TEST_SPEND_STOP).

Stream-terminal codes

When a run that started dies, its stream ends with a typed terminal error event — { "phase": "error", "message": "…", "code": "…" }. message is prose and may change; code is the stable surface. These are stream codes, a vocabulary distinct from the HTTP catalog above — they never appear as HTTP request errors. Same additive open-enum rule as the HTTP catalog: new codes may appear; handle an unknown code as internal-shaped.