Pagination and the collection envelope
Every collection answers the same way, so one parser serves all of them.
Ask for the envelope
Send the version you were built against:
GET /api/orgs/{orgId}/spaces/{spaceId}/items?limit=100
Forumulate-Version: 2026-09-01{
"items": [ { "id": "item_912", "updatedAt": "2026-08-18T04:12:09Z" } ],
"page": { "limit": 100, "has_more": true, "next_cursor": "eyJ2IjoxLCJ1Ijoi…", "total": 4812 },
"sync": { "checkpoint": "2026-08-18T04:12:09Z" }
}Pass next_cursor back as cursor to continue. Sending a cursor is itself enough to get the envelope, since there is no other way to hand one back.
Why cursors, not offsets
An offset skips and duplicates rows when anything is written mid-walk — for a documentation sync that means silently losing pages. A cursor names the exact row it stopped after. The sort is (updatedAt, id): total, so two pieces saved in the same millisecond cannot straddle a page boundary with one of them never appearing.
Cursors are opaque. Store them, do not parse them; one we did not issue is refused with a 422 that says so.
Limits
limit defaults to 50 and is capped at 200 — or 50 when you include content, because every row then carries a whole piece. The cap that applied is always in page.limit, so you never have to infer it.
Callers who say nothing
A request with no Forumulate-Version, no cursor and no sync parameters gets exactly the array this endpoint has always returned, in the same order. Nothing breaks on a day we choose.