Responses and Errors
Every HTTP endpoint under /api/v1 and /admin wraps its response in one
envelope. Learn it once and every service reads the same way. (Two surfaces
speak their own RFC formats instead: SCIM under /scim/v2 returns RFC 7644
documents, and the OAuth/OIDC endpoints return their standard JSON.)
Success
{
"status": 200,
"msg": "OK",
"data": {
"key": "docs/hello",
"value": { "greeting": "hello world" },
"content_type": "application/json",
"updated_at": "2026-08-28T03:41:02.485104Z"
}
}status mirrors the HTTP status code, msg is human-readable, and data
carries the payload. Creation endpoints return 201 with the created
resource in data.
Errors
Errors keep the same envelope but add a machine-readable error code. On
most errors data is omitted entirely - do not expect data: null. A few
errors attach structured context in data where acting on the failure
needs it: distributed-state's lock_already_held carries the current
owner and a retry hint, and chrono's insufficient_funds carries the
available balance and seconds until affordable. Treat data on an error
as optional extra context, keyed by the error code:
{
"status": 404,
"msg": "Key not found: docs/missing",
"error": "key_not_found"
}{
"status": 401,
"msg": "Missing Authorization header with Bearer token",
"error": "UNAUTHORIZED"
}{
"status": 400,
"msg": "Invalid filter syntax: bogus",
"error": "invalid_request"
}One gap to code defensively around: when a query parameter fails
validation, the response is currently a bare text/plain body rather than
this envelope, and carries no error code:
GET /api/v1/webhooks?page=0
400 Bad Request
content-type: text/plain
page: lower than 1Request body validation on the same endpoints returns the proper envelope. Treat a non-JSON 400 as a malformed-parameter error rather than assuming every response parses as JSON.
Branch on error (stable, machine-readable) and show msg to humans.
Service-specific codes like key_not_found are documented per endpoint in
the API reference; cross-cutting codes
(UNAUTHORIZED, EMAIL_NOT_VERIFIED, invalid_request) come from the
middleware and validation layers.
Lists and pagination
Most list responses put their items under an entity-named array plus a
pagination object with a consistent shape:
{
"status": 200,
"msg": "OK",
"data": {
"guilds": [ ... ],
"pagination": {
"total": 2,
"page": 1,
"page_size": 2,
"has_more": false
}
}
}A few services name the array items instead of the entity (cart, item, kv,
lottery, and narrative), and a small number return a different counter
alongside it rather than pagination - liveness returns total_matched and
no pagination object at all. Read the array name from the endpoint's own
page in the API reference rather than assuming it
from the service name.
See Search queries for the query parameters that drive these endpoints.
The CLI unwraps the envelope
In --output json mode the CLI strips the transport envelope and prints the
bare payload, so scripts address fields directly - jq '.key', never
jq '.data.key':
{
"key": "docs/hello",
"value": { "greeting": "hello world" },
"content_type": "application/json",
"updated_at": "2026-08-28T03:41:02.485104Z"
}On failure the CLI still prints something parseable on stdout and exits non-zero:
{
"error": {
"status": 404,
"body": {
"status": 404,
"msg": "Key not found: docs/missing",
"error": "key_not_found"
}
}
}The full command-line contract - streams, exit codes, destructive-operation confirmation - is in the CLI section.