Skip to content

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": "filter is not a list parameter; use filter[<field>][<operator>]=<value>",
  "error": "BAD_REQUEST"
}

Query-parameter validation returns the same envelope as body validation, with "error": "BAD_REQUEST" and the offending field named in msg:

GET /api/v1/webhooks?page=0
400 Bad Request

{"status":400,"msg":"page must be a whole number of at least 1, got \"0\"","error":"BAD_REQUEST"}

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 pages

List responses put their rows under an entity-named array (or items) plus a page object with the same shape everywhere:

{
  "status": 200,
  "msg": "OK",
  "data": {
    "webhooks": [ ... ],
    "page": {
      "next_cursor": "eyJ2IjoxLCJzIjoiZmI1YzU2ZjM5ZTYyOTVkYyIsInAiOnsibyI6Mn19",
      "has_more": true,
      "total": 42
    }
  }
}

next_cursor is absent on the last page, total appears only when the request asked for it with total=true, and page (the page number) only when the request used page. 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.