Some refusals carry an extra key
Besidecode and message, a refusal may add one of three optional keys. They exist to make a
failure mechanical to act on rather than to read:
field appears only when ONE key is at fault. A request that omits several names them all in
message and sends no field, because there is no single input for a client to point at.
Treat all three as optional. They are added where a route knows the answer, so a body carrying
only
code and message is not a malformed one. Branch on code first and read these for
detail; field in particular lets a client point a validation message at the right input
without parsing prose.This shape covers everything
Including the responses that usually escape an API’s error format:404for a path that is not mounted405for a method the route does not accept401from the authentication layer, before any handler runs429from the rate limiter, before authentication runs500when something failed on our side
/api/v1 that answers a refusal in HTML, in plain text, or in a
different JSON shape. You never need a fallback parser for a body you could not decode.
Statuses
Reading a 401: code says unauthorized, reason says what happened
In HTTP terms 401 means we do not know who you are and 403 means we do and you may
not. The code on our 401 is unauthorized, which is the 403’s word. That is a
compatibility decision, not an accident: renaming it would break every client already
branching on it, so the accurate value is carried additively in reason.
The distinction is worth branching on:
credential_missing is a configuration mistake
you can fix in your client, while credential_invalid usually means a key was rotated or
revoked and needs replacing.
The message stays deliberately vague about which. It never says “this key was revoked”
or “this key expired”, because that would let a probe learn which invented keys are real.
A client reading only
code keeps working. reason is additive, so branching on it is
optional. On a 403 the code is insufficient_scope, which already says what happened
and needs no separate reason.Every response carries a correlation id
401 and 429 that
are answered before any handler runs. It is the same value that appears in our logs for
that request.
Log the
x-request-id alongside your own errors. When you report a problem, quoting
it is the difference between support finding the exact request in seconds and asking
you a series of narrowing questions.Handling errors
Client mistakes are never 5xx
A malformed body, a bad cursor, an unknown field or an invalid value answers4xx. It
does not answer 500.
This matters for your retry logic. A 5xx from this API means something failed on our
side and retrying is reasonable. If client faults could also surface as 500, you could
not distinguish “retry this” from “this will fail identically forever”, and a retry loop
on a permanently malformed request is just load with no chance of success.
Unknown codes
New error codes can be added as the API grows. Handle the codes you care about explicitly and fall through to a default that logscode, message and x-request-id.
Do not treat an unrecognised code as success.