Pagination
Page through contract results that don't fit in a single response, and read the response envelope correctly.
The REST query API returns results in pages. Every successful response uses the same envelope.
The response envelope
{
"data": {
"pagination": {
"items": [
{ "store_name": "Downtown", "revenue_usd": 84210.50 },
{ "store_name": "Midtown", "revenue_usd": 91040.25 }
],
"total_items": 248,
"page": 1,
"limit": 100,
"total_pages": 3,
"has_next_page": true,
"has_prev_page": false
},
"execution_time_ms": 53
},
"message": null
}The rows live at data.pagination.items, not at the top of data.
Reading data.items or data.rows will return undefined; clients that
assume rows live higher up than they do are a common cause of "the query
returned nothing" bugs that aren't actually empty.
| Field | Meaning |
|---|---|
data.pagination.items | The rows for the current page. |
data.pagination.total_items | Total rows across all pages. |
data.pagination.page | Current page number, starting at 1. |
data.pagination.limit | Page size actually applied (may differ from requested). |
data.pagination.total_pages | Total number of pages. |
data.pagination.has_next_page | Whether a page after this one exists. |
data.pagination.has_prev_page | Whether a page before this one exists. |
data.execution_time_ms | Query execution time in milliseconds. |
message | Informational message, usually null. |
The 10,000-row maximum
Every query response is capped at 10,000 rows per page. If you ask for
more, the request succeeds but the response silently clamps to 10,000;
pagination.limit will reflect the effective value, not what you
requested.
// Request
{ "query": { "limit": 1000000 } }
// Response (success, but clamped)
{ "data": { "pagination": { "limit": 10000, ... } } }This is the single most-important behavior to know if you're processing a
large result set. Always read pagination.limit from the response and
trust it over what you sent, and always check has_next_page to
decide whether to keep paging. Code that assumes "I asked for a million
rows so I have them all" will see exactly 10,000 rows and miss the rest.
Requesting a page
Control paging with limit and offset in the query:
limit: requested page size (clamped to 10,000).offset: how many rows to skip.
For page n, use offset = (n - 1) * limit. Total page count is
total_pages = ceil(total_items / limit).
{
"contract_name": "store_performance",
"query": {
"measures": ["revenue_usd"],
"dimensions": [{ "name": "store_name" }],
"sorts": [{ "field": "revenue_usd", "direction": "desc" }],
"limit": 100,
"offset": 0
}
}A two-call example
You want all 248 stores' revenue, sorted desc, in pages of 100.
Call 1:
{
"contract_name": "store_performance",
"query": {
"measures": ["revenue_usd"],
"dimensions": [{ "name": "store_name" }],
"sorts": [{ "field": "revenue_usd", "direction": "desc" }],
"limit": 100,
"offset": 0
}
}Response:
{
"data": {
"pagination": {
"items": [ /* 100 rows */ ],
"total_items": 248,
"page": 1,
"limit": 100,
"total_pages": 3,
"has_next_page": true,
"has_prev_page": false
},
"execution_time_ms": 53
}
}Call 2: offset = 100, same limit.
{
"contract_name": "store_performance",
"query": {
"measures": ["revenue_usd"],
"dimensions": [{ "name": "store_name" }],
"sorts": [{ "field": "revenue_usd", "direction": "desc" }],
"limit": 100,
"offset": 100
}
}Response: another 100 rows, page: 2, has_next_page: true. One more
call with offset: 200 finishes the result set.
Paging loop
To consume a whole result set programmatically:
offset = 0
limit = 100 # or whatever, will be capped at 10000
loop:
response = query(limit, offset)
process(response.data.pagination.items)
if not response.data.pagination.has_next_page: stop
offset = offset + response.data.pagination.limitTwo things to note: increment offset by the effective limit from the
response, not your requested limit, in case the request was clamped.
And stop on has_next_page == false, not on item count, in case the last
page is partial.
Sort stability across pages
sorts is required for stable paging. Without an explicit sort, the
order of items can shift between calls and pagination will skip or
duplicate rows. Always specify at least one sort field, and pick one
whose values are unique (a primary-key dimension is safest) so two rows
never tie.