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_paramnaming the first missing required field (usuallyquery) — 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, not405. 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:
- Pre-spend signal:
POST /v1/sessions/estimatereturnswould_run: falsewith the parsed intent, what’s missing, and the reason — free, before any commitment. This is the gate to build on. - If you create anyway:
POST /v1/sessionsreturns201with a running session, and within seconds the run ends with anabstainevent (reasonattached) → statusabstained, $0 settled, reservation released.
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 terminalerror 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.