Every refusal on this API uses the same body, at every status:
One shape means you write one parser and one branch, rather than one per route.
Branch on code, never on message.code is part of this contract and is safe to switch on. message is English prose for a human reading a log. Its wording can change at any time and is not part of the contract. Matching on message text produces an integration that breaks on a copy edit.

This shape covers everything

Including the responses that usually escape an API’s error format:
  • 404 for a path that is not mounted
  • 405 for a method the route does not accept
  • 401 from the authentication layer, before any handler runs
  • 429 from the rate limiter, before authentication runs
  • 500 when something failed on our side
There is no route on /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

Every response carries a correlation id

This is on every response, success or failure, including the 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 answers 4xx. 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 logs code, message and x-request-id. Do not treat an unrecognised code as success.