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.
Browsing with cursor
Section titled “Browsing with cursor”/v1/structured and /v1/updates return newest first. The response carries nextCursor; send it back as cursor to get the next, older page.
curl "https://api.chizumulu.net/v1/structured?limit=2"curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQxNToyMDo1Mi4wMDBafDQ="curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQwNDo0ODoxOC4wMDBafDI="Semantics, exactly as implemented:
cursoris opaque. Only ever pass back a value you received. A value that cannot be decoded returns400cursor is invalid.nextCursoris present when the page was full (it hadlimititems). A full page means there may be more: the next page can be empty.nextCursorisnullwhen the page had fewer thanlimititems.- 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.sourceTimestampitself is returned exactly as sent. /v1/structuredfilters (competition,teamId,teamAlias,team,postType) are applied before the page is cut, so a filtered page holds up tolimitmatching items andnextCursorfollows 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.
Syncing with /v1/feed
Section titled “Syncing with /v1/feed”/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.
- Start with
after=0. - Process the
updatesyou get, then storenextAfter. - Ask again with
after=<nextAfter>. Stop for now whenhasMoreisfalse; 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).
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 runsfor (;;) { 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.