Skip to main content
Every /v1 endpoint shares one envelope, one pagination scheme and one error shape. Read this once and the rest of the reference is per-endpoint specifics.
Two endpoints predate this contract and keep their original flat shapes for compatibility with live callers: POST /v1/users and POST /v1/answer. They are marked as such in the reference.

The envelope

A single object:
A list:
has_more and next_cursor are always present, even when false and null, so a paging loop is written once and never branches. Every response also carries X-Request-Id (matching request_id) and Cache-Control: no-store — this is workspace data behind a credential, and a shared cache in front of it is how one tenant gets served another’s response.
Quote a request_id to support for any outcome, success or failure. It is the only thing that ties your report to the server-side log line — error messages deliberately never echo the offending row.

Pagination

Keyset cursors only. There is no offset paging.
Do not parse or construct a cursor. It is opaque and we stay free to change it. It is also not a security boundary — it carries only a sort value and a row id, both of which you already have. The tenant boundary is your credential.
Why keyset and not offset: the customer this API is for is exporting six months of sessions from a table that is still being written to. With OFFSET, every row inserted ahead of the window shifts everything down — rows are silently skipped, others arrive twice, and the export looks complete. A keyset cursor pins the read to “strictly after this row”, so the walk is stable no matter what lands mid-export. has_more is derived by fetching one row beyond the page, not by a count. A count taken alongside the page is stale the moment it is taken, so it would be a number presented as authoritative that we could not stand behind.

Reusing a cursor under a different sort is rejected

The cursor simply does not describe a position in the new ordering, so returning data would be quietly wrong rather than an error.

Common list parameters

Every list endpoint accepts these; per-endpoint filters are on each operation.

Strict parameters

An unrecognised query parameter is a 400, not a shrug.The failure this prevents is the expensive one: you write ?since=2026-01-01, get a 200 and a full first page, and conclude your filter worked — then reconcile against a number that was never filtered. Silently ignoring a parameter is indistinguishable from honouring it.
The same applies to duplicates — ?status=new&status=won has no obvious meaning, so picking one silently discards the other. On writes, unknown body keys are rejected, not dropped; enums are enums, not free strings; and money is in minor units (cents), never decimals.

Errors

One shape, everywhere:
  • type — the coarse family. Branch on this; it is stable.
  • code — the specific machine label.
  • message — what was expected, in words.
  • param — the offending query parameter or dotted body path, when there is one.

404 is deliberately ambiguous

A record belonging to another workspace and a record that does not exist return the identical 404. A distinguishable 403 would turn the endpoint into an existence oracle for other tenants’ ids. The same collapse applies to MCP: a token for a different workspace than the URL gets the same 404 as an unknown workspace.

401 vs 403

  • 401 — the credential is wrong: missing, invalid, revoked, or the wrong kind. The message names what arrived and what was wanted.
  • 403 — the credential is fine; the role does not reach. See Roles.

Rate limits

Every /v1 endpoint shares one bucket per credential, not a bucket per endpoint. Per-endpoint buckets would let a caller multiply their real ceiling by the number of endpoints we happen to have shipped, so the published limit would drift upwards every time we added a route. Ceilings follow your plan — see Rate limits. A rate-limited response carries Retry-After in seconds. A call refused for role reasons is rejected before the limiter charges your workspace: a call that was never allowed should not consume your budget.