Errors, pagination and versioning
Three cross-cutting conventions apply to every endpoint in this API, without exception. Once you know them you can guess a lot about an endpoint you have never called.
Errors#
Every error response — 400, 401, 403, 404, 409, 422, 500, whatever the status — carries the same body:
{
"error": "forbidden",
"message": "you do not have the required role for this action"
}
error is a stable, machine-readable snake_case code, safe to switch
on. message is prose for a human and can change wording between
releases — never parse it. A 401 raised by token verification itself
(missing header, expired token, JWKS unreachable) carries the identical
shape, even though it comes from a different layer than the rest of the
API.
A 500 never leaks what actually went wrong — you always get a fixed
{ "error": "internal_error", "message": "an unexpected error occurred" },
with the real error logged on our side, not yours. There is no
machine-readable request_id in the body today; if you need to report a
failure, include the request's timestamp, the endpoint, and the body you
sent.
Pagination#
Every list endpoint returns the same envelope:
{
"data": [ ... ],
"page": 1,
"per_page": 20,
"total": 137
}
pageis 1-indexed, not 0-indexed.per_pagedefaults to 20 and is capped at 100 — aper_pageabove the cap is not rejected, it is silently clamped.totalis the count of the filtered set, so it moves with whatever query parameters you passed (a status filter, a search term), not the whole collection.
There is no cursor and no next_page_token. Page through a large
collection by incrementing page until data comes back shorter than
per_page.
Versioning#
Every route lives under a /v1 prefix — /livez and /readyz are the
only exceptions, deliberately: they are infrastructure liveness checks,
not part of the data contract, so they stay reachable the same way across
any future version.
The rule going forward is additive-within-a-version, breaking-only-with-a- new-version:
- Adding a field to a response, a new optional request field, a new
endpoint, or a new error
codeis safe and ships inside/v1— you should not need to change anything to keep working. - Removing or renaming a field, changing a field's type, or tightening
what a request accepts is a breaking change. It gets
/v2, added alongside/v1rather than replacing it —/v1keeps working for whoever is still on it.
There is no /v2 yet. Everything in this API today is /v1.
Next: Live updates, or Roles and permissions if you have not read that yet.