Collections page by cursor, not by page number. This guide is the whole procedure.
Request the first page, then follow links.next until it is null.
GET /v1/sites?limit=200
{
"data": [ … ],
"meta": { "page": { "limit": 200, "has_more": true } },
"links": {
"self": "https://api.winteriscoming.fyi/v1/sites?limit=200",
"next": "https://api.winteriscoming.fyi/v1/sites?limit=200&cursor=eyJpZCI6…",
"prev": null
}
}
Follow links.next as given. Do not rebuild it: it already carries your
filters, and a cursor is only meaningful alongside the filters and sort it was
issued for. Reassembling the query string by hand is the usual way filters get
dropped halfway through an export.
url = "https://api.winteriscoming.fyi/v1/sites?limit=200"
while url:
page = get(url).json()
handle(page["data"])
url = page["links"]["next"]
You will not find total or last_page. Counting every matching row on every
page is the expensive half of offset paging, and it is what made the legacy export
surface unusable at scale. meta.page.has_more answers the question a paging
client actually has.
If you need a count, count what you received.
?page=2Offset paging is unstable while data changes underneath you: insert a row while a client is on page 4 and every later page shifts, so records get returned twice or skipped entirely. A cursor says "give me what comes after this row", which is stable regardless of what else happens, and stays fast at any depth.
Records are ordered by id ascending by default — an immutable unique key, so a
row edited mid-walk cannot move and be seen twice.
Full exports are rarely what you want after the first one. Every collection
accepts filter[updated_since]:
GET /v1/sites?filter[updated_since]=2026-08-01T00:00:00Z
Store the time your last successful run started, and pass that next time. Use the start rather than the end, so anything written while the run was in flight is picked up by the following one rather than missed.
Two ways to shrink a response, both worth using on a large export:
fields[sites]=id,name,updated_at — only the fields listed. id always comes
back whether you ask for it or not.include=contacts — related records that are otherwise absent. They cost
something, so they are opt-in.Anything you name in include survives a fields selection; you do not have to
list it twice.