콘텐츠로 이동

페이지네이션과 cursor

서로 다른 두 가지 방식이 있습니다. 하려는 일에 맞게 고르세요.

하려는 일 사용 위치 표시
최신순으로 페이지를 넘기며 둘러보기 /v1/structured, /v1/updates 불투명한 cursor
늦게 온 글이나 수정된 글까지 놓치지 않고 동기화 /v1/feed 숫자 after

limit의 기본값은 20이며 모든 경로에서 1..100으로 제한됩니다.

/v1/structured/v1/updates는 최신순으로 반환합니다. 응답의 nextCursorcursor로 다시 보내면 다음(더 오래된) 페이지를 받습니다.

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="

구현 그대로의 동작입니다.

  • cursor불투명한 값입니다. 받은 값만 그대로 돌려보내세요. 해석할 수 없는 값은 400 cursor is invalid를 반환합니다.
  • nextCursor는 페이지가 가득 찼을 때(항목이 limit개) 들어 있습니다. 가득 찬 페이지는 더 있을 수도 있다는 뜻이며, 다음 페이지가 비어 있을 수도 있습니다. 항목이 limit개보다 적으면 nextCursornull입니다.
  • 정렬은 메시지의 원본 타임스탬프와 API가 받은 시각 중 더 이른 쪽, 그다음 rawUpdateId 순입니다. 그래서 미래로 적힌 원본 타임스탬프가 메시지를 맨 위에 고정시킬 수 없습니다. sourceTimestamp 자체는 보낸 그대로 반환됩니다.
  • /v1/structured 필터(competition, teamId, teamAlias, team, postType)는 페이지를 자르기 전에 적용되므로, 필터를 건 페이지에는 조건에 맞는 항목이 최대 limit개 담기고 nextCursor 규칙도 같습니다. 모든 페이지에 같은 필터를 보내세요.
  • cursor는 그 정렬에서의 위치를 가리킵니다. 이것은 이지 동기화 수단이 아닙니다. 나중에 저장된 메시지가 더 새로운 위치를 가지면 cursor보다 앞쪽에 나타나며, cursor를 이어서 따라가도 반환되지 않습니다. 새 메시지를 보려면 첫 페이지를 다시 가져오세요.

/v1/updates는 대략적인 필터링을 위한 since(오프셋이 있는 타임스탬프)도 받습니다. since만으로는 안전한 이어가기를 표현할 수 없으므로 페이지를 넘길 때는 cursor를 쓰세요.

/v1/feed는 API가 저장한 순서대로 observation을 반환합니다(id는 계속 증가하며 오래된 것이 먼저). 원본 타임스탬프는 전혀 관여하지 않으므로, 늦게 도착한 글, 재전달된 글, 오래된 글의 수정본이 도착하는 대로 모두 반환됩니다.

  1. after=0으로 시작합니다.
  2. 받은 updates를 처리한 뒤 nextAfter를 저장합니다.
  3. after=<nextAfter>로 다시 요청합니다. hasMorefalse이면 일단 멈추고, 나중에 저장해 둔 같은 값에서 다시 폴링하세요.

nextAfter는 반환된 마지막 id(새 항목이 없으면 내가 보낸 after)이므로 언제든 저장해도 안전합니다. hasMore는 페이지가 가득 찼을 때(더 있을 수 있을 때) true입니다.

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
}

최소한의 반복문 예시입니다.

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;
}

수정은 새 observation이므로 소비자는 두 버전을 모두 받습니다. rawUpdateId로 같은 메시지의 observation을 묶고, id가 가장 큰 것이 최신입니다.

관심 있는 메시지의 구조화 결과는 /v1/structuredhistory 경로로 가져오세요.