Skip to main content

Conventions

Every endpoint on the API follows the same handful of rules, so once you have called one resource you know how to call the rest. This page is those rules. The shape of any particular request or response — its fields, types, enums and status codes — is in the API reference, generated from the code, and is not repeated here.

URLs​

  • Everything lives under /api/v1/ on your deployment's API host. The v1 is the major version; see Versioning.
  • Resources are plural nouns: /api/v1/cohorts, /api/v1/users/{id}.
  • A relation between two resources is a nested sub-resource: GET /api/v1/users/{id}/tags.
  • Almost everything is a create, read, update or delete. The exceptions are actions, for an effect outside the record — sending an email, minting a token: always a POST, named as a camelCase verb phrase, on the resource it acts on — POST /api/v1/invitations/{id}/resend. The reference lists the few there are.

Lists​

Every list endpoint takes the same query parameters and returns the same envelope.

ParameterWhat it does
page, perPageOffset paging. page starts at 1; perPage defaults to 25 and allows up to 1000.
sort, orderSort field and ASC / DESC. Sort fields are allow-listed per resource.
<field>=valueExact-match filter on an allow-listed field. Repeat a parameter to get an IN filter: ?id=a&id=b is how you fetch several records by id.
<field>Gte, <field>LteRange filters, on the resources that offer them.
qFree-text search, on the resources that offer it. Results are ranked by relevance and sort is ignored while q is present.

Unknown parameters are rejected with 400 validation_error rather than ignored. null is the literal null; booleans are lowercase.

GET /api/v1/users?page=1&perPage=25&sort=createdAt&order=DESC&status=active
{
"data": [{ "id": "…", "status": "active" }],
"meta": { "total": 47, "page": 1, "perPage": 25 }
}

meta.total is the exact count for the whole filtered set, not just this page.

Counting. There are no count endpoints. Ask for perPage=0 and read meta.total; the count runs through the same filters as the list, so it can never disagree with it.

GET /api/v1/invitations?status=invited&perPage=0

Chat history is the one exception to offset paging: GET /api/v1/chats/{id}/messages walks forward with limit and cursor, because message threads are read newest-first without end.

Reading one record​

GET /api/v1/{resource}/{id} returns the record as a bare object — no envelope.

Writing​

CallBodyOn success
POST /api/v1/{resource}the full record to create201 with the created record
PATCH /api/v1/{resource}/{id}only the fields to change200 with the updated record
DELETE /api/v1/{resource}/{id}none204, no body
  • Writes always return the whole record, so what you cache is what the server holds.
  • PATCH is JSON Merge Patch: a field you send as null is cleared; a field you leave out is untouched.
  • Some records carry an array of ids for a set-membership relation — tagIds on a user, for instance. Sending the array replaces the whole set atomically; send [] to clear it; leave it out to leave it alone.
  • DELETE deletes. A second DELETE of the same id is 404. Records that are retired rather than removed have a status field, changed with PATCH.
  • There is no PUT: an update is a PATCH of the fields that change.
  • A state change — consent, status, revoking, switching tenancy — is a PATCH on that field, never an action.

Bulk creation, where a resource offers it, is POST /api/v1/{resource}/createMany with an array; the reference marks which resources do.

Errors​

Every non-2xx response has the same body:

{
"error": {
"code": "validation_error",
"message": "Invalid input.",
"field": "email"
}
}

code is stable and machine-readable; message is safe to show to a person; field is present only when the error is about one request field.

CodeStatusMeaning
validation_error400The body, query or path failed validation, or a business rule named a field
unauthorized401No credential, or an invalid one
forbidden403Authenticated, but the role does not allow it
consent_required403The participant has not given consent, so their health data cannot be written
enterprise_required403The tenant's tier does not include this
not_found404No such record
conflict409A uniqueness or integrity rule was hit
update_required426The app version is below the minimum
rate_limited429Slow down; see Retry-After

422 is never used — every validation failure is a 400. A 500 is a bug on our side, never a deliberate response.

Transport​

  • HTTPS. Requests and responses are application/json; charset=utf-8; 204 has no body.
  • No multipart. File content goes through Assets, which upload directly to object storage; the API carries JSON only.
  • gzip and br are available; negotiate with Accept-Encoding.

Versioning​

  • The major version is in the path. A new major (/api/v2/) appears only for a breaking change: removing or renaming a field, tightening a type, removing an endpoint, changing a status code or the meaning of a value.
  • Within a major you should expect, and tolerate, additions: new endpoints, new optional request fields, new response fields, new query parameters, new error codes.
  • When a major is being retired, its routes carry Deprecation and Sunset headers and the window is announced in the changelog.

Idempotency and retries​

  • GET, DELETE, and a PATCH repeated with the same body are idempotent by design.
  • POST to create is not; a duplicate natural key returns 409 conflict.
  • Actions are POST and should be treated as non-idempotent unless the reference says an Idempotency-Key header is accepted for that action.

Rate limits​

Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. A 429 rate_limited carries Retry-After in seconds. Limits are per caller on authenticated routes and per IP on unauthenticated ones; the sign-in, sign-up and sync routes are tighter than the rest.

Caching​

Every authenticated response is sent Cache-Control: no-store, reference data such as tags, groups and cohorts included, because what it holds depends on the tenancy the session is in. Do not cache health data on your side beyond what you need for the screen in front of the person.