# Pagination

https://gorally.games/en/developers/pagination

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**

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

**JavaScript**

```js
const response = await fetch('https://gorally.games/v1/matches', {
  headers: {
    Authorization: `Bearer ${process.env.RALLY_API_KEY}`,
  },
});
const data = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://gorally.games/v1/matches",
    headers={"Authorization": f"Bearer {os.environ['RALLY_API_KEY']}"},
    timeout=10,
)
data = response.json()
```

## The envelope

Every paginated list returns the same object.

```json
{
  "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`.

```js
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.
