Skip to main content
Every list response uses the same envelope, so one paging helper works for all of them.
boolean
Whether another page exists. This — not an empty data array — is the loop condition.
string | null
Pass as cursor on the next request. Null on the last page.
integer | null
Approximate number of matches. Null when the result set is too large to count exactly. Useful for a progress estimate, never as a loop bound.

The loop

Search endpoints take the cursor in the JSON body; GET endpoints take it as a query parameter. Otherwise the mechanics are identical.

Cursor rules

The encoding is an implementation detail and will change. Never decode one, construct one, or infer an offset from it.
Filters, sort, period and currency are captured when the first page is issued. Changing any of them invalidates the cursor — start a new search instead.
An expired cursor returns 400 with cursor_expired. If you are pausing between pages for longer than that, restart the search rather than holding the cursor.
A cursor holds a consistent view for its lifetime, so an entity will not appear twice or vanish mid-scan because the data refreshed underneath you.

Page size

limit accepts 1 to 100 and defaults to 25. Use 100 for anything you are iterating. Credits are charged per row, not per request, so larger pages cost the same in credits and a quarter as much in rate limit budget.
The default of 25 exists so an exploratory call in a terminal returns something readable. In code there is essentially no reason not to pass 100.

Result depth

Searches page to a depth of 1,000 rows. Past that, has_more is false even if total_count is larger.
Detail and management endpoints — GET /shops/{id}/products, GET /lists/{id}/items, GET /jobs/{id}/results — have no such cap. The limit applies to ranked search, where depth beyond the top of the ranking is not meaningfully ordered. To go deeper: narrow the filters, shard by category or period, or use an export job. See Search.

What not to do

Do not loop on len(page["data"]) > 0. A page can legitimately come back short — filtered rows are removed after the page is assembled — while has_more is still true. Breaking on a short page truncates the result set silently.