Pagination
Traverse large result sets with cursor-based pagination.
List endpoints that can return many results use cursor-based pagination. The cursor is opaque — you don't need to parse it; just pass the next_cursor from one response back as cursor on the next request.
Endpoints that paginate
GET /v1/matching-markets/sports— the list, with itsleague,scheduled_dateandparticipantfilters. Defaultlimit=25, max100.GET /v1/markets— all requests are paginated, including?provider=and?category=filters. Defaultlimit=25, max100.GET /v1/polymarket/wallet/positions— default first-pagelimit=50, max100. Later pages keep the page size carried by the cursor.GET /v1/crypto-prices/binance— per-second Binance prices, newest first. Default and maxlimit=100. With nostart_time,end_timeorcursorit answers the single latest price and no next page. Until 2026-10-09 it answered{prices, pagination_key, total}and tookpagination_key; it now answers this envelope and takescursor, andpagination_keyis a400that namescursor.
GET /v1/matching-markets/sports/lookup does not paginate — the response is
bounded by at most 100 unique IDs across event_id and source_id, and it has
no pagination block, limit or cursor.
Response shape
Every paginated response carries its items in data, in page order, and a pagination block that is always the same:
{
"pagination": {
"limit": 25,
"total": 143,
"has_more": true,
"next_cursor": "eyJzIjoiYWJjMTIzZWYi..."
}
}| Field | Description |
|---|---|
limit | Page size used for this response. Wallet positions cursors preserve the first page's size; other endpoints echo the limit query param (or the default). Crypto prices report 1 when you send no time range. |
total | Total matching items across all pages, or null when the route cannot count them: wallet positions and crypto prices, whose upstreams give no total. 0 only ever means an empty result. Use has_more and next_cursor whatever total says. |
has_more | true when more pages exist beyond this one. |
next_cursor | Pass back via the cursor query param for the next page. Absent when has_more is false. |
Walking every page
The SDKs walk the cursor for you. A call to one of the four paginated operations returns a page (TypeScript and Go) or a pager (Python); iterating it visits every item on every page, sends each next cursor, and repeats the filters that issued it. With cURL you follow next_cursor yourself.
# Page 1
curl -H "Authorization: Bearer $API_KEY" \
"https://api.predictorsdk.com/v1/matching-markets/sports?limit=25"
# Page 2 (use next_cursor from page 1's response)
curl -H "Authorization: Bearer $API_KEY" \
"https://api.predictorsdk.com/v1/matching-markets/sports?limit=25&cursor=eyJzIjoi..."Iterating walks every page, so to read only the first, use page.data (TypeScript), pager.items (Python) or page.Results (Go). A manual cursor is still accepted, to resume a walk you saved; send it with the same filters.
Important rules
Keep request shape constant across pages
The cursor is scoped to the request shape it was issued with. If you change include_settled, provider, category, or any other supported filter mid-traversal, you'll get a 400:
{
"code": "invalid_cursor",
"message": "pagination cursor does not match the current filters; a cursor continues the provider and category values that issued it, on /v1/markets only",
"status_code": 400,
"param": "cursor"
}On matching-markets the message also quotes the cursor and names the
parameters it binds (include_settled, include_submarkets,
player_prop_match, league, scheduled_date and participant).
Drop the cursor and start fresh from page 1 whenever filters change.
Wallet positions cursors preserve the page size
For GET /v1/polymarket/wallet/positions, choose limit on the first page.
When following next_cursor, omit limit or keep it the same: a different
valid limit does not change the cursor's page size, and pagination.limit
reports the size actually used. Keep the same resolved wallet address.
The migration to Polymarket Data API v2 invalidates cursors issued by the
previous positions implementation. Restart from the first page if a saved
positions cursor returns 400; those older cursors cannot be continued.
Crypto price cursors carry their range
For GET /v1/crypto-prices/binance, next_cursor carries the start_time of
the request that issued it, so you can follow it with only currency, limit
and cursor: the next page stays inside the range. Sending a different
start_time with a cursor is a 400, and end_time is ignored when a cursor
is sent. Pages run newest first, each the limit newest seconds at or before
the end of the last one, and walking every page returns every second of the
range exactly once.
curl -H "Authorization: Bearer $API_KEY" \
"https://api.predictorsdk.com/v1/crypto-prices/binance?currency=btcusdt&start_time=1775680500000&end_time=1775680559999&limit=30"
# next page: cursor only, from the previous pagination.next_cursor
curl -H "Authorization: Bearer $API_KEY" \
"https://api.predictorsdk.com/v1/crypto-prices/binance?currency=btcusdt&limit=30&cursor=eyJpZCI6..."Market cursors bind to an immutable snapshot
GET /v1/markets binds the first page to an immutable catalog manifest. A
provider refresh can publish concurrently without changing what that cursor
sees, so continuation returns the remaining records from the same snapshot and
never advances past records that were not delivered.
Replaced snapshots normally remain available for up to 8 hours. Under storage pressure, Valkey may evict a retained snapshot earlier; active snapshots are protected. In either case, the API rejects an unfinished old cursor with:
{
"code": "invalid_cursor",
"message": "pagination cursor is stale; restart from the first page",
"status_code": 400,
"param": "cursor"
}Restart from page 1 whenever a retained cursor goes stale.
snapshot.observed_at tells you how old the page is
Because the endpoint reads a stored snapshot rather than calling the venues,
the rows are as old as the last ingestion crawl — not as old as the request.
Every GET /v1/markets response carries that age:
{
"data": [ ],
"snapshot": { "observed_at": "2026-08-25T13:41:22.108Z" },
"pagination": { "limit": 25, "total": 1393200, "has_more": true, "next_cursor": "..." }
}It sits beside data rather than inside pagination because it describes the
data, not the page. Three things worth knowing:
- It is a floor. Providers are crawled on independent schedules, so the catalog is a merge of snapshots of different ages, and this is the OLDEST of them. Every row is at least this fresh; most are fresher. The value is when that crawl began, not when it finished or was published, so it holds for the first rows the crawl read too: a crawl takes up to about a minute (Kalshi's, the longest, about 45 seconds).
?provider=makes it exact. Under a provider filter the value is that provider's own crawl start, the same waypagination.totalcounts only the filtered set. That is the cheapest way to get a tight bound for the rows you actually care about.- It does not move during a traversal. The cursor is bound to one immutable snapshot, so every page of one traversal reports the identical value even while newer snapshots publish behind you. Start a fresh first page to see a newer one.
null means the bound snapshot carries no read time; treat it as unknown age
rather than as fresh.
Cursors are ephemeral
Treat cursors as short-lived session state — finish traversal promptly, never assume the full 8-hour grace is guaranteed under storage pressure, and don't share them between clients. Start a new first page when you want the latest provider snapshots.
Default behavior
- No
limit→limit=25for matching-markets and markets; wallet positions use50on the first page and the cursor's page size on later pages; crypto prices use100. - No
cursor→ first page. limit > 100→400withcode: "invalid_parameter"andparam: "limit".- Malformed
cursor→400withcode: "invalid_cursor". - Cursor with stale scope →
400withcode: "invalid_cursor"; the message says the cursor does not match the current filters. - Unavailable retained
/v1/marketssnapshot (expired or evicted under pressure) →400withcode: "invalid_cursor"and the messagepagination cursor is stale; restart from the first page.
Game-line rules - ties, overtime and pushes
How each venue settles a tied game, overtime and a line that lands exactly, summarised on every moneyline, spread and total, with the rows behind it on request.
Trading fees
Compute the true cost of a trade on any provider market, and compare venues that quote the same price.