Pagination
List endpoints use cursor pagination, updates use ETag-based optimistic concurrency, and creates accept idempotency keys — the three mechanics every integration needs to get right.
Cursor pagination
List endpoints never return unbounded arrays. Each list response is a page; the request takes a cursor and a page size, and the envelope tells you whether more pages exist:
| Parameter | Meaning |
|---|---|
cursor | Opaque continuation token from the previous page (omit it for the first page). |
limit | Page size, bounded by a server-side maximum. |
The response envelope for every list endpoint:
{
"data": [
{ "id": "STUDY_ID_1", "name": "Study A" },
{ "id": "STUDY_ID_2", "name": "Study B" }
],
"nextCursor": "NEXT_CURSOR",
"hasMore": true
}Treat cursors as opaque: never parse them or construct them yourself. To iterate a full collection, keep calling with the latest nextCursor while hasMore is true, then stop. Filters and the cursor are combined in the same request; changing filters mid-iteration starts a new iteration.
ETag / If-Match on detail updates
Detail resources return an ETag response header identifying the current revision. Send it back as If-Match when updating, so concurrent editors cannot silently overwrite each other:
GET /api/v1/studies/STUDY_ID
← ETag: "v3"
PATCH /api/v1/studies/STUDY_ID
If-Match: "v3"
→ succeeds only if the resource is still at revision v3If the resource changed underneath you, the API returns 409 with the conflict_etag error code — re-read the resource, reapply your change to the new revision, and retry with the new ETag. This is standard optimistic concurrency control; see errors for the response format.
Idempotency keys on POST
Sending the same create request twice — a network retry, a crashed worker retrying — must not create two of something. POST endpoints accept an Idempotency-Key header. If the same key arrives with the same body, the API returns the original result instead of creating a duplicate; if the same key arrives with a different body, you get 409 with idempotency_replayed.
- Generate a unique key per logical operation (a UUID is fine) and keep it stable across retries of that operation.
- Keys are scoped to the API client, so two clients can never collide.
- Treat the idempotency mechanism as insurance, not as a queue — the request still has to succeed once.