Pagination
How list endpoints split results into pages, and how to read them all.
Endpoints that can return a lot of results hand them out a page at a time. You choose the page and its size, and a meta object tells you how many pages there are.
Which endpoints are paged
| Endpoint | Default page size | Largest page |
|---|---|---|
/competitions |
20 | 100 |
/teams |
20 | 100 |
/players |
20 | 100 |
/matches |
20 | 100 |
/coverage/leagues |
100 | 200 |
Everything else returns the whole list in one go: seasons, squads, standings and timelines, for example. Those endpoints don't take page or per_page, and sending them gets 400 UNKNOWN_PARAMETER.
Parameters
| Parameter | Meaning |
|---|---|
page |
Which page you want, counting from 1. Defaults to 1. |
per_page |
How many results on each page. Defaults to 20 (100 on /coverage/leagues). |
Both must be whole numbers above zero. A zero, a negative number, or a per_page above the maximum returns 400 BAD_REQUEST.
The meta object
Paged responses put a meta object next to data:
{
"data": [],
"meta": {
"page": 1,
"per_page": 20,
"total": 204,
"total_pages": 11
}
}
| Field | Meaning |
|---|---|
page |
The page you asked for. |
per_page |
The page size you asked for. The last page can hold fewer results. |
total |
How many results there are across all pages. |
total_pages |
How many pages that makes at this page size. 0 when nothing matched. |
Reading every page
Ask for the largest page size, then keep going until page reaches meta.total_pages.
async function fetchAllPages(path, filters = {}) {
const results = [];
for (let page = 1; ; page++) {
const params = new URLSearchParams({ ...filters, page: String(page), per_page: "100" });
const res = await fetch(`https://api.soccer-data.co/v1${path}?${params}`, {
headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY },
});
if (!res.ok) throw new Error(`Request failed with ${res.status}`);
const body = await res.json();
results.push(...body.data);
if (page >= body.meta.total_pages) return results;
}
}
const matches = await fetchAllPages("/matches", {
competition_id: "competition_705uv6jm8myn",
season_id: "season_hr1ie7yrsdmt",
});
console.log(`${matches.length} matches`);
import os, requests
def fetch_all_pages(path, **filters):
results, page = [], 1
while True:
res = requests.get(
f"https://api.soccer-data.co/v1{path}",
params={**filters, "page": page, "per_page": 100},
headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]},
)
res.raise_for_status()
body = res.json()
results.extend(body["data"])
if page >= body["meta"]["total_pages"]:
return results
page += 1
matches = fetch_all_pages("/matches", competition_id="competition_705uv6jm8myn", season_id="season_hr1ie7yrsdmt")
print(len(matches), "matches")
Note Every page is a separate request and counts against your rate limits. Narrow the list with filters such as
competition_id,season_id,team_id,date_fromanddate_tobefore you page through it, rather than downloading everything and filtering in your code.
Going past the last page
- On
/matches, asking for a page after the last one returns400 PAGE_OUT_OF_RANGE. The message tells you the page you asked for and how many pages there are. - On the other paged endpoints you get
200with an emptydatalist.metastill shows the realtotalandtotal_pages.
Either way, the loop above never gets there, because it stops at meta.total_pages.
Ordering
The order of results is fixed, so pages don't shuffle between requests.
/matcheslists the latest kickoff first (sort=-utc_date), so upcoming fixtures come before past results. Usesort=utc_datefor the soonest first. Matches with the same kickoff time always come back in the same order./competitions,/teamsand/playersput the best match first when yousearch, and sort A to Z by name otherwise. Addsort=nameto get A to Z with a search too.
One-result shortcuts
per_page=1 combined with the right filters and order answers common questions in one request:
# A team's next match
curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=scheduled&sort=utc_date&per_page=1" \
-H "X-Api-Key: YOUR_API_KEY"
# A team's latest result (the default order already puts it first)
curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=finished&per_page=1" \
-H "X-Api-Key: YOUR_API_KEY"
Without status and sort, per_page=1 gives you the team's match furthest in the future, which is rarely what you want.
Common mistakes
- Sending
pageorper_pageto an endpoint that isn't paged. It returns400 UNKNOWN_PARAMETER. - Setting
per_pagetoo high, orpageto zero. Both return400 BAD_REQUEST. - Looping past
total_pageson/matches. You getPAGE_OUT_OF_RANGE, not an empty page. - Forgetting the stop condition. Always compare
pagewithmeta.total_pages. - Paging through everything to filter in code. Filter in the request instead and save requests.
# Pagination
How list endpoints split results into pages, and how to read them all.
Endpoints that can return a lot of results hand them out a page at a time. You choose the page and its size, and a `meta` object tells you how many pages there are.
## Which endpoints are paged
| Endpoint | Default page size | Largest page |
|---|---:|---:|
| [`/competitions`](/docs/reference/competitions/list) | 20 | 100 |
| [`/teams`](/docs/reference/teams/list) | 20 | 100 |
| [`/players`](/docs/reference/players/list) | 20 | 100 |
| [`/matches`](/docs/reference/matches/list) | 20 | 100 |
| [`/coverage/leagues`](/docs/reference/coverage/leagues) | 100 | 200 |
Everything else returns the whole list in one go: seasons, squads, standings and timelines, for example. Those endpoints don't take `page` or `per_page`, and sending them gets `400 UNKNOWN_PARAMETER`.
## Parameters
| Parameter | Meaning |
|---|---|
| `page` | Which page you want, counting from 1. Defaults to 1. |
| `per_page` | How many results on each page. Defaults to 20 (100 on `/coverage/leagues`). |
Both must be whole numbers above zero. A zero, a negative number, or a `per_page` above the maximum returns `400 BAD_REQUEST`.
## The `meta` object
Paged responses put a `meta` object next to `data`:
```json
{
"data": [],
"meta": {
"page": 1,
"per_page": 20,
"total": 204,
"total_pages": 11
}
}
```
| Field | Meaning |
|---|---|
| `page` | The page you asked for. |
| `per_page` | The page size you asked for. The last page can hold fewer results. |
| `total` | How many results there are across all pages. |
| `total_pages` | How many pages that makes at this page size. `0` when nothing matched. |
## Reading every page
Ask for the largest page size, then keep going until `page` reaches `meta.total_pages`.
```javascript
async function fetchAllPages(path, filters = {}) {
const results = [];
for (let page = 1; ; page++) {
const params = new URLSearchParams({ ...filters, page: String(page), per_page: "100" });
const res = await fetch(`https://api.soccer-data.co/v1${path}?${params}`, {
headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY },
});
if (!res.ok) throw new Error(`Request failed with ${res.status}`);
const body = await res.json();
results.push(...body.data);
if (page >= body.meta.total_pages) return results;
}
}
const matches = await fetchAllPages("/matches", {
competition_id: "competition_705uv6jm8myn",
season_id: "season_hr1ie7yrsdmt",
});
console.log(`${matches.length} matches`);
```
```python
import os, requests
def fetch_all_pages(path, **filters):
results, page = [], 1
while True:
res = requests.get(
f"https://api.soccer-data.co/v1{path}",
params={**filters, "page": page, "per_page": 100},
headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]},
)
res.raise_for_status()
body = res.json()
results.extend(body["data"])
if page >= body["meta"]["total_pages"]:
return results
page += 1
matches = fetch_all_pages("/matches", competition_id="competition_705uv6jm8myn", season_id="season_hr1ie7yrsdmt")
print(len(matches), "matches")
```
> **Note:** Every page is a separate request and counts against your [rate limits](/docs/concepts/rate-limits). Narrow the list with filters such as `competition_id`, `season_id`, `team_id`, `date_from` and `date_to` before you page through it, rather than downloading everything and filtering in your code.
## Going past the last page
- On `/matches`, asking for a page after the last one returns `400 PAGE_OUT_OF_RANGE`. The message tells you the page you asked for and how many pages there are.
- On the other paged endpoints you get `200` with an empty `data` list. `meta` still shows the real `total` and `total_pages`.
Either way, the loop above never gets there, because it stops at `meta.total_pages`.
## Ordering
The order of results is fixed, so pages don't shuffle between requests.
- **`/matches`** lists the latest kickoff first (`sort=-utc_date`), so upcoming fixtures come before past results. Use `sort=utc_date` for the soonest first. Matches with the same kickoff time always come back in the same order.
- **`/competitions`, `/teams` and `/players`** put the best match first when you `search`, and sort A to Z by name otherwise. Add `sort=name` to get A to Z with a search too.
## One-result shortcuts
`per_page=1` combined with the right filters and order answers common questions in one request:
```bash
# A team's next match
curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=scheduled&sort=utc_date&per_page=1" \
-H "X-Api-Key: YOUR_API_KEY"
# A team's latest result (the default order already puts it first)
curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=finished&per_page=1" \
-H "X-Api-Key: YOUR_API_KEY"
```
Without `status` and `sort`, `per_page=1` gives you the team's match furthest in the future, which is rarely what you want.
## Common mistakes
- **Sending `page` or `per_page` to an endpoint that isn't paged.** It returns `400 UNKNOWN_PARAMETER`.
- **Setting `per_page` too high, or `page` to zero.** Both return `400 BAD_REQUEST`.
- **Looping past `total_pages` on `/matches`.** You get `PAGE_OUT_OF_RANGE`, not an empty page.
- **Forgetting the stop condition.** Always compare `page` with `meta.total_pages`.
- **Paging through everything to filter in code.** Filter in the request instead and save requests.