Skip to content

Pagination and cursors

There are two different mechanisms. Pick by what you are doing.

You want to… Use Position marker
browse newest first, page by page /v1/structured, /v1/updates opaque cursor
sync everything, without missing late or edited posts /v1/feed numeric after

limit defaults to 20 and is clamped to 1..100 on every route.

/v1/structured and /v1/updates return newest first. The response carries nextCursor; send it back as cursor to get the next, older page.

Terminal window
curl "https://api.chizumulu.net/v1/structured?limit=2"
Terminal window
curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQxNToyMDo1Mi4wMDBafDQ="
Terminal window
curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQwNDo0ODoxOC4wMDBafDI="

Semantics, exactly as implemented:

  • cursor is opaque. Only ever pass back a value you received. A value that cannot be decoded returns 400 cursor is invalid.
  • nextCursor is present when the page was full (it had limit items). A full page means there may be more: the next page can be empty. nextCursor is null when the page had fewer than limit items.
  • Order is by the earlier of the message’s source timestamp and the time the API received it, then by rawUpdateId. A source timestamp in the future therefore cannot pin a message to the top. sourceTimestamp itself is returned exactly as sent.
  • /v1/structured filters (competition, teamId, teamAlias, team, postType) are applied before the page is cut, so a filtered page holds up to limit matching items and nextCursor follows the same rule. Send the same filters with every page.
  • The cursor marks a position in that ordering. It is a view, not a synchronization primitive: a message that is stored later with a newer position appears ahead of your cursor and will not be returned by continuing it. Fetch the first page again to see it.

/v1/updates also accepts since (a timestamp with an offset) for coarse filtering. since alone cannot express a safe continuation; use cursor to page.

/v1/feed returns observations in the order the API stored them (id, strictly increasing, oldest first). Source timestamps play no part, so a late post, a replayed post and an edit of an old post are all returned when they arrive.

  1. Start with after=0.
  2. Process the updates you get, then store nextAfter.
  3. Ask again with after=<nextAfter>. Stop for now when hasMore is false; poll again later from the same stored value.

nextAfter is the last id returned, or your own after when nothing new exists, so it is always safe to persist. hasMore is true when the page was full (there may be more).

Terminal window
curl "https://api.chizumulu.net/v1/feed?after=3&limit=3"
GET /v1/feed?after=3&limit=3
{
"updates": [
{
"id": 4,
"observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e04",
"rawUpdateId": 4,
"source": "whatsapp",
"sourceChannelId": "120363420132778835@newsletter",
"sourceMessageId": "3EB0A1F2C4D5E6F70004",
"sourceServerId": null,
"sourceTimestamp": "2026-09-19T15:20:52.000Z",
"receivedAt": "2026-09-19T15:20:54.451Z",
"collectorObservedAt": "2026-09-19T15:20:54.140Z",
"content": {
"text": "88 GOAL! \n\nNorman Ndlovu completes his brace as Mafu Stars extend their lead! \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame"
}
},
{
"id": 5,
"observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e05",
"rawUpdateId": 5,
"source": "whatsapp",
"sourceChannelId": "120363420132778835@newsletter",
"sourceMessageId": "3EB0A1F2C4D5E6F70005",
"sourceServerId": null,
"sourceTimestamp": "2026-09-19T15:24:20.000Z",
"receivedAt": "2026-09-19T15:24:22.805Z",
"collectorObservedAt": "2026-09-19T15:24:22.493Z",
"content": {
"text": "Fulltime \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame"
}
},
{
"id": 6,
"observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e06",
"rawUpdateId": 4,
"source": "whatsapp",
"sourceChannelId": "120363420132778835@newsletter",
"sourceMessageId": "3EB0A1F2C4D5E6F70004",
"sourceServerId": null,
"sourceTimestamp": "2026-09-19T15:20:52.000Z",
"receivedAt": "2026-09-19T15:26:31.207Z",
"collectorObservedAt": "2026-09-19T15:26:30.911Z",
"content": {
"text": "88' GOAL! \n\nNorman Ndlovu completes his brace as Mafu Stars extend their lead! \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame"
}
}
],
"nextAfter": 6,
"hasMore": true
}

A minimal loop:

let after = 0; // persist this between runs
for (;;) {
const res = await fetch(`https://api.chizumulu.net/v1/feed?after=${after}&limit=100`);
const page = await res.json();
for (const observation of page.updates) handle(observation);
after = page.nextAfter;
if (!page.hasMore) break;
}

Because an edit is a new observation, a consumer sees both versions. Use rawUpdateId to group observations of the same message; the one with the highest id is the latest.

Then fetch structured results for the messages you care about with /v1/structured or the history route.