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. Thev1is 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.
| Parameter | What it does |
|---|---|
page, perPage | Offset paging. page starts at 1; perPage defaults to 25 and allows up to 1000. |
sort, order | Sort field and ASC / DESC. Sort fields are allow-listed per resource. |
<field>=value | Exact-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>Lte | Range filters, on the resources that offer them. |
q | Free-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
| Call | Body | On success |
|---|---|---|
POST /api/v1/{resource} | the full record to create | 201 with the created record |
PATCH /api/v1/{resource}/{id} | only the fields to change | 200 with the updated record |
DELETE /api/v1/{resource}/{id} | none | 204, no body |
- Writes always return the whole record, so what you cache is what the server holds.
PATCHis JSON Merge Patch: a field you send asnullis cleared; a field you leave out is untouched.- Some records carry an array of ids for a set-membership relation —
tagIdson a user, for instance. Sending the array replaces the whole set atomically; send[]to clear it; leave it out to leave it alone. DELETEdeletes. A secondDELETEof the same id is404. Records that are retired rather than removed have astatusfield, changed withPATCH.- There is no
PUT: an update is aPATCHof the fields that change. - A state change — consent, status, revoking, switching tenancy — is a
PATCHon 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.
| Code | Status | Meaning |
|---|---|---|
validation_error | 400 | The body, query or path failed validation, or a business rule named a field |
unauthorized | 401 | No credential, or an invalid one |
forbidden | 403 | Authenticated, but the role does not allow it |
consent_required | 403 | The participant has not given consent, so their health data cannot be written |
enterprise_required | 403 | The tenant's tier does not include this |
not_found | 404 | No such record |
conflict | 409 | A uniqueness or integrity rule was hit |
update_required | 426 | The app version is below the minimum |
rate_limited | 429 | Slow 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;204has no body. - No multipart. File content goes through Assets, which upload directly to object storage; the API carries JSON only.
gzipandbrare available; negotiate withAccept-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
DeprecationandSunsetheaders and the window is announced in the changelog.
Idempotency and retries
GET,DELETE, and aPATCHrepeated with the same body are idempotent by design.POSTto create is not; a duplicate natural key returns409 conflict.- Actions are
POSTand should be treated as non-idempotent unless the reference says anIdempotency-Keyheader 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.