Skip to main content
Every response is JSON in a fixed envelope. Successes carry data and meta; failures carry error. Both always include a request id.
data is a single object for a detail endpoint and an array for a listing. A 204 has no body at all — DELETE /v1/assessments/{id}, and the invitation resend and revoke actions.

Request ids

Every response carries one, in meta.request_id (or error.request_id) and in the X-Request-Id header.
Log it on every call, especially failures. With a request id we can find your exact request; without one, a 500 from three days ago is unfindable.

Offset pagination

Used where the collection is bounded: challenges, assessments, invitations.
meta returns page, page_size, total and has_more. Page size defaults to 20 and maxes at 100 — asking for more is a 400, not a silent clamp.

Cursor pagination

Used where the collection grows without bound: submissions and the event log. An offset walk would both drift as new rows arrive and get slower the deeper you go. Follow meta.next_cursor until it comes back null:
Treat the cursor as opaque. Its encoding is ours to change, and a hand-built one is a 400. Pass back exactly what next_cursor gave you.
The event log paginates on a sequence number instead: pass the last seq you saw as since_seq. meta.next_cursor is that number.

Filtering

Listings share a filter vocabulary where it applies:
There is no sort parameter. Listings return newest first; cursor ordering depends on that, so exposing a sort needs its own design. Filter down and sort client-side.

Dates and numbers

  • Timestamps are ISO-8601 UTC strings: 2026-01-15T12:00:00.000Z.
  • Money is a number in dollars, not a string and not cents: 1.25.
  • A field that isn’t available is null rather than absent, except where noted — an ungraded report omits verdict_label, summary and scored_as_role entirely.