Pagination
This page describes how to walk the pages of endpoints that return lists. In Open Play there is no single pagination format: most endpoints use an opaque cursor, while a few use a different method. The table below shows which applies on which endpoint and the page size limits.
Cursor (keyset) pagination
Most core list endpoints are cursor-based:
- Request:
?limit=<n>&cursor=<cursor>. On the first page you do not providecursor. - Response:
{"items":[…],"next_cursor":"…"}. Ifnext_cursorisnull, you are on the last page. - Next page: give the
next_cursorfrom the previous response exactly as thecursor. - The cursor is opaque: do not parse its contents, do not modify it, and do not assume you can store it and use it after a long time. A broken cursor gets
400. - The ordering is fixed and
OFFSETis not used; if new records are added between pages, no skipping or repetition occurs (records are ordered by a stable key). - The page size is given with
limit; a value outside the bounds is clamped on most endpoints (it does not give an error). The exceptions are noted in the table.
GET /v1/characters?limit=2 HTTP/1.1
Authorization: Bearer crx_live_<prefix>_<secret>
{"items":[{"id":"…","definition":"hero","version":3}],"next_cursor":"<opaque-cursor>"}
GET /v1/characters?limit=2&cursor=<opaque-cursor> HTTP/1.1
Authorization: Bearer crx_live_<prefix>_<secret>
Formats by endpoint
| Endpoint | Format | Default | Limit |
|---|---|---|---|
GET /v1/characters | Cursor (definition filter) | 50 | clamped between 1 and 200 |
GET /v1/characters/{id}/events, …/timeline | Cursor (ascending seq) | 50 | clamped between 1 and 200 |
GET /v1/rules, GET /v1/webhooks | Cursor | 50 | clamped between 1 and 200 |
GET /v1/ledger | Cursor (wallet_id required) | 50 | clamped between 1 and 200 |
GET /v1/items, GET /v1/player/items | Cursor (with owner) | 50 | clamped between 1 and 200 |
GET /v1/assets | Cursor | 50 (0 or absent = default) | at most 200 |
GET /v1/catalog/items, GET /v1/catalog/characters | Cursor | 50 | clamped between 1 and 100 |
GET /v1/notifications | Cursor (status, limit, cursor) | 50 | clamped between 1 and 100 |
GET /v1/leaderboards/{key}/entries | Cursor | 20 (0 or absent = default) | at most 100; more gets 400 (not clamped) |
GET /v1/leaderboards/{key}/around-me | Around the player (player_id, radius) | — | No pages; the surrounding windows are bounded |
GET /v1/player/leaderboards/{key} | Single page (limit) | 20 | 1 to 50; outside that 400 |
GET /v1/chat/channels/{channel_id}/messages | Time-based (before_ms + limit) | 50 | 1 to 100 (the service validates) |
GET /v1/quests | offset + limit (search filter) | 20 (0 or absent = default) | limit at most 100, offset at most 100000; more gets 400 |
GET /v1/auth/admin/list-players | Cursor (UUID) | the endpoint’s default | Endpoint-specific |
GET /v1/clans, GET /v1/parties | No pagination; the whole result (a player_id filter for parties) | — | — |
GET /v1/wallets, GET /v1/inventories/{owner} | No pagination (the whole list by owner) | — | — |
Plugin queries (/v1/plugins/…/queries/…) | Plugin-specific; defined in the input schema | — | The plugin’s schema document |
Note
In the response, the cursor field comes as
next_cursor(snake_case) in core REST. In chat messages, to get a new page you give the oldest message’ssent_at_msvalue asbefore_ms. The pagination of the panel APIs (offset/grid) is for the panel interface’s internal use and is outside the scope of this documentation.
Pagination in the SDK
The TypeScript SDK offers iterators for cursor lists: iterateListCharacterEvents, iterateGetCharacterTimeline, iterateListRules, iterateListWebhooks. These iterators follow next_cursor for you. Important rules:
- The cursor is opaque; the iterator gives it back without converting it.
- An empty
next_cursorends the loop; a cursor loop (the same cursor coming back) or exceedingmaxPagesis not a silent cut-off but an error of typepagination. limitisu32on the wire; the server clamps it to the range 1 to 200.
Patterns
Walking everything (server-to-server work): build a loop until the cursor comes back empty; put a short upper bound on pages (for example at most 1000 pages).
cursor = none
repeat:
page = GET /v1/rules?limit=200[&cursor=cursor]
process page.items
cursor = page.next_cursor
stop if cursor is empty
An in-game “load more” list: use a small limit (20 to 50), keep next_cursor in memory, and request the next page as the user scrolls. Do not store the cursor persistently.
Consistency: cursor pages walk live data; new records added while walking may not be visible if they fall behind the cursor. For reports that need a snapshot, record the query time (for example as of) on your side.
Related: endpoints, rate limits.