Three lists are read page by page: leagues, matches and events. The same mechanism serves all three.

Read a page

Two query parameters set the page.

  • limit: the number of items. It is 25 by default, 100 at most.
  • after: the id of the last item of the previous page. Without it, you read from the start.

A limit above 100 is refused, not trimmed. So is a limit that is not an integer. The API answers INVALID_INPUT.

curl https://gorally.games/v1/matches \
  -H "Authorization: Bearer $RALLY_API_KEY"

The envelope

Every paginated list returns the same object.

{
  "object": "list",
  "data": [
    {
      "id": "f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c",
      "categoryId": "e2b4d6f8-1a3c-4e5b-9d7f-0c2e4a6b8d1f",
      "status": "SCHEDULED",
      "teamAId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "teamBId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
      "scoreA": null,
      "scoreB": null,
      "sets": null,
      "startsAt": "2027-01-13T00:15:00.000Z",
      "revision": 3
    }
  ],
  "has_more": true
}
  • object is always list.
  • data holds the page’s items.
  • has_more is true when items remain after this page.

Walk the whole list

  1. Read the first page, without after.
  2. Process data.
  3. If has_more is true, read again with after set to the id of the last item in data. If data is empty, keep the same after, wait a moment and read again.
  4. Stop when has_more is false.
let after;
let hasMore = true;
while (hasMore) {
  const url = new URL('https://gorally.games/v1/leagues');
  url.searchParams.set('limit', '100');
  if (after) url.searchParams.set('after', after);
  const page = await (await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.RALLY_API_KEY}` },
  })).json();
  for (const league of page.data) console.log(league.name);
  hasMore = page.has_more;
  if (page.data.length > 0) after = page.data.at(-1).id;
  else if (hasMore) await new Promise((resolve) => setTimeout(resolve, 1000));
}

An unknown cursor is refused with INVALID_INPUT. Rally does not restart from the beginning: you would receive a page twice. Do not build a cursor, copy an id you received.

What stays stable

The walk follows an internal insertion number that nothing moves. Renaming a league or correcting a match between two pages neither hides it nor brings it back.

A write whose transaction is still in flight is delayed, never skipped. It appears in its place once committed, exactly once. A page can therefore be shorter than limit while has_more is true: keep going. When that write sits right after your cursor, data is even empty with has_more at true. Nothing is lost: read again a little later with the same after.

Complete lists

A season’s categories and a category’s standings return all their items at once. They take neither limit nor after, and their envelope has no has_more.

What next

  • Read the “Limits” guide: a loop of pages counts against your quota.

Next guide: Limits →