{ data, hasMore, nextCursor } and you page by passing nextCursor back unmodified.
Cursors are keyset, not offset. A row inserted while you are paging cannot shift the window and make you skip or repeat an item, which is the failure offset pagination has and this does not.
The envelope
There is no total count. Counting every matching row on every request is a cost you would pay on every page for a number almost no caller uses.
Parameters
pageSize clamping is worth noting: asking for 500 gives you 100 and a 200, not a 422. Read hasMore rather than inferring the end of the list from the number of rows you received.
Paging through a list
Cursors are opaque
A cursor encodes the position of the last row you were served. Treat it as a token: pass it back exactly as received, do not decode it, do not construct one, and do not carry one between routes. Any tampering fails with 400invalid_cursor: bad base64, bad JSON, a missing key, or a timestamp that does not parse.
Which routes paginate
Two lists deliberately do not paginate.
GET /v1/import is a fixed window of the 20 most recent imports, and GET /v1/keys returns the caller’s keys in full.
Recall pagination is opt-in
GET /v1/recall/{recall_type} serves four different shapes from one route, and only two of them are lists.
insightsandtemporalacceptcursorandpageSize.identityandgraphare composite documents. Sending either parameter returns 400pagination_unsupported, with the supported views named insuggestedAction.
Next
Recall
The four recall views and what each one returns.
Errors
The problem envelope and the full code catalogue.