One envelope, a stable code to branch on, and a message written to be acted on.
The envelope
Every failure, at every status, looks like this:
{ "error": { "type": "not_found_error", "code": "unknown_kb_article", "message": "This knowledge base has no published article at \"instal-the-cli\". Did you mean \"install-the-cli\"? List the published articles with GET /v1/kb/articles.", "request_id": "6e5c4029-5e1b-47fd-a72e-eb2d5b8f1e2c", "param": "slug", "did_you_mean": "install-the-cli", "doc_url": "https://docs.zevcontent.com/errors/unknown_kb_article" }}
Key
type
The broad class. One of the seven below.
code
The specific reason. Branch on this — it is stable.
message
A sentence saying what to do next. Written for a person reading a log; may be reworded.
request_id
Also on every response as X-Request-Id. Quote it when asking us.
param
Which parameter or field was at fault.
did_you_mean
One closest match, a string — not a list.
available_fields
Every field key a type declares, on a filter or sort naming one it does not have.
available_types
Every content type slug, on an unknown type.
expected / got
The type a value should have had, and the one it had.
doc_url
The page explaining this code.
Types
type
Status
authentication_error
401
We do not know who you are
permission_error
403
We do, and this is not allowed
not_found_error
404
No such thing
conflict_error
409
It exists, or it is in use
rate_limit_error
429
Too many requests
invalid_request_error
4xx
The request was malformed
api_error
5xx
Our fault
Errors are written to teach
The messages are not status codes restated. Naming something that does not
exist answers with the ones that do:
GET /v1/content/homepage
{ "error": { "code": "unknown_content_type", "message": "This project has no content type with slug \"homepage\". Available types: homepage-hero, filter-demo.", "param": "typeSlug", "available_types": ["homepage-hero", "filter-demo"] }}
A 500 carries a generic message on purpose — an internal error’s text can
leak query shapes and table names, so it is logged in full and reported as
internal_error. The request_id is the link between the two: quote it and we
can find the exact request.