Guides

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 1

Request 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.

On this page