Pagination
Cursors, their lifetime, and the one rule that invalidates a cursor mid-walk.
List endpoints take limit and cursor.
limit is an integer from 1 to 100 and defaults to 20. Anything else — a float, a word, zero, 101 — is 400 invalid_request with the message limit must be an integer between 1 and 100.
{
"data": [],
"has_more": true,
"next_cursor": "cur_9f2e"
}Pass next_cursor back as cursor until has_more is false. When has_more is false, next_cursor is absent — walk to that condition rather than to an empty data.
Cursors are signed and expire
A cursor is not an offset. It is a signed token carrying the position, the list it belongs to, and the time it was issued; it is verified with an HMAC on the way back in. A cursor that has been edited, truncated, or invented returns 400 cursor expired or malformed — the same message as an expired one, deliberately, because distinguishing the two tells a prober something.
Cursors expire 24 hours after they are issued. A pagination walk is a session, not a bookmark: to hold a position for longer, keep the last object id and filter on it instead.
A cursor is bound to its filters
This is the rule that catches people. The filter set is part of what the cursor signs, so changing any filter between pages invalidates the cursor:
# page 1
GET /v1/decisions?verdict=deny_limit&limit=50
# page 2 — same cursor, one extra filter → 400
GET /v1/decisions?verdict=deny_limit&agent=agt_procurement_01&cursor=cur_9f2eSwitching from an organization key to an agent key mid-walk does the same thing, because the visible set changes with the key.
The reason is that a cursor points into a specific ordered result set. Carrying it into a different one would silently skip or repeat rows, and a paging bug that loses records in a payments log is worse than an error. Change a filter, start the walk again.

