/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: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.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
Common list parameters
Every list endpoint accepts these; per-endpoint filters are on each operation.Strict parameters
?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 identical404. 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.