> ## Agent Instructions
> For the complete API reference (every endpoint, query parameter and response field), fetch https://api.thestatsapi.com/llms.txt. Only use endpoints, parameters and fields listed there.
> Base URL: https://api.thestatsapi.com/api. Auth header: Authorization: Bearer YOUR_API_KEY. Keep the key in server-side code only.
# Get a competition
Source: https://www.thestatsapi.com/docs/api-reference/competitions/get
GET /football/competitions/{competition_id}
Returns one competition with its `current_season_id` and flags for which data it has (team stats, player stats, odds, xG). Use `current_season_id` to fetch this season's matches, standings and stats.
**Example:** `GET /football/competitions/comp_3039`
Read `current_season_id` from here instead of hard-coding a season. See [Finding IDs](/docs/guides/finding-ids).
# List groups in a season
Source: https://www.thestatsapi.com/docs/api-reference/competitions/groups
GET /football/competitions/{competition_id}/seasons/{season_id}/groups
Returns the groups (A to L) of a tournament season with a group stage, such as the FIFA World Cup, EURO, AFCON or the pre-2024 UEFA Champions League. Returns an empty list for competitions without groups, such as the Premier League. Pass a `group_label` to `/football/matches?group=` or to the standings `group` filter. Returns 400 if `season_id` belongs to a different competition.
**Example:** `GET /football/competitions/comp_6107/seasons/sn_326766/groups` (World Cup 2022, groups A to H)
Only useful for tournaments with a group stage. See [Standings](/docs/guides/standings).
# List competitions
Source: https://www.thestatsapi.com/docs/api-reference/competitions/list
GET /football/competitions
Returns football competitions (leagues, cups and international tournaments), 20 per page. Use it to find a `competition_id` by name or country. With `search`, the best match comes first.
**Examples**
- Find the Premier League: `GET /football/competitions?search=Premier`
- English leagues: `GET /football/competitions?country_code=GB-ENG&type=league`
Use it to find a `competition_id`, then pass it to matches, teams and standings. More ways to look up IDs: [Finding IDs](/docs/guides/finding-ids).
# List a competition's seasons
Source: https://www.thestatsapi.com/docs/api-reference/competitions/seasons
GET /football/competitions/{competition_id}/seasons
Returns every season of a competition, newest first, with its `season_id`. The season with `is_current: true` is the same as `current_season_id` on the competition.
**Example:** `GET /football/competitions/comp_3039/seasons`
Use it to get `season_id`s for past seasons. How far back each competition goes varies: see [Coverage](/docs/concepts/coverage).
# Get season standings
Source: https://www.thestatsapi.com/docs/api-reference/competitions/standings
GET /football/competitions/{competition_id}/seasons/{season_id}/standings
Returns the league table for one competition season, one row per team, sorted by `position`. Group-stage tournaments return every group's table, sorted by `group_label` and then `position`; add `group` to get one group. Knockout-only cups (such as the FA Cup) return an empty list. Returns 400 if `season_id` belongs to a different competition.
**Examples**
- Premier League 2026/27: `GET /football/competitions/comp_3039/seasons/sn_8406098/standings`
- World Cup 2022, Group A: `GET /football/competitions/comp_6107/seasons/sn_326766/standings?group=A`
Walkthrough with code: [Standings](/docs/guides/standings).
# Get coverage for a competition
Source: https://www.thestatsapi.com/docs/api-reference/coverage/league
GET /coverage/leagues/{competition_id}
Returns season-by-season coverage for one competition: finished and total matches, a season status, and how many finished matches have each data type. The list includes every season we know of, so check `status` (`not_loaded` means no matches are loaded for that season). Seasons are not sorted by date. New data type keys may be added, so ignore keys you don't recognise.
**Example:** `GET /coverage/leagues/comp_3039` (Premier League)
How to check coverage before you build: [Coverage](/docs/concepts/coverage).
# List coverage by competition
Source: https://www.thestatsapi.com/docs/api-reference/coverage/leagues
GET /coverage/leagues
Returns one row per competition: how many seasons are loaded, which data types it has (fixtures, team stats, xG, odds, lineups, player stats, standings), the latest season's status and the number of finished matches. Use it to check what's available before you build. Add `data_type` to list only competitions that have that data. Counts only include finished matches, and responses are cached for several hours.
**Example:** `GET /coverage/leagues?data_type=xg&search=Premier`
How to check coverage before you build: [Coverage](/docs/concepts/coverage).
# Get coverage totals
Source: https://www.thestatsapi.com/docs/api-reference/coverage/summary
GET /coverage/summary
Returns headline totals across all competitions: number of competitions, seasons and finished matches, and seasons by status.
**Example:** `GET /coverage/summary`
For per-competition detail, use [List coverage by competition](/docs/api-reference/coverage/leagues). See [Coverage](/docs/concepts/coverage).
# Check API health
Source: https://www.thestatsapi.com/docs/api-reference/health
GET /health
Returns `healthy` and the server time when the API is up. No API key is needed and the call doesn't count against your quota.
Use it for uptime monitoring. To check that your key works, call any other endpoint, such as [List competitions](/docs/api-reference/competitions/list).
# Get a match
Source: https://www.thestatsapi.com/docs/api-reference/matches/get
GET /football/matches/{match_id}
Returns one match with its score breakdown, half-time score, venue, referee and managers. Flags such as `odds_available`, `xg_available` and `shotmap_available` tell you which other match endpoints have data, so check them first.
**Example:** `GET /football/matches/mt_838955483`
Get the `match_id` from [List matches](/docs/api-reference/matches/list). Recipes with code: [Fixtures and results](/docs/guides/fixtures-and-results).
# Get match lineups
Source: https://www.thestatsapi.com/docs/api-reference/matches/lineups
GET /football/matches/{match_id}/lineups
Returns both teams' starting XI, substitutes and formation. Before kickoff you get a predicted XI, replaced by the official team sheet once it's announced (about an hour before kickoff). For started and finished matches you get the XI that played, with every substitute (used and unused). `type` tells you which one you have (`predicted`, `team_sheet` or `played`), and `confirmed` is true only for a team sheet or a played match. Returns 404 when there's no lineup data for the match yet.
**Example:** `GET /football/matches/mt_838955483/lineups`
Check `type` and `confirmed` to know whether you have a predicted XI, the official team sheet or the XI that played. The player IDs work with [match player stats](/docs/api-reference/matches/player-stats) and [match heatmaps](/docs/api-reference/matches/player-heatmap).
# List matches
Source: https://www.thestatsapi.com/docs/api-reference/matches/list
GET /football/matches
Returns fixtures and results, 20 per page. Combine filters to get a team's next match, a matchday, or every match between two dates.
**Order:** newest kickoff first by default (`sort=-utc_date`). Use `sort=utc_date` for soonest first.
**Examples**
- Next fixture for a team (first row is the next match): `GET /football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1`
- Latest result for a team: `GET /football/matches?team_id=tm_9145&status=finished&per_page=1`
- One day's Premier League matches: `GET /football/matches?competition_id=comp_3039&date_from=2026-10-10&date_to=2026-10-10&sort=utc_date`
- One matchday: `GET /football/matches?competition_id=comp_3039&season_id=sn_8406098&matchday=6`
There is no `date` parameter. For a single day, set `date_from` and `date_to` to the same date.
Start here to get the `match_id` that every other match endpoint needs. Recipes with code: [Fixtures and results](/docs/guides/fixtures-and-results).
# Get live match odds
Source: https://www.thestatsapi.com/docs/api-reference/matches/live-odds
GET /football/matches/{match_id}/odds/live
Returns in-play odds for a match, one entry per bookmaker quoting in-play prices (Bet365 first, then Paddy Power, BetMGM UK, Betfair Exchange). Every market a bookmaker quotes is included: match result, totals (with quarter lines), both teams to score, corners, Asian and European handicaps, double chance, draw no bet, correct score, team totals, to qualify and more. Bookmakers and markets change as the match goes on and books suspend and reopen. Check `live_odds_available` on the match first, and poll during the match. Returns 404 when there are no live odds for the match.
**Example:** `GET /football/matches/mt_200199332/odds/live`
How to poll during a match: [Live matches](/docs/guides/live-matches). More on odds: [Odds](/docs/guides/odds).
# Get live match player stats
Source: https://www.thestatsapi.com/docs/api-reference/matches/live-player-stats
GET /football/matches/{match_id}/live-player-stats
Returns per-player stats while a match is in play, with `meta.match_status = "live"`. Poll it during the match. Returns 409 when the match is not live; use `/player-stats` after full-time.
**Example:** `GET /football/matches/mt_838955483/live-player-stats`
After full-time, use [Get match player stats](/docs/api-reference/matches/player-stats). How to poll during a match: [Live matches](/docs/guides/live-matches).
# Get live match stats
Source: https://www.thestatsapi.com/docs/api-reference/matches/live-stats
GET /football/matches/{match_id}/live-stats
Returns running team stats while a match is in play, with the clock and score in `data.meta`. Poll it during the match; responses may be cached for up to 30 seconds.
For up to 5 minutes after full-time it still returns the final snapshot with `meta.match_status = "finished"`. After that, and for matches that haven't started or were postponed or cancelled, it returns 409. Use `/stats` for post-match totals.
**Example:** `GET /football/matches/mt_838955483/live-stats`
After full-time, use [Get match stats](/docs/api-reference/matches/stats). How to poll during a match: [Live matches](/docs/guides/live-matches).
# Get live match timeline
Source: https://www.thestatsapi.com/docs/api-reference/matches/live-timeline
GET /football/matches/{match_id}/live-timeline
Returns the same events as `/timeline` while a match is in play, with `meta.match_status = "live"`. Poll it during the match. Returns 409 when the match is not live.
**Example:** `GET /football/matches/mt_838955483/live-timeline?event_type=goal,penalty_scored`
After full-time, use [Get match timeline](/docs/api-reference/matches/timeline). How to poll during a match: [Live matches](/docs/guides/live-matches).
# Get match odds
Source: https://www.thestatsapi.com/docs/api-reference/matches/odds
GET /football/matches/{match_id}/odds
Returns pre-match odds for a match, one entry per bookmaker (Bet365, Paddy Power, BetMGM UK, Pinnacle, Betfair Exchange), with the opening and latest price for each selection. Markets include match result (full time and half time), both teams to score, goals, corners and cards lines, Asian and European handicaps, team totals, shots, correct score, to qualify and penalty specials. A market appears only when that bookmaker prices it. Works for upcoming and finished matches where odds were captured; check `odds_available` on the match first. Returns 404 when the match has no odds. Older matches mostly have Bet365 prices only.
**Example:** `GET /football/matches/mt_200199332/odds?bookmaker=bet365,pinnacle`
Walkthrough with code: [Odds](/docs/guides/odds).
# Get a player's match heatmap
Source: https://www.thestatsapi.com/docs/api-reference/matches/player-heatmap
GET /football/matches/{match_id}/players/{player_id}/heatmap
Returns the pitch locations where one player touched the ball in one match, as `x` and `y` from 0 to 100. Returns 404 when there's no heatmap, for example for an unused substitute or a match without positional data.
**Example:** `GET /football/matches/mt_404012971/players/pl_84027040/heatmap`
For a whole season, use [Get a player's season heatmap](/docs/api-reference/players/season-heatmap).
# Get player-prop odds
Source: https://www.thestatsapi.com/docs/api-reference/matches/player-odds
GET /v2/football/matches/{match_id}/odds/players
Returns the latest player-prop odds for a match, grouped by bookmaker (Bet365 first) and then by market. A bookmaker appears only if it prices at least one player prop for the match. Markets cover goalscorers, shots, assists, cards, tackles, fouls, passes, goalkeeper saves and player of the match; which ones you get depends on the bookmaker and the match. Markets with no prices are left out.
Line markets (`player_shots`, `player_shots_on_target`, `player_shots_on_target_outside_box`, `player_headed_shots_on_target`, `player_tackles`, `player_fouls_committed`, `player_to_be_fouled`, `player_passes`, `goalkeeper_saves`) have one entry per player, line and direction, so a player priced at Over 0.5, 1.5 and 2.5 gives three entries. Other markets (goalscorers, `player_assists`, `score_or_assist`, cards, `player_of_the_match`) have no `line` or `market_type` field. `id` is null when a priced player can't be matched to a player ID. Within each market, entries are sorted by line, then `Over` before `Under`, then shortest odds first, then name.
**Example:** `GET /v2/football/matches/mt_200199332/odds/players?bookmaker=bet365`
This is the current version. It replaces the deprecated [v1 endpoint](/docs/api-reference/matches/player-odds-v1). Walkthrough with code: [Odds](/docs/guides/odds).
# Get player-prop odds (v1, deprecated)
Source: https://www.thestatsapi.com/docs/api-reference/matches/player-odds-v1
GET /football/matches/{match_id}/odds/players
**Deprecated since 2026-07-25. Use `/v2/football/matches/{match_id}/odds/players` instead.** v2 has more markets and more bookmakers.
Returns the latest player-prop odds for a match from Bet365 only, for 5 markets: `anytime_goalscorer`, `first_goalscorer`, `player_shots`, `player_shots_on_target` and `player_assists`. Every entry has `line` and `market_type`; they are null where a market has no line (for example `first_goalscorer`). Within each market, entries are sorted by line, then `Over` before `Under`, then shortest odds first, then name. Markets with no prices are left out.
**Example:** `GET /football/matches/mt_200199332/odds/players?markets=anytime_goalscorer,player_shots`
Use [Get player-prop odds](/docs/api-reference/matches/player-odds) instead. It returns more markets and more bookmakers, grouped by bookmaker.
# Get match player stats
Source: https://www.thestatsapi.com/docs/api-reference/matches/player-stats
GET /football/matches/{match_id}/player-stats
Returns per-player stats for a played match: rating, minutes, passing, shooting (including xG and xA), duels, defending, goalkeeping, fouls and cards. Add `player_ids` to get only some players. While the match is live it returns 409; use `/live-player-stats` instead.
**Example:** `GET /football/matches/mt_838955483/player-stats?player_ids=pl_4089246,pl_08984824` (Virgil van Dijk and Alexis Mac Allister)
During a match, use [Get live match player stats](/docs/api-reference/matches/live-player-stats). See [Player and team stats](/docs/guides/player-and-team-stats).
# Get a match's referee
Source: https://www.thestatsapi.com/docs/api-reference/matches/referee
GET /football/matches/{match_id}/referee
Returns the referee assigned to a match, with career totals for games, yellow cards and red cards. `referee` is null when no referee is assigned.
**Example:** `GET /football/matches/mt_838955483/referee`
[Get a match](/docs/api-reference/matches/get) already includes the referee's name. Use this endpoint when you also want their career card totals.
# Get match shotmap
Source: https://www.thestatsapi.com/docs/api-reference/matches/shotmap
GET /football/matches/{match_id}/shotmap
Returns every shot in a match with its xG, outcome, body part, situation and pitch coordinates, plus non-penalty xG totals for both teams. Add `player_id` to get one player's shots. Check `shotmap_available` on the match first.
**Example:** `GET /football/matches/mt_838955483/shotmap`
Walkthrough with code: [Player and team stats](/docs/guides/player-and-team-stats).
# Get match stats
Source: https://www.thestatsapi.com/docs/api-reference/matches/stats
GET /football/matches/{match_id}/stats
Returns team stats for a played match: possession, shots, xG, passes, duels, defending and goalkeeping, each split into full match, first half and second half. While the match is live it returns 409 (`MATCH_IS_LIVE`); use `/live-stats` instead.
**Example:** `GET /football/matches/mt_838955483/stats`
During a match, use [Get live match stats](/docs/api-reference/matches/live-stats). See [Player and team stats](/docs/guides/player-and-team-stats).
# Get match timeline
Source: https://www.thestatsapi.com/docs/api-reference/matches/timeline
GET /football/matches/{match_id}/timeline
Returns a played match's events in order: goals, shots, cards, fouls, offsides, corners, substitutions, penalties, VAR reviews and period markers, each with its team and player where known. Stoppage time is kept separate, so 45+3' comes back as `"minute": 45, "extra_time": 3`.
When there's no timeline you get 200 with an empty `events` list, `meta.coverage = "none"` and a `reason`: `match_not_started`, `no_events` or `not_supported` (the competition isn't covered). 404 means the match doesn't exist. While the match is live it returns 409; use `/live-timeline` instead.
**Example:** `GET /football/matches/mt_838955483/timeline?event_type=goal,penalty_scored,yellow_card`
During a match, use [Get live match timeline](/docs/api-reference/matches/live-timeline). How the live and post-match endpoints fit together: [Live matches](/docs/guides/live-matches).
# API reference overview
Source: https://www.thestatsapi.com/docs/api-reference/overview
Base URL, authentication, response format, IDs, pagination and errors for TheStatsAPI football data REST API.
What every endpoint has in common. Read this once, then use the endpoint pages for exact parameters and response fields.
**Fastest way: give your AI coding tool the full reference.** [`api.thestatsapi.com/llms.txt`](https://api.thestatsapi.com/llms.txt) has every endpoint, parameter and response shape in one Markdown file. Save it in your project:
```bash theme={null}
curl -sL --create-dirs -o docs/thestatsapi-llms.txt https://api.thestatsapi.com/llms.txt
```
Then ask Claude Code, Codex or Cursor: *"Read docs/thestatsapi-llms.txt and build a page that shows Arsenal's next fixture. Keep my API key on the server."* Setup for each tool, including Lovable: [Build with AI](/docs/build-with-ai).
## Base URL
```text theme={null}
https://api.thestatsapi.com/api
```
All endpoints are `GET` and return JSON. The API covers football only.
## Authentication
Send your API key in the `Authorization` header on every request. Get a key at [thestatsapi.com/api-keys](https://www.thestatsapi.com/api-keys). Only [`/health`](/docs/api-reference/health) works without one.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/competitions?search=Premier" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/competitions?search=Premier",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data, meta } = await res.json();
```
```python Python theme={null}
import os, requests
res = requests.get(
"https://api.thestatsapi.com/api/football/competitions",
params={"search": "Premier"},
headers={"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"},
)
data = res.json()["data"]
```
Call the API from your server, not the browser, so your key stays private. More in [Authentication](/docs/authentication).
## Response format
Paginated lists (competitions, teams, players, matches and coverage by competition) return a `data` array and a `meta` object with pagination:
```json theme={null}
{
"data": [
{ "id": "comp_3039", "name": "Premier League", "country": "England", "type": "league" }
],
"meta": { "page": 1, "per_page": 20, "total": 11, "total_pages": 1 }
}
```
Single items return one object in `data`:
```json theme={null}
{ "data": { "id": "comp_3039", "name": "Premier League", "current_season_id": "sn_8406098" } }
```
Short lists such as seasons, standings or a squad come back whole, as a `data` array with no `meta`. A few endpoints, such as the shotmap and timelines, add other top-level fields. The examples are shortened; each endpoint page shows the exact shape and every field.
## IDs
IDs are strings with a prefix that tells you the type.
| Prefix | Type | Example |
| - | - | - |
| `comp_` | Competition | `comp_3039` (Premier League) |
| `sn_` | Season | `sn_8406098` (Premier League 2026/27) |
| `tm_` | Team | `tm_9145` (Arsenal) |
| `mt_` | Match | `mt_200199332` |
| `pl_` | Player | `pl_45126714` (Bukayo Saka) |
Look IDs up with the `search` filter on [competitions](/docs/api-reference/competitions/list), [teams](/docs/api-reference/teams/list) and [players](/docs/api-reference/players/list). See [Finding IDs](/docs/guides/finding-ids) and the [data model](/docs/concepts/data-model).
## Pagination
Paginated endpoints return 20 results per page by default. Set `per_page` (up to 100) and `page`, and stop when `page` reaches `meta.total_pages`. Coverage by competition is the exception: 100 per page by default, up to 200. See [Pagination](/docs/concepts/pagination).
## Errors
Every error has the same shape. Branch on `code`:
```json theme={null}
{
"error": {
"code": "UNKNOWN_PARAMETER",
"message": "Unknown query parameter 'date' (did you mean 'date_from' or 'date_to'?). Supported parameters: ...",
"status_code": 400
}
}
```
A misspelled or unsupported query parameter returns `400 UNKNOWN_PARAMETER`, and the message suggests the right name. A missing or invalid key returns `401 UNAUTHORIZED`. All codes are in [Errors](/docs/concepts/errors).
## Rate limits
Every authenticated response carries `X-RateLimit-*` (per minute) and `X-Monthly-Quota-*` (per billing cycle) headers. Going over returns `429` with a `Retry-After` header. See [Rate limits](/docs/concepts/rate-limits) and [pricing](https://www.thestatsapi.com/#pricing).
## Machine-readable reference
| What | URL |
| - | - |
| Full API reference for AI tools (Markdown) | [api.thestatsapi.com/llms.txt](https://api.thestatsapi.com/llms.txt) |
| OpenAPI spec | [api.thestatsapi.com/openapi.json](https://api.thestatsapi.com/openapi.json) |
| These guides for AI tools | [thestatsapi.com/docs/llms-full.txt](https://www.thestatsapi.com/docs/llms-full.txt) |
## Endpoints
Competitions, seasons, groups and standings.
Teams, squads, season stats, standings history, injuries and suspensions.
Fixtures and results, lineups, timelines, team and player stats, shotmaps, heatmaps and referees.
Player profiles, season stats, injuries and suspensions, and season heatmaps.
Pre-match, in-play and player-prop odds.
Which competitions and seasons have which data.
# Get a player
Source: https://www.thestatsapi.com/docs/api-reference/players/get
GET /football/players/{player_id}
Returns one player's profile: name, position, date of birth, age, nationality, height and current team with shirt number.
**Example:** `GET /football/players/pl_45126714` (Bukayo Saka)
For goals, assists and other season numbers, use [Get a player's season stats](/docs/api-reference/players/stats).
# Get a player's injuries and suspensions
Source: https://www.thestatsapi.com/docs/api-reference/players/injuries
GET /football/players/{player_id}/injuries-suspensions
Returns the player's injuries and suspensions in effect now, split into `injuries` and `suspensions`. Set `current=false` to get every record, past ones included (check each record's `active` flag). A suspension only applies in its `competition`. A ban reported twice is listed once, and national-team call-ups listed under an international competition are left out.
**Example:** `GET /football/players/pl_45126714/injuries-suspensions`
For a whole squad in one call, use [Get a team's injuries and suspensions](/docs/api-reference/teams/injuries).
# List players
Source: https://www.thestatsapi.com/docs/api-reference/players/list
GET /football/players
Returns players, 20 per page. Search by name, list a team's players, or fetch several players by ID in one call. With `search`, the best match comes first.
**Examples**
- Find a player: `GET /football/players?search=saka`
- Several players at once: `GET /football/players?player_ids=pl_45126714,pl_29627593`
Use it to find a `player_id`. More ways to look up IDs: [Finding IDs](/docs/guides/finding-ids).
# Get a player's season heatmap
Source: https://www.thestatsapi.com/docs/api-reference/players/season-heatmap
GET /football/players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap
Returns a player's touches across every match they played in one competition season, grouped into pitch cells with a `count`. For tournaments such as the World Cup, group and knockout matches are combined into one map. Returns 400 if `season_id` belongs to a different competition, and 404 when there's no heatmap (for example a season not played yet, or a competition without positional data).
**Example:** `GET /football/players/pl_84027040/competitions/comp_6107/seasons/sn_326766/heatmap` (Dušan Vlahović, World Cup 2022)
For one match, use [Get a player's match heatmap](/docs/api-reference/matches/player-heatmap).
# Get a player's season stats
Source: https://www.thestatsapi.com/docs/api-reference/players/stats
GET /football/players/{player_id}/stats
Returns a player's totals for one season: appearances, starts, minutes, rating, goals, assists, shooting, passing, defending, duels and discipline. `season_id` is required. Add `competition_id` to count one competition only, and `stage` to split league matches from play-offs.
**Example:** `GET /football/players/pl_45126714/stats?season_id=sn_6125938&competition_id=comp_3039` (Bukayo Saka, Premier League 2025/26)
Walkthrough with code: [Player and team stats](/docs/guides/player-and-team-stats).
# Get a team
Source: https://www.thestatsapi.com/docs/api-reference/teams/get
GET /football/teams/{team_id}
Returns one team with its country, main competition, stadium and `is_mens_team`.
**Example:** `GET /football/teams/tm_9145` (Arsenal)
To get a team's fixtures and results, pass its `team_id` to [List matches](/docs/api-reference/matches/list). See [Fixtures and results](/docs/guides/fixtures-and-results).
# Get a team's injuries and suspensions
Source: https://www.thestatsapi.com/docs/api-reference/teams/injuries
GET /football/teams/{team_id}/injuries-suspensions
Returns the team's players who are unavailable now, split into `injuries` and `suspensions`. Set `current=false` to get every record, past ones included (check each record's `active` flag).
By default a suspension is only listed where it applies: a club sees bans from club competitions and a national team sees bans from national-team competitions (a Champions League ban doesn't rule a player out of a Nations League match). Bans whose competition is unknown are always listed. A national team's list leaves out national-team call-ups (reason `national_team`), because the player is with that team; a club's list keeps them, because the player misses club matches. A ban reported twice is listed once.
**Example:** `GET /football/teams/tm_73673/injuries-suspensions` (Real Madrid)
To check one player, use [Get a player's injuries and suspensions](/docs/api-reference/players/injuries).
# List teams
Source: https://www.thestatsapi.com/docs/api-reference/teams/list
GET /football/teams
Returns teams, 20 per page. Use it to find a `team_id` by name, or to list every team in a competition season. With `search`, the best match comes first. Men's and women's sides of a club can share a name, so add `is_mens_team` to tell them apart.
**Examples**
- Arsenal men's team: `GET /football/teams?search=Arsenal&is_mens_team=true`
- Premier League 2026/27 teams: `GET /football/teams?competition_id=comp_3039&season_id=sn_8406098`
Use it to find a `team_id`. More ways to look up IDs: [Finding IDs](/docs/guides/finding-ids).
# Get a team's squad
Source: https://www.thestatsapi.com/docs/api-reference/teams/players
GET /football/teams/{team_id}/players
Returns the players registered to a team (up to 100), with extra profile fields such as preferred foot, contract end date, market value and national team. For a national team, returns the players who represent it.
**Example:** `GET /football/teams/tm_9145/players` (Arsenal)
Use it to get the `player_id`s in a squad, then call [Get a player's season stats](/docs/api-reference/players/stats). See [Player and team stats](/docs/guides/player-and-team-stats).
# List a team's standings history
Source: https://www.thestatsapi.com/docs/api-reference/teams/standings
GET /football/teams/{team_id}/standings
Returns every competition where the team has a league table entry, with its seasons nested inside (newest first). Use it to build a team's table history, then fetch a full table from `/football/competitions/{competition_id}/seasons/{season_id}/standings`. Competitions are ordered by their most recent season. Teams that only play knockout cups get an empty list.
**Example:** `GET /football/teams/tm_9145/standings` (Arsenal)
Walkthrough with code: [Standings](/docs/guides/standings).
# Get a team's season stats
Source: https://www.thestatsapi.com/docs/api-reference/teams/stats
GET /football/teams/{team_id}/stats
Returns a team's record for one season: matches played, wins, draws, losses, goals, points, table position and recent form. `season_id` is required. Returns 404 when the team has no stats for that season.
**Example:** `GET /football/teams/tm_9145/stats?season_id=sn_6125938` (Arsenal, Premier League 2025/26)
Walkthrough with code: [Player and team stats](/docs/guides/player-and-team-stats).
# Authentication
Source: https://www.thestatsapi.com/docs/authentication
Send your TheStatsAPI key in the Authorization header as a Bearer token, keep it on your server, and fix 401 and 403 errors.
Use this page to send your API key correctly, keep it off the browser and fix `401` and `403` errors.
**Fastest way: ask your AI.** Follow [Build with AI](/docs/build-with-ai), and add this to your prompt: "Read my TheStatsAPI key from the THESTATSAPI\_API\_KEY env var and only call the API from server code, never from the browser."
## Send the header
Every request needs this header, except [`/health`](/docs/api-reference/health):
```text theme={null}
Authorization: Bearer YOUR_API_KEY
```
* The value must start with `Bearer` (capital B), then one space, then your key.
* `bearer` in lowercase, a missing space or extra spaces all fail with `401`.
* The header is the only way to send the key. A key in the URL is ignored.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/competitions?per_page=1" \
-H "Authorization: Bearer $THESTATSAPI_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch("https://api.thestatsapi.com/api/football/competitions?per_page=1", {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
console.log(res.status, await res.json());
```
```python Python theme={null}
import os
import requests
res = requests.get(
"https://api.thestatsapi.com/api/football/competitions",
params={"per_page": 1},
headers={"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"},
)
print(res.status_code, res.json())
```
## Get and manage keys
Manage keys on the [API keys page](https://www.thestatsapi.com/api-keys) in your dashboard.
* **Create:** a new key is shown only once. Copy it into your environment variables or secrets manager straight away.
* **Revoke:** a revoked key stops working at once and returns `401 Invalid API key`.
* **Several keys:** you can have up to 5 active keys, for example one per app or environment. Each key has its own per-minute rate limit, and all your keys share your account's monthly quota. See [Rate limits](/docs/concepts/rate-limits).
## Keep your key on the server
Read the key from an environment variable in server code. Never hard-code it.
```bash .env theme={null}
THESTATSAPI_API_KEY=YOUR_API_KEY
```
```javascript Node.js theme={null}
// Run with: node --env-file=.env server.mjs (Node 20.6+), or load .env with the dotenv package
const apiKey = process.env.THESTATSAPI_API_KEY;
if (!apiKey) throw new Error("THESTATSAPI_API_KEY is not set");
```
```python Python theme={null}
import os
api_key = os.environ["THESTATSAPI_API_KEY"] # or load .env with the python-dotenv package
```
Add `.env` to your `.gitignore`.
If you have a web frontend, have it call your own server route, and let only that route call TheStatsAPI. For example, a Next.js route handler:
```javascript app/api/next-fixture/route.js theme={null}
export async function GET() {
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
return Response.json(await res.json(), { status: res.status });
}
```
**Never put your key in browser code.** The API allows cross-origin requests from any website, so a request from the browser will work, but anyone can then read your key from the page or the network tab. In particular:
* Don't use `NEXT_PUBLIC_` or `VITE_` variables for the key. They're bundled into the browser code.
* In **Lovable**, store the key with the **Add secret** form and call the API from a server function or Edge Function. Lovable commits `.env`, so the key must not go there.
* Don't commit the key or paste it into logs, screenshots or support tickets.
If a key leaks, create a new one, update your server, then revoke the old one.
## Auth errors
Auth errors use the standard [error envelope](/docs/concepts/errors):
```json theme={null}
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing authorization header",
"status_code": 401
}
}
```
| Status | `code` | `message` | Fix |
| - | - | - | - |
| 401 | `UNAUTHORIZED` | `Missing authorization header` | Add the `Authorization` header. |
| 401 | `UNAUTHORIZED` | `Invalid authorization format. Use: Bearer ` | Start the value with `Bearer` and one space. |
| 401 | `UNAUTHORIZED` | `Invalid API key` | Check for typos, a missing character or extra spaces. Revoked keys also return this. |
| 403 | `KEY_REVOKED` | `API key has no active subscription plan` | Your account has no active plan. See [pricing](https://www.thestatsapi.com/#pricing). |
| 403 | `FORBIDDEN` | `Account is inactive`, `API key is inactive` or `API key has expired` | Check your subscription and keys in the [dashboard](https://www.thestatsapi.com/api-keys). |
| 403 | `ADDON_REQUIRED` | Names the add-on the endpoint needs | Add it to your subscription, or use a different endpoint. |
| 429 | `RATE_LIMITED`, `USAGE_LIMIT_EXCEEDED` | Per-minute limit or monthly quota reached | Wait for the `Retry-After` header. See [Rate limits](/docs/concepts/rate-limits). |
Every authenticated response, including errors after your key is accepted, also carries your rate-limit and quota headers. See [Rate limits](/docs/concepts/rate-limits).
# Build with AI (fastest way)
Source: https://www.thestatsapi.com/docs/build-with-ai
The fastest way to build on TheStatsAPI: give an AI coding tool like Claude Code, Codex, Cursor or Lovable our llms.txt API reference and a starter prompt.
Use this page to set up your AI coding tool so it writes code against the real API instead of guessing. It takes about five minutes.
## Why this works
* [`https://api.thestatsapi.com/llms.txt`](https://api.thestatsapi.com/llms.txt) is the whole API reference in one Markdown file: every endpoint, parameter and response field. It's generated from our OpenAPI spec, so it matches the live API.
* With it, your AI uses real endpoint and parameter names instead of guessing.
* If the AI still sends a wrong parameter, the API answers with `400 UNKNOWN_PARAMETER` and suggests the right name, so the AI can fix its own mistake.
## Three steps
[Create an account](https://www.thestatsapi.com/auth?mode=signup), then make a key on the [API keys page](https://www.thestatsapi.com/api-keys). The key is shown only once, so save it straight into a `.env` file in your project:
```bash .env theme={null}
THESTATSAPI_API_KEY=YOUR_API_KEY
```
Make sure `.env` is listed in your `.gitignore`.
This is the link your AI needs:
```text theme={null}
https://api.thestatsapi.com/llms.txt
```
If your tool works on files in your project (Claude Code, Codex, Cursor), save a copy into the project too:
```bash theme={null}
mkdir -p docs
curl -sL https://api.thestatsapi.com/llms.txt -o docs/thestatsapi-llms.txt
```
The file is about 300 KB. Web-fetch tools often summarise or cut off long pages, but a local copy can be searched in full. Re-run the `curl` command now and then to pick up API changes. Don't add this file to `.gitignore`: Cursor hides git-ignored files from its AI.
Replace `[describe your app]` and paste this into your AI tool. It points the AI at the reference, keeps your key on the server and heads off the most common mistakes.
```text Starter prompt wrap theme={null}
I'm building [describe your app] with TheStatsAPI, a football data REST API.
Before you write any code, read the API reference: https://api.thestatsapi.com/llms.txt
If docs/thestatsapi-llms.txt exists in this project, search that copy instead (same file).
Only use endpoints, query parameters and response fields that appear in it. Don't guess.
Rules:
- Base URL: https://api.thestatsapi.com/api. Send the header Authorization: Bearer .
- Read the key from the server-side env var THESTATSAPI_API_KEY. Never put it in browser
code, in a VITE_ or NEXT_PUBLIC_ variable, or in git. The frontend calls my own server
route, and only that route calls TheStatsAPI.
- IDs are prefixed strings (comp_, sn_, tm_, pl_, mt_). Look them up with the search
endpoints, e.g. /football/teams?search=Arsenal&is_mens_team=true. Don't guess IDs.
- Dates use date_from and date_to (YYYY-MM-DD). There is no "date" parameter.
- /football/matches returns newest first. For upcoming fixtures use status=scheduled&sort=utc_date.
- Lists are paginated with page and per_page (max 100 on most endpoints). Read meta.total_pages.
- Errors look like {"error": {"code", "message", "status_code"}}. 400 UNKNOWN_PARAMETER
means a parameter name is wrong: use the name the message suggests. On 429 (RATE_LIMITED
or USAGE_LIMIT_EXCEEDED), wait for the number of seconds in the Retry-After header.
- Cache responses. There are no webhooks, so poll live data only as often as the feature needs.
- Check availability flags (has_player_stats, xg_available, shotmap_available, odds_available)
before calling optional endpoints. History depth varies by competition: check /coverage/leagues.
First, make one test request from the server (Arsenal's next fixture:
/football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1) and show me the result.
```
Want a prompt written for your exact idea? The [Build with AI prompt generator](https://www.thestatsapi.com/build-with-ai) in your dashboard asks a few questions about your app, then writes a prompt for Claude Code, Cursor, Lovable or another tool. You need to be logged in.
## Set up your tool
For Claude Code, Codex and Cursor, also save these project notes in your project. The AI reads them every session, so it keeps following the rules. Keep the reference file path in backticks and don't paste the whole `llms.txt` into this file: it's too big to load every time.
```markdown Project notes wrap theme={null}
## TheStatsAPI
- API reference: `docs/thestatsapi-llms.txt` (live copy: https://api.thestatsapi.com/llms.txt).
Search it for the exact endpoint, parameter and field names before writing a call.
Don't load the whole file at once.
- Base URL https://api.thestatsapi.com/api. Header: `Authorization: Bearer $THESTATSAPI_API_KEY`.
- The key lives in `.env` as THESTATSAPI_API_KEY and is only read by server code.
Never send it to the browser, log it or commit it.
- Dates: `date_from` / `date_to` (YYYY-MM-DD). There is no `date` parameter.
- Upcoming fixtures: `status=scheduled&sort=utc_date`. `per_page` max 100.
- 400 `UNKNOWN_PARAMETER` = wrong parameter name; the message suggests the right one.
- 429 = wait for `Retry-After` seconds. Cache responses.
```
1. In your project folder, download the reference (step 2) and create `.env` (step 1).
2. Save the project notes above as `CLAUDE.md` in the project root.
3. Run `claude` and paste the starter prompt.
Optional: stop Claude Code from reading your key file. Add this to `.claude/settings.json`:
```json .claude/settings.json theme={null}
{
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}
```
1. Download the reference (step 2) and create `.env` (step 1) **before** you start Codex. Codex's default sandbox has no network access.
2. Save the project notes above as `AGENTS.md` in the project root. Codex reads it automatically.
3. Run `codex` and paste the starter prompt.
4. When Codex first calls the API, approve the network request. To allow it every time, add this to `~/.codex/config.toml`:
```toml ~/.codex/config.toml theme={null}
[sandbox_workspace_write]
network_access = true
```
**Codex Cloud:** commit `docs/thestatsapi-llms.txt` and `AGENTS.md` so the cloud agent can read them. The agent has no internet access by default: in the environment settings, turn it on and allow `api.thestatsapi.com`. Codex Cloud removes secrets before the agent runs, so if you want it to make test calls, add `THESTATSAPI_API_KEY` as an environment variable instead. Use a separate key that you can revoke.
1. Download the reference (step 2) and create `.env` (step 1).
2. Save the project notes above as `AGENTS.md` in the project root. Cursor reads it automatically.
3. Open the Agent and paste the starter prompt. You can also mention `@docs/thestatsapi-llms.txt` in chat to point it at the file.
Cursor hides git-ignored files from its AI. That keeps `.env` private, but it also means you must not git-ignore `docs/thestatsapi-llms.txt`.
1. Open **Project settings → Knowledge** and paste this (it's always in context):
```text Lovable knowledge wrap theme={null}
This app uses TheStatsAPI (football data REST API).
API reference (every endpoint, parameter and response field): https://api.thestatsapi.com/llms.txt
Only use endpoints, parameters and fields listed there.
Base URL https://api.thestatsapi.com/api. Auth header: Authorization: Bearer .
The key is the secret THESTATSAPI_API_KEY. Only server code (server function or Edge Function)
may call TheStatsAPI. Never put the key in frontend code, .env or a VITE_ variable.
Dates: date_from/date_to (YYYY-MM-DD), no "date" parameter. Upcoming fixtures:
status=scheduled&sort=utc_date. per_page max 100; read meta.total_pages.
400 UNKNOWN_PARAMETER = wrong parameter name. 429 = wait Retry-After. Cache responses.
```
2. Send your first prompt. [Open the starter prompt in Lovable](https://lovable.dev/#prompt=Build%20%5Bdescribe%20your%20app%5D.%20The%20football%20data%20comes%20from%20TheStatsAPI.%0A%0AThe%20full%20API%20reference%20is%20at%20https%3A%2F%2Fapi.thestatsapi.com%2Fllms.txt.%20Read%20it%20first%20and%20only%20use%20the%20endpoints%2C%20query%20parameters%20and%20response%20fields%20listed%20there.%0A%0ACall%20TheStatsAPI%20only%20from%20server-side%20code%20%28a%20server%20function%20or%20Edge%20Function%29%2C%20with%20the%20header%20Authorization%3A%20Bearer%20%3Ckey%3E.%20Store%20my%20API%20key%20as%20a%20secret%20named%20THESTATSAPI_API_KEY%20and%20ask%20me%20for%20it%20with%20the%20secure%20secret%20form.%20Never%20put%20it%20in%20frontend%20code%2C%20.env%20or%20a%20VITE_%20variable.%0A%0AStart%20with%20one%20server%20call%20that%20fetches%20Arsenal%27s%20next%20fixture%20%28GET%20https%3A%2F%2Fapi.thestatsapi.com%2Fapi%2Ffootball%2Fmatches%3Fteam_id%3Dtm_9145%26status%3Dscheduled%26sort%3Dutc_date%26per_page%3D1%29%20and%20show%20it%20on%20the%20page.): it opens with the prompt filled in but not sent, so replace `[describe your app]` first.
3. When Lovable asks for your key, enter it in the secure **Add secret** form, named `THESTATSAPI_API_KEY`. Don't type it into the chat.
Lovable secrets are only available to server code (server functions and Edge Functions) and never reach the browser. `VITE_` variables do reach the browser, and Lovable commits `.env`, so never put the key there.
**v0, Replit and other app builders:** paste the starter prompt and store the key in the tool's secrets or environment variable settings. Never use a `NEXT_PUBLIC_` or `VITE_` variable for it: those are bundled into browser code.
**Never put your API key in frontend code.** The API allows requests from browsers, so an AI tool's browser-side code will work, but anyone can then copy your key from the page. Keep the key in an environment variable or secret, and call TheStatsAPI only from your own server route. If a key leaks, create a new one on the [API keys page](https://www.thestatsapi.com/api-keys) and revoke the old one.
## Get live data in your AI chat (MCP)
Our MCP server lets your AI assistant look up live football data while you work: find an ID, check today's fixtures, or see what a response looks like. It doesn't replace `llms.txt` for writing code.
| Setting | Value |
| - | - |
| URL | `https://mcp.thestatsapi.com/mcp` |
| Transport | Streamable HTTP |
| Auth header | `Authorization: Bearer YOUR_API_KEY` |
It uses your own API key, so its calls count against your plan. Its tools are read-only and cover competitions, standings, teams, squads, players, injuries, matches, lineups, match and player stats, shot maps, heatmaps, referees and coverage. It doesn't include odds; use the REST API for those.
Export the key first, then add the server. If you don't export it, the shell expands `$THESTATSAPI_API_KEY` to nothing and the saved header is empty.
```bash theme={null}
export THESTATSAPI_API_KEY=YOUR_API_KEY
claude mcp add --transport http thestatsapi https://mcp.thestatsapi.com/mcp \
--header "Authorization: Bearer $THESTATSAPI_API_KEY"
```
Run `claude mcp list` to check it shows as connected.
Add this to `~/.cursor/mcp.json`. Use this global file, not the project's `.cursor/mcp.json`, so your key never ends up in your repo.
```json ~/.cursor/mcp.json theme={null}
{
"mcpServers": {
"thestatsapi": {
"url": "https://mcp.thestatsapi.com/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
```
Codex reads the key from an environment variable. Export it in the shell you start Codex from.
```bash theme={null}
export THESTATSAPI_API_KEY=YOUR_API_KEY
codex mcp add thestatsapi --url https://mcp.thestatsapi.com/mcp --bearer-token-env-var THESTATSAPI_API_KEY
```
This writes the following to `~/.codex/config.toml`, which you can also add by hand:
```toml ~/.codex/config.toml theme={null}
[mcp_servers.thestatsapi]
url = "https://mcp.thestatsapi.com/mcp"
bearer_token_env_var = "THESTATSAPI_API_KEY"
```
Add this to `.vscode/mcp.json`. VS Code asks for the key once and stores it securely, so the file is safe to commit.
```json .vscode/mcp.json theme={null}
{
"inputs": [
{
"type": "promptString",
"id": "thestatsapi-key",
"description": "TheStatsAPI API key",
"password": true
}
],
"servers": {
"thestatsapi": {
"type": "http",
"url": "https://mcp.thestatsapi.com/mcp",
"headers": { "Authorization": "Bearer ${input:thestatsapi-key}" }
}
}
}
```
`-s user` saves the server to your user settings, so the key stays out of your project.
```bash theme={null}
gemini mcp add -s user --transport http \
--header "Authorization: Bearer YOUR_API_KEY" \
thestatsapi https://mcp.thestatsapi.com/mcp
```
Windsurf and other clients that support streamable HTTP with a custom header work with the same URL and header. ChatGPT and Claude.ai connectors can't send an API key yet, so they're coming soon.
MCP tool arguments are not REST parameters. For example, the `find_matches` tool accepts `date`, but the REST endpoint `/football/matches` only accepts `date_from` and `date_to`. When your AI writes code, it should take names from `llms.txt`, not from the MCP tools.
## Prompt ideas
Add one of these after the starter prompt:
* **Live scores widget:** "Show today's Premier League matches with live scores. Use `/football/matches` with `date_from` and `date_to` set to today, and poll every 60 seconds only while a match is live."
* **League table page:** "Build a page with the current Premier League table. Get `current_season_id` from `/football/competitions/comp_3039`, then call the season's standings endpoint."
* **Fantasy points:** "For a finished match, get `/football/matches/{match_id}/player-stats` and score each player with my rules: 4 points a goal, 3 an assist, 1 for playing 60 minutes or more."
* **xG shot map:** "Draw a pitch with every shot from `/football/matches/{match_id}/shotmap`, sized by `expected_goals`. Only offer it for matches where `shotmap_available` is true."
* **Odds comparison:** "For this weekend's Premier League matches, show the best home, draw and away price across bookmakers from `/football/matches/{match_id}/odds`. Skip matches where `odds_available` is false."
## Tips for good results
* **Paste errors back in.** A `400 UNKNOWN_PARAMETER` message names the right parameter, for example: `Unknown query parameter 'date' (did you mean 'date_from' or 'date_to'?)`. AI tools can usually fix the call from that.
* **Give it real IDs.** Look them up in the [ID finder](https://www.thestatsapi.com/id-finder) in your dashboard, or let the AI use the `search=` endpoints. Don't let it make IDs up.
* **Watch your quota.** An agent stuck in a retry loop can use up your requests quickly, and trials get 10% of the plan's limits. Ask for caching. See [Rate limits](/docs/concepts/rate-limits).
* **Check coverage before you build a feature.** Not every competition has xG, player stats or odds. See [Coverage](/docs/concepts/coverage).
## More AI resources
| Resource | Link | Use it for |
| - | - | - |
| Full API reference | [api.thestatsapi.com/llms.txt](https://api.thestatsapi.com/llms.txt) | Every endpoint, parameter and response shape. Give this to your AI. |
| OpenAPI spec | [api.thestatsapi.com/openapi.json](https://api.thestatsapi.com/openapi.json) | Code generators, Postman and other OpenAPI tools |
| Docs index for AI | [thestatsapi.com/docs/llms.txt](https://www.thestatsapi.com/docs/llms.txt) | A list of these docs pages |
| All guides as text | [thestatsapi.com/docs/llms-full.txt](https://www.thestatsapi.com/docs/llms-full.txt) | The text of every docs page in one file |
| Any docs page as Markdown | Add `.md` to the page URL | Pasting a single guide into a chat |
| MCP server | `https://mcp.thestatsapi.com/mcp` | Live data inside your AI assistant |
## Next steps
Make your first requests by hand and see what comes back.
Look up the competition, season, team and player IDs your app needs.
What each error code means and how to fix it.
Every endpoint, with a playground to try requests.
# Data coverage
Source: https://www.thestatsapi.com/docs/concepts/coverage
Check which competitions, seasons and data types exist before you build, using the coverage endpoints and competition availability flags.
Use this page to check what data exists for a competition and season before you build on it.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), call GET /coverage/leagues/comp_8814 from the server and print a table of LaLiga seasons with coverage_pct for xg, player_stats and closing_odds. Skip seasons with status not_loaded.
```
Just want a quick look? Browse the [coverage page](https://www.thestatsapi.com/coverage).
## History depth varies
How far back data goes depends on the competition, and on the data type within it. Don't assume a fixed number of years. Check the competitions you need.
Some examples from the coverage endpoints:
| Competition | Seasons from |
| - | - |
| Premier League | 2014/15 |
| LaLiga, Bundesliga, Serie A, Ligue 1, UEFA Champions League | 2020/21 |
| MLS | 2020 |
Within one competition, data types start at different seasons. In the Premier League:
| Data type | Available from |
| - | - |
| Fixtures, lineups, team stats, player stats, standings | 2014/15 |
| Closing odds | 2015/16 |
| xG | 2022/23 |
| Opening odds | 2026/27 |
### Odds history
Odds history is thinner than match data:
* **Before the 2026/27 season (around August 2026)**, matches mostly have closing prices only (the last price before kickoff), mostly from Bet365.
* **From the 2026/27 season**, matches can also have opening prices, and prices from more bookmakers: Bet365, Paddy Power, BetMGM UK, Pinnacle and Betfair Exchange.
Check `opening_odds` and `closing_odds` in the coverage response for the exact seasons you need.
## The coverage endpoints
| Endpoint | Use it to |
| - | - |
| [`GET /coverage/summary`](/docs/api-reference/coverage/summary) | See headline totals: competitions, seasons and finished matches. |
| [`GET /coverage/leagues`](/docs/api-reference/coverage/leagues) | Find competitions that have a data type. Filter with `data_type` and `search`. |
| [`GET /coverage/leagues/{competition_id}`](/docs/api-reference/coverage/league) | See every season of one competition, with match counts and the percentage of matches that have each data type. |
## Check coverage before you build
Add `data_type` to list only competitions that have it, and `search` to filter by name.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/coverage/leagues?data_type=xg&search=Premier" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/coverage/leagues?data_type=xg&search=Premier",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
for (const league of data) {
console.log(league.id, league.name, league.seasons.first?.year, "to", league.seasons.last?.year);
}
```
Each row shows the competition, how many seasons are loaded, which data types it has and the state of its latest season (trimmed):
```json theme={null}
{
"id": "comp_3039",
"name": "Premier League",
"country": "England",
"seasons": {
"count": 13,
"first": { "id": "sn_612289", "year": "14/15" },
"last": { "id": "sn_8406098", "year": "26/27" }
},
"data_types": {
"fixtures": { "available": true },
"xg": { "available": true },
"opening_odds": { "available": true },
"closing_odds": { "available": true },
"player_stats": { "available": true }
},
"latest_season": {
"id": "sn_8406098",
"year": "26/27",
"status": "in_progress",
"finished_events": 50,
"total_events": 100
},
"total_finished_events": 4610
}
```
Ask for one competition to see coverage season by season.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/coverage/leagues/comp_3039" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch("https://api.thestatsapi.com/api/coverage/leagues/comp_3039", {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
const { data } = await res.json();
for (const season of data.seasons) {
if (season.status === "not_loaded") continue;
console.log(season.year, "xG:", season.data_types.xg.coverage_pct ?? 0, "%");
}
```
Each season shows its status, match counts, and for each data type how many finished matches have it (trimmed):
```json theme={null}
{
"id": "sn_6125938",
"name": "Premier League 25/26",
"year": "25/26",
"status": "complete",
"events": { "finished": 380, "total": 380 },
"data_types": {
"xg": { "available": true, "covered_events": 380, "coverage_pct": 100 },
"opening_odds": { "available": false, "covered_events": 0, "coverage_pct": 0 },
"closing_odds": { "available": true, "covered_events": 380, "coverage_pct": 100 },
"player_stats": { "available": true, "covered_events": 380, "coverage_pct": 100 },
"standings": { "available": true }
}
}
```
Coverage uses the same `sn_` season IDs as every other endpoint. Pass the one you picked to `/football/matches`, standings or stats.
## Data types
| Key | What it means | Where you get it |
| - | - | - |
| `fixtures` | Matches are loaded. | `/football/matches` |
| `team_stats` | Team statistics for each match. | `/football/matches/{match_id}/stats` |
| `xg` | Expected goals for the match. | `expected_goals` in `/football/matches/{match_id}/stats` |
| `player_stats` | Player statistics for each match. | `/football/matches/{match_id}/player-stats` |
| `lineups` | Starting lineups and formations. | `/football/matches/{match_id}/lineups` |
| `standings` | A league table exists for the season. | `/football/competitions/{competition_id}/seasons/{season_id}/standings` |
| `closing_odds` | The last pre-match prices. | `last_seen` in `/football/matches/{match_id}/odds` |
| `opening_odds` | The first prices captured. | `opening` in `/football/matches/{match_id}/odds` |
| `odds` | Same as `closing_odds`. Kept for older code. | |
Each data type has `available` (`true` or `false`). On `/coverage/leagues/{competition_id}`, most also have:
* `covered_events`: finished matches in the season that have this data.
* `coverage_pct`: those matches as a percentage of all finished matches, to one decimal place.
New data type keys may be added, so ignore keys your code doesn't recognise.
## Season status
| `status` | Meaning |
| - | - |
| `complete` | All loaded matches are finished. |
| `in_progress` | Matches are still to be played. |
| `not_loaded` | The season is known but has no matches loaded. |
## Good to know
* **Only finished matches are counted** in `covered_events`, `coverage_pct` and `finished_events`.
* **For a season in progress, `total` can be lower than the full season**, because fixtures are still being added.
* **Coverage responses are cached for several hours**, so the latest matches can take a while to show up in the counts.
* **`seasons.first` and `seasons.last` on `/coverage/leagues` are a quick guide.** For the exact list of seasons, use `/coverage/leagues/{competition_id}`. Seasons there aren't always in date order.
## Quick check with competition flags
Every competition also carries flags: `has_team_stats`, `has_player_stats`, `xg_available`, `odds_available` and `live_odds_available`. They tell you whether the data type exists anywhere in the competition, not which seasons have it. See [Data model](/docs/concepts/data-model#availability-flags).
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/competitions/comp_3039" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Next steps
* [Find IDs](/docs/guides/finding-ids): get the `competition_id` and `season_id` for the leagues you picked.
* [Odds](/docs/guides/odds): which bookmakers and markets a match has.
# Data model and IDs
Source: https://www.thestatsapi.com/docs/concepts/data-model
How competitions, seasons, matches, teams and players connect, what the ID prefixes mean, and how to read availability flags, dates and null values.
Use this page to understand how the API's objects fit together before you write code.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text theme={null}
Read the API reference for TheStatsAPI at https://api.thestatsapi.com/llms.txt. Write TypeScript types for the Competition, Season, Team, Player and Match objects, using only the fields listed there. Mark nullable fields as nullable and keep every ID as a string.
```
## How it fits together
Everything starts with a competition. A competition has seasons, and a season has matches. Teams and players appear in matches and in season stats.
| Object | What it is | How to get it |
| - | - | - |
| Competition | A league, cup or tournament, such as the Premier League. | `GET /football/competitions?search=Premier` |
| Season | One edition of a competition, such as Premier League 26/27. | `GET /football/competitions/{competition_id}/seasons` |
| Match | One fixture in a season, with teams, status and score. | `GET /football/matches?competition_id=…&season_id=…` |
| Team | A club or national team. | `GET /football/teams?search=Arsenal` |
| Player | A person, with their current team. | `GET /football/players?search=saka` |
What to send for common requests:
| You want | Send |
| - | - |
| A league table | `competition_id` and `season_id` in the path of [standings](/docs/api-reference/competitions/standings) |
| A team's season stats | `team_id` in the path, `season_id` in the query ([team stats](/docs/api-reference/teams/stats)) |
| A player's season stats | `player_id` in the path, `season_id` in the query, and optionally `competition_id` ([player stats](/docs/api-reference/players/stats)) |
| A team's current squad | `team_id` ([team players](/docs/api-reference/teams/players)) |
## IDs
Every ID is a string with a prefix that tells you what it points to.
| Prefix | Object | Example |
| - | - | - |
| `comp_` | Competition | `comp_3039` (Premier League) |
| `sn_` | Season | `sn_8406098` (Premier League 26/27) |
| `tm_` | Team | `tm_9145` (Arsenal men), `tm_515555` (Arsenal women) |
| `pl_` | Player | `pl_45126714` (Bukayo Saka) |
| `mt_` | Match | `mt_200199332` (Arsenal v Leeds United) |
| `ref_` | Referee | `ref_0890533` (Paul Tierney) |
| `mgr_` | Manager | `mgr_45779068` (Mikel Arteta) |
| `sh_` | Shot, in a shotmap | `sh_969103047` |
Treat IDs as text. Keep the prefix and never convert them to numbers: some have leading zeros, such as `tm_0256` (Leeds United). A malformed ID returns `400 BAD_REQUEST`, for example `Invalid match_id`.
Not sure of an ID? See [Find IDs](/docs/guides/finding-ids).
## Competitions
| Field | Meaning |
| - | - |
| `id`, `name` | For example `comp_3039`, `Premier League`. |
| `type` | `league`, `cup` or `tournament`. |
| `country`, `country_code` | For example `England`, `GB`. Both are `null` for international competitions. |
| `subdivision_code` | The UK nation, such as `GB-ENG` (England) or `GB-SCT` (Scotland). `null` outside the UK. |
| `confederation` | For international competitions, such as `UEFA` or `FIFA`. `null` for domestic ones. |
| `current_season_id` | The latest season. Returned by [Get a competition](/docs/api-reference/competitions/get). |
| Availability flags | See [Availability flags](#availability-flags). |
The `country_code` filter on `/football/competitions` accepts `GB` for all UK nations, or `GB-ENG`, `GB-SCT`, `GB-WLS` and `GB-NIR` for one nation. Other countries use ISO codes such as `DE` or `ES`.
## Seasons
| Field | Meaning |
| - | - |
| `id`, `name` | For example `sn_8406098`, `Premier League 26/27`. |
| `year` | Display label: `26/27` for seasons that span two years, `2026` for calendar-year seasons. |
| `start_year`, `end_year` | Numbers you can sort on, such as `2026` and `2027`. |
| `is_current` | `true` for the competition's latest season. |
## Teams
| Field | Meaning |
| - | - |
| `id`, `name`, `short_name` | For example `tm_9145`, `Arsenal`. |
| `country` | For example `England`. |
| `is_mens_team` | `true` for a men's team, `false` for a women's team, `null` when unknown. |
| `primary_competition` | The team's main competition, as `{ id, name }`, or `null`. |
A club's men's and women's teams can share a name. Arsenal men are `tm_9145` and Arsenal women are `tm_515555`. Use `is_mens_team` to tell them apart, or filter the list with `GET /football/teams?search=Arsenal&is_mens_team=false`.
## Players
| Field | Meaning |
| - | - |
| `id`, `name`, `first_name`, `last_name` | For example `pl_45126714`, `Bukayo Saka`. |
| `position` | `G`, `D`, `M`, `F`, or `""` when unknown. |
| `date_of_birth`, `age` | `YYYY-MM-DD` and whole years. `null` when unknown. |
| `nationality`, `height_cm` | `height_cm` is `null` when unknown. |
| `current_team` | `{ id, name, jersey_number }`. `jersey_number` is `null` when unknown. |
## Matches
| Field | Meaning |
| - | - |
| `id` | For example `mt_200199332`. |
| `competition_id`, `season_id` | Where the match belongs. |
| `status` | `scheduled`, `live`, `finished`, `postponed` or `cancelled`. |
| `utc_date` | Kickoff time in UTC. |
| `matchday` | League round number. Can be `null` for cup and knockout fixtures. |
| `stage_name` | Named round for cup and knockout fixtures, such as `quarter_final` or `final`. `null` for regular league fixtures. |
| `group_label` | Group letter for group-stage matches in tournaments such as the World Cup, otherwise `null`. |
| `home_team`, `away_team` | `{ id, name }`. |
| `score` | `home` and `away` are normal-time goals. For extra time and penalties, read `after_extra_time`, `penalty_shootout` and `winner` (the overall winner). |
| `is_neutral` | `true` when neither team is at home. `null` means unknown, not `false`. |
| `live` | The in-play clock, on [List matches](/docs/api-reference/matches/list) only. `null` unless `status` is `live`. |
| `home_manager`, `away_manager` | `{ id, name }`, or `null` when not recorded. |
| Availability flags | See below. |
Full field lists are on [Get a match](/docs/api-reference/matches/get) and [List matches](/docs/api-reference/matches/list).
## Availability flags
Some data is optional. Flags tell you whether it exists, so you can skip calls that would return nothing.
| Flag | Found on | `true` means |
| - | - | - |
| `has_team_stats` | Competition | Team statistics exist for this competition. |
| `has_player_stats` | Competition | Player statistics exist for this competition. |
| `xg_available` | Competition, match | Expected goals (xG) data exists. |
| `shotmap_available` | Match | The [shotmap](/docs/api-reference/matches/shotmap) has shots with xG values. |
| `xg_quality` | Match | `full` when shots carry per-shot xG, `none` when they don't. |
| `odds_available` | Competition, match | Pre-match [odds](/docs/api-reference/matches/odds) exist. |
| `live_odds_available` | Competition, match | In-play [live odds](/docs/api-reference/matches/live-odds) are available. |
Competition flags tell you whether a data type exists anywhere in that competition. They don't tell you which seasons have it. For a season-by-season answer, use the [coverage endpoints](/docs/concepts/coverage).
## Dates and times
* **Kickoff times** (`utc_date`) are ISO 8601 in UTC, for example `2026-10-10T11:30:00.000Z`. Convert to local time in your app.
* **Filtering by day** uses `date_from` and `date_to` in `YYYY-MM-DD` format. There is no `date` parameter. For one day, send the same date in both.
* **Local days:** add `utc_offset`, such as `-05:00`, to read `date_from` and `date_to` in another time zone. Kickoff times in the response stay in UTC. In a URL, write a plus sign as `%2B` (`utc_offset=%2B02:00`), or build the query with `URLSearchParams`.
* **Birth dates** (`date_of_birth`) are `YYYY-MM-DD`.
## Null values
`null` means the value is unknown or doesn't exist yet. It never means zero or `false`.
| You see | It means |
| - | - |
| `score.home: null` | The match hasn't been played. |
| `live: null` | The match isn't live right now. |
| `matchday: null` | Usually a cup or knockout fixture. Read `stage_name` instead. |
| `is_neutral: null` | Unknown. Don't assume a home advantage either way. |
| `height_cm: null` | Not recorded. |
When optional data doesn't exist for a match, some endpoints return `404` (for example `Heatmap not available for this player in this match`) and others return an empty `data` list. Check the availability flags first. See [Errors](/docs/concepts/errors).
## Next steps
* [Find IDs](/docs/guides/finding-ids): look up the IDs for your competitions, teams and players.
* [Data coverage](/docs/concepts/coverage): check which seasons have the data you need.
* [Pagination and sorting](/docs/concepts/pagination): fetch long lists in the right order.
# Errors
Source: https://www.thestatsapi.com/docs/concepts/errors
Every error code the API returns, when it happens and what to do about it, with real example responses and code for handling them.
Use this page to understand an error response and fix the request that caused it.
**Fastest way: ask your AI.** Error messages say what's wrong, and for a misspelled parameter they name the right one. Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste the request and the full error:
```text theme={null}
This TheStatsAPI request failed. Check it against https://api.thestatsapi.com/llms.txt and fix it. Request: . Error:
```
## The error format
Every error has the same shape. The HTTP status code is repeated in the body.
```json theme={null}
{
"error": {
"code": "NOT_FOUND",
"message": "Match not found",
"status_code": 404
}
}
```
| Field | Meaning |
| - | - |
| `code` | An UPPERCASE code. Use this in your logic. |
| `message` | What went wrong, in plain English. Log it and show it to your AI tool. |
| `status_code` | The HTTP status, such as `404`. |
## Error codes
| Status | Code | When it happens | What to do |
| - | - | - | - |
| 400 | `BAD_REQUEST` | A parameter is missing or invalid. For example `season_id is required`, `Invalid match_id` or `per_page cannot exceed 100`. | Fix the parameter named in `message`. |
| 400 | `UNKNOWN_PARAMETER` | You sent a query parameter the endpoint doesn't accept, such as `date` instead of `date_from`. | Use the name the message suggests. The message also lists every parameter the endpoint accepts. |
| 400 | `SEASON_COMPETITION_MISMATCH` | `season_id` belongs to a different competition from `competition_id`. | Get the right season from `GET /football/competitions/{competition_id}/seasons`. |
| 400 | `PAGE_OUT_OF_RANGE` | `page` is past the last page on `/football/matches`. | Stop at `meta.total_pages`. See [Pagination](/docs/concepts/pagination). |
| 401 | `UNAUTHORIZED` | The `Authorization` header is missing, isn't in `Bearer` format, or the key is invalid. | Send `Authorization: Bearer YOUR_API_KEY`. Copy your key from [API keys](https://www.thestatsapi.com/api-keys). |
| 403 | `FORBIDDEN` | The key is inactive or expired, or the account is inactive. | Check the key and your account on the [dashboard](https://www.thestatsapi.com/api-keys). |
| 403 | `KEY_REVOKED` | The key has no active plan. | Choose a plan on the [pricing page](https://www.thestatsapi.com/#pricing). |
| 403 | `ADDON_REQUIRED` | The endpoint needs an add-on your plan doesn't include. | See the [pricing page](https://www.thestatsapi.com/#pricing). |
| 404 | `NOT_FOUND` | The ID doesn't exist, the path is wrong (`Route not found`), or the data doesn't exist, such as `Heatmap not available for this player in this match`. | Check the ID and path. Check [availability flags](/docs/concepts/data-model#availability-flags) before calling optional endpoints. |
| 404 | `STATS_NOT_AVAILABLE_AT_SOURCE` | Detailed stats were never collected for this finished match. | Don't retry. This won't change for that match. |
| 405 | `METHOD_NOT_ALLOWED` | You used a method other than `GET`. The API is read-only. | Use `GET`. |
| 409 | `CONFLICT` | You called a live endpoint for a match that isn't live (`Match is not live`), or a post-match endpoint while the match is live. | Check the match `status`. Use the live endpoints during the match and the regular ones after it. |
| 409 | `MATCH_IS_LIVE` | You asked for full match stats while the match is still being played. | Use `/football/matches/{match_id}/live-stats` until full time. |
| 429 | `RATE_LIMITED` | You used up your per-minute limit. | Wait for the number of seconds in the `Retry-After` header, then retry. See [Rate limits](/docs/concepts/rate-limits). |
| 429 | `USAGE_LIMIT_EXCEEDED` | You used up your monthly quota. | Don't retry. The quota resets when your billing cycle rolls over, or you can upgrade your plan. |
| 500 | `INTERNAL_SERVER_ERROR` | Something went wrong on our side. | Retry after a short wait. |
## Examples
There is no `date` parameter on `/football/matches`, so this request fails:
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&date=2026-10-10" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json theme={null}
{
"error": {
"code": "UNKNOWN_PARAMETER",
"message": "Unknown query parameter 'date' (did you mean 'date_from' or 'date_to'?). Supported parameters: page, per_page, limit, competition_id, season_id, team_id, date_from, date_to, utc_offset, matchday, status, stage, group, sort.",
"status_code": 400
}
}
```
The fix: `?team_id=tm_9145&date_from=2026-10-10&date_to=2026-10-10`.
```json theme={null}
{
"error": {
"code": "SEASON_COMPETITION_MISMATCH",
"message": "season_id does not belong to competition_id",
"status_code": 400
}
}
```
No `Authorization` header:
```json theme={null}
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing authorization header",
"status_code": 401
}
}
```
Other messages you may see: `Invalid authorization format. Use: Bearer ` (the key was sent without `Bearer `) and `Invalid API key`.
```json theme={null}
{
"error": {
"code": "KEY_REVOKED",
"message": "API key has no active subscription plan",
"status_code": 403
}
}
```
```json theme={null}
{
"error": {
"code": "NOT_FOUND",
"message": "Match not found",
"status_code": 404
}
}
```
A misspelled path, such as `/football/matchez`, returns the same code with the message `Route not found`.
Calling `/football/matches/{match_id}/live-stats` for a finished match:
```json theme={null}
{
"error": {
"code": "CONFLICT",
"message": "Match is not live",
"status_code": 409
}
}
```
The response includes a `Retry-After` header with the seconds to wait.
```json theme={null}
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Please slow down your requests.",
"status_code": 429
}
}
```
## Handle errors in code
Check `code`, not just the HTTP status. Two errors can share a status but need different handling, such as `RATE_LIMITED` and `USAGE_LIMIT_EXCEEDED`.
```javascript JavaScript theme={null}
const BASE_URL = "https://api.thestatsapi.com/api";
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
class TheStatsApiError extends Error {
constructor(status, code, message) {
super(`${status} ${code}: ${message}`);
this.status = status;
this.code = code;
}
}
async function getJson(path) {
const res = await fetch(`${BASE_URL}${path}`, { headers });
const body = await res.json().catch(() => null);
if (!res.ok) {
const { code = "ERROR", message = res.statusText } = body?.error ?? {};
throw new TheStatsApiError(res.status, code, message);
}
return body;
}
// Returns null when the match doesn't exist, and rethrows anything else.
async function getMatch(matchId) {
try {
const { data } = await getJson(`/football/matches/${matchId}`);
return data;
} catch (err) {
if (err.code === "NOT_FOUND") return null;
throw err;
}
}
console.log(await getMatch("mt_200199332"));
```
```python Python theme={null}
import os
import requests
BASE_URL = "https://api.thestatsapi.com/api"
HEADERS = {"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"}
class TheStatsApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status = status
self.code = code
def get_json(path, params=None):
res = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params)
if not res.ok:
try:
error = res.json()["error"]
except (ValueError, KeyError):
error = {"code": "ERROR", "message": res.reason}
raise TheStatsApiError(res.status_code, error["code"], error["message"])
return res.json()
# Returns None when the match doesn't exist, and re-raises anything else.
def get_match(match_id):
try:
return get_json(f"/football/matches/{match_id}")["data"]
except TheStatsApiError as err:
if err.code == "NOT_FOUND":
return None
raise
print(get_match("mt_200199332"))
```
For `429` responses, wait and retry as shown in [Rate limits](/docs/concepts/rate-limits).
# Pagination and sorting
Source: https://www.thestatsapi.com/docs/concepts/pagination
Page through list endpoints with page and per_page, read the meta object, know what happens past the last page, and control the order of results.
Use this page to fetch long lists one page at a time and get results in the order you want.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), write a server-side helper that fetches every page of /football/matches for competition_id=comp_3039 and season_id=sn_8406098 with per_page=100, stopping at meta.total_pages.
```
## Which endpoints are paginated
| Endpoint | Default page size | Maximum |
| - | - | - |
| `GET /football/competitions` | 20 | 100 |
| `GET /football/teams` | 20 | 100 |
| `GET /football/players` | 20 | 100 |
| `GET /football/matches` | 20 | 100 |
| `GET /coverage/leagues` | 100 | 200 |
Other endpoints that return lists, such as seasons, squads, standings and timelines, return everything in one response. They don't accept `page` or `per_page`: sending them returns `400 UNKNOWN_PARAMETER`.
## Parameters
| Parameter | Default | What it does |
| - | - | - |
| `page` | `1` | The page to return, starting at 1. |
| `per_page` | `20` (`100` on `/coverage/leagues`) | Results per page. Over the maximum returns `400 BAD_REQUEST`, for example `per_page cannot exceed 100`. |
| `limit` | | Older name for `per_page`. Use `per_page` in new code. |
`page` and `per_page` must be positive whole numbers. `0` or a negative number returns `400 BAD_REQUEST`.
## The `meta` object
Paginated responses put results in `data` and page details in `meta`:
```json theme={null}
{
"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` | Results across all pages. |
| `total_pages` | Pages at this page size. `0` when there are no results. |
## Past the last page
| Endpoint | What you get |
| - | - |
| `/football/matches` | `400 PAGE_OUT_OF_RANGE`, for example `page 999 is out of range (total_pages=5)`. |
| All other paginated endpoints | `200` with an empty `data` array. `meta` still shows the real `total` and `total_pages`. |
Either way, stop when `page` reaches `meta.total_pages`.
## Loop through every page
Ask for `per_page=100` so you make fewer requests. Every page counts toward your [rate limits](/docs/concepts/rate-limits).
```javascript JavaScript theme={null}
const BASE_URL = "https://api.thestatsapi.com/api";
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
async function fetchAllPages(path, params = {}) {
const results = [];
let page = 1;
while (true) {
const query = new URLSearchParams({ ...params, page: String(page), per_page: "100" });
const res = await fetch(`${BASE_URL}${path}?${query}`, { headers });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const { data, meta } = await res.json();
results.push(...data);
if (page >= meta.total_pages) break;
page += 1;
}
return results;
}
const matches = await fetchAllPages("/football/matches", {
competition_id: "comp_3039",
season_id: "sn_8406098",
});
console.log(`Fetched ${matches.length} matches`);
```
```python Python theme={null}
import os
import requests
BASE_URL = "https://api.thestatsapi.com/api"
HEADERS = {"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"}
def fetch_all_pages(path, params=None):
results = []
page = 1
while True:
query = {**(params or {}), "page": page, "per_page": 100}
res = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=query)
res.raise_for_status()
body = res.json()
results.extend(body["data"])
if page >= body["meta"]["total_pages"]:
break
page += 1
return results
matches = fetch_all_pages(
"/football/matches",
{"competition_id": "comp_3039", "season_id": "sn_8406098"},
)
print(f"Fetched {len(matches)} matches")
```
Filter before you page. Narrowing `/football/matches` with `competition_id`, `season_id`, `team_id` or `date_from` and `date_to` is cheaper than paging through everything and filtering in your code.
## Default order and sorting
| Endpoint | Default order | Change it with |
| - | - | - |
| `/football/matches` | Newest kickoff first (`sort=-utc_date`). Upcoming fixtures come before results. | `sort=utc_date` for soonest first. |
| `/football/competitions`, `/football/teams`, `/football/players` | With `search`: best match first. Without `search`: A to Z by name. | `sort=name` for A to Z. |
On `/football/matches`, matches with the same kickoff time are ordered by match ID, so pages don't shuffle between requests.
Two common recipes:
```bash Next fixture theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash Latest result theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=finished&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Because the default order puts the furthest-away fixtures first, a plain `?team_id=tm_9145&per_page=1` returns a match weeks away, not the next one. Add `status` and `sort` as shown above.
## Next steps
* [Fixtures and results](/docs/guides/fixtures-and-results): more recipes for filtering and sorting matches.
* [Rate limits](/docs/concepts/rate-limits): every page is one request, so plan your budget.
* [Errors](/docs/concepts/errors): what `PAGE_OUT_OF_RANGE` and other codes mean.
# Rate limits and quotas
Source: https://www.thestatsapi.com/docs/concepts/rate-limits
How the per-minute limit and monthly quota work, how to read the rate-limit headers, and how to handle 429 responses with Retry-After.
Use this page to keep your app inside your plan's limits and recover cleanly when you hit one.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), write a server-side fetch wrapper that caches responses, and on a 429 with code RATE_LIMITED waits the Retry-After seconds and retries up to 3 times. On USAGE_LIMIT_EXCEEDED it must stop and report the error, not retry.
```
## Two budgets
Every API key works within two separate budgets.
| Budget | Resets | Counted per | When it runs out |
| - | - | - | - |
| Per-minute rate limit | At the start of each minute | API key | `429 RATE_LIMITED` |
| Monthly quota | When your billing cycle rolls over | Account | `429 USAGE_LIMIT_EXCEEDED` |
The numbers depend on your plan. Some plans have no monthly cap. See [pricing](https://www.thestatsapi.com/#pricing).
* **Trials** are metered at 10% of the plan's limits.
* **Every request counts once your key is accepted**, including requests that return an error such as `400` or `404`.
* **`GET /health`** needs no key and doesn't count.
## Read the headers
Every authenticated response, success or error, includes these headers:
| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left this minute, counting this one. `0` means your next request is rejected. |
| `X-RateLimit-Reset` | Unix time, in seconds, when the minute resets. |
| `X-Monthly-Quota-Limit` | Requests allowed in this billing cycle. `-1` means no monthly cap. |
| `X-Monthly-Quota-Remaining` | Requests left in this billing cycle, counting this one. `-1` means no monthly cap. |
| `X-Monthly-Quota-Reset` | Unix time, in seconds, when your billing cycle rolls over. |
| `Retry-After` | Only on `429` responses. Seconds to wait before retrying. |
To see them, add `-i` to any request:
```bash curl theme={null}
curl -i "https://api.thestatsapi.com/api/football/competitions?per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch("https://api.thestatsapi.com/api/football/competitions?per_page=1", {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
console.log({
perMinuteLeft: res.headers.get("X-RateLimit-Remaining"),
monthlyLeft: res.headers.get("X-Monthly-Quota-Remaining"), // "-1" = no monthly cap
});
```
## When you hit a limit
Both limits return `429` with a `Retry-After` header. Check the error `code` to know which one you hit.
| Code | What `Retry-After` holds | What to do |
| - | - | - |
| `RATE_LIMITED` | Seconds until the current minute resets, from 1 to 60. | Wait that long, then retry. |
| `USAGE_LIMIT_EXCEEDED` | Seconds until your billing cycle rolls over. This is days, not seconds. | Don't retry in a loop: every retry gets another `429`. Upgrade on the [pricing page](https://www.thestatsapi.com/#pricing) or wait for the new cycle. |
```json theme={null}
{
"error": {
"code": "USAGE_LIMIT_EXCEEDED",
"message": "Monthly usage limit exceeded. Please upgrade your plan.",
"status_code": 429
}
}
```
## Retry with backoff
This wrapper retries `RATE_LIMITED` after `Retry-After` seconds and gives up straight away on `USAGE_LIMIT_EXCEEDED`.
```javascript JavaScript theme={null}
const BASE_URL = "https://api.thestatsapi.com/api";
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function getWithRetry(path, maxRetries = 3) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(`${BASE_URL}${path}`, { headers });
const body = await res.json();
if (res.ok) return body;
const { code, message } = body.error;
const canRetry = code === "RATE_LIMITED" && attempt < maxRetries;
if (!canRetry) throw new Error(`${res.status} ${code}: ${message}`);
const waitSeconds = Number(res.headers.get("Retry-After")) || 1;
await sleep(waitSeconds * 1000);
}
}
const { data } = await getWithRetry("/football/competitions/comp_3039");
console.log(data.name);
```
```python Python theme={null}
import os
import time
import requests
BASE_URL = "https://api.thestatsapi.com/api"
HEADERS = {"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"}
def get_with_retry(path, params=None, max_retries=3):
attempt = 0
while True:
res = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params)
body = res.json()
if res.ok:
return body
error = body["error"]
can_retry = error["code"] == "RATE_LIMITED" and attempt < max_retries
if not can_retry:
raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")
time.sleep(int(res.headers.get("Retry-After", "1")))
attempt += 1
data = get_with_retry("/football/competitions/comp_3039")["data"]
print(data["name"])
```
To avoid the `429` in the first place, check `X-RateLimit-Remaining`. When it reaches `0`, wait until `X-RateLimit-Reset` before the next request.
## Use fewer requests
* **Call the API from your server and cache the responses.** One cached response can serve all your users. Calling from the browser also exposes your key.
* **Cache slow-changing data for longer**, such as competitions, seasons and finished matches.
* **Poll live endpoints only while a match is live**, and stop at full time. The API has no webhooks or push updates, so polling is the only way to get new data.
* **Fetch 100 results per page** (`per_page=100`) and filter on the server with parameters like `team_id`, `date_from` and `date_to`. See [Pagination](/docs/concepts/pagination).
* **Check availability flags** such as `xg_available` and `odds_available` before calling optional endpoints. See [Data model](/docs/concepts/data-model#availability-flags).
AI coding agents can use up quota fast when they run code in a loop. Add a line like this to your prompt: "Make one test request first, cache every response while you build, and never call TheStatsAPI in a tight loop."
# Find competition, team, player and match IDs
Source: https://www.thestatsapi.com/docs/guides/finding-ids
Look up the comp_, sn_, tm_, pl_ and mt_ IDs every other request needs, using search, season lists and the is_mens_team filter.
Use this page to find the IDs that every other endpoint needs.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text Prompt wrap theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), write a server-side function that looks up the team_id for Arsenal's men's team and the current Premier League season_id, then prints both.
```
## How IDs work
IDs are strings with a prefix that tells you what they point to. You look them up by name with the requests below, then pass them to other endpoints.
| Prefix | What it is | Example |
| - | - | - |
| `comp_` | Competition | `comp_3039` (Premier League) |
| `sn_` | Season of a competition | `sn_8406098` (Premier League 26/27) |
| `tm_` | Team | `tm_9145` (Arsenal) |
| `pl_` | Player | `pl_45126714` (Bukayo Saka) |
| `mt_` | Match | `mt_200199332` (Arsenal v Leeds United) |
Just need one ID? Use the [ID finder](https://www.thestatsapi.com/id-finder) in your dashboard (you need to be logged in).
The JavaScript examples run on your server and read your key from the `THESTATSAPI_API_KEY` environment variable.
## Find a competition
Search by name. The best match comes first. Add `sort=name` if you want A to Z instead.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/competitions?search=Premier&per_page=3" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/competitions?search=Premier&per_page=3",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
console.log(data[0].id); // "comp_3039"
```
Response (trimmed):
```json Response theme={null}
{
"data": [
{
"id": "comp_3039",
"name": "Premier League",
"country": "England",
"country_code": "GB",
"subdivision_code": "GB-ENG",
"type": "league",
"odds_available": true,
"xg_available": true
},
{
"id": "comp_84287",
"name": "Egyptian Premier League",
"country": "Egypt",
"country_code": "EG",
"type": "league"
}
],
"meta": { "page": 1, "per_page": 3, "total": 11, "total_pages": 4 }
}
```
You can also filter instead of searching:
* `country_code=GB-ENG&type=league` lists English leagues. `GB` returns every UK nation; `GB-ENG`, `GB-SCT`, `GB-WLS` and `GB-NIR` return one. Other countries use codes such as `DE` or `ES`.
* `country=Spain` matches the country name.
## Find the current season and past seasons
Fetch the competition to get `current_season_id`.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/competitions/comp_3039" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/competitions/comp_3039",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
console.log(data.current_season_id); // "sn_8406098"
```
```json Response theme={null}
{
"data": {
"id": "comp_3039",
"name": "Premier League",
"type": "league",
"has_team_stats": true,
"has_player_stats": true,
"odds_available": true,
"xg_available": true,
"current_season_id": "sn_8406098",
"total_teams": 20
}
}
```
For older seasons, list them. They come back newest first, and the current one has `is_current: true`.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/competitions/comp_3039/seasons" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/competitions/comp_3039/seasons",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
const lastSeason = data.find((s) => s.year === "25/26");
```
```json Response theme={null}
{
"data": [
{ "id": "sn_8406098", "name": "Premier League 26/27", "year": "26/27", "start_year": 2026, "end_year": 2027, "is_current": true },
{ "id": "sn_6125938", "name": "Premier League 25/26", "year": "25/26", "start_year": 2025, "end_year": 2026, "is_current": false }
]
}
```
A season ID belongs to one competition. If you send a `season_id` with a different `competition_id`, you get a `400` error: "season\_id does not belong to competition\_id".
## Find a team (men's or women's)
Search by name. A club's men's and women's teams often share a name, so check `is_mens_team`.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/teams?search=Arsenal&per_page=5" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/teams?search=Arsenal&per_page=5",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
```
Response (trimmed):
```json Response theme={null}
{
"data": [
{
"id": "tm_9145",
"name": "Arsenal",
"country": "England",
"is_mens_team": true,
"primary_competition": { "id": "comp_3039", "name": "Premier League" }
},
{
"id": "tm_515555",
"name": "Arsenal",
"country": "England",
"is_mens_team": false,
"primary_competition": { "id": "comp_08207", "name": "Women's Super League" }
},
{
"id": "tm_033813",
"name": "Arsenal U21",
"country": "England",
"is_mens_team": true,
"primary_competition": { "id": "comp_0488", "name": "Football League Trophy" }
}
],
"meta": { "page": 1, "per_page": 5, "total": 15, "total_pages": 3 }
}
```
To get one side only, add `is_mens_team=true` (men) or `is_mens_team=false` (women). On a team, `is_mens_team` is `null` when it isn't known.
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/teams?search=Arsenal&is_mens_team=true&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
To list every team in a season, filter by competition and season instead: `/football/teams?competition_id=comp_3039&season_id=sn_8406098` returns the 20 Premier League teams.
## Find a player
Search needs at least 3 characters. It ignores case and accents, so `odegaard` finds "Ødegaard".
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/players?search=saka&per_page=2" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/players?search=saka&per_page=2",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
console.log(data[0].id); // "pl_45126714"
```
Response (trimmed):
```json Response theme={null}
{
"data": [
{
"id": "pl_45126714",
"name": "Bukayo Saka",
"position": "F",
"date_of_birth": "2001-09-05",
"nationality": "England",
"current_team": { "id": "tm_9145", "name": "Arsenal", "jersey_number": 7 }
}
],
"meta": { "page": 1, "per_page": 2, "total": 157, "total_pages": 79 }
}
```
Other ways to find players:
* `/football/players?team_id=tm_9145` lists a team's current players. Add `position=F` (or `G`, `D`, `M`) to narrow it.
* `/football/teams/tm_9145/players` returns the squad with extra profile fields.
* `/football/players?player_ids=pl_45126714,pl_29627593` fetches several players in one call.
## Find a match
Match IDs come from `/football/matches`. Filter by team, competition, season, date or status. For example, a team's next fixture:
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
See [Fixtures and results](/docs/guides/fixtures-and-results) for every filter.
## API reference
* [List competitions](/docs/api-reference/competitions/list) and [Get a competition](/docs/api-reference/competitions/get)
* [List a competition's seasons](/docs/api-reference/competitions/seasons)
* [List teams](/docs/api-reference/teams/list) and [Get a team's squad](/docs/api-reference/teams/players)
* [List players](/docs/api-reference/players/list)
* [List matches](/docs/api-reference/matches/list)
# Get fixtures and results
Source: https://www.thestatsapi.com/docs/guides/fixtures-and-results
Get a team's next fixture, latest results, one day's matches, a matchday or a tournament group from /football/matches, with date ranges, time zones and sort order.
Use `/football/matches` to get upcoming fixtures and finished results for a team, competition, date range or matchday.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text Prompt wrap theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), build a server route that returns Arsenal's next 5 fixtures and last 5 results with scores. Keep the API key server-side.
```
## The filters you'll use most
| Parameter | What it does |
| - | - |
| `team_id` | Matches this team plays, home or away. |
| `competition_id`, `season_id` | Matches in a competition or season. The season must belong to the competition. |
| `status` | `scheduled`, `live`, `finished`, `postponed` or `cancelled`. |
| `date_from`, `date_to` | Kickoff date range, `YYYY-MM-DD`. There is no `date` parameter. |
| `utc_offset` | Time zone for reading the dates, e.g. `-05:00`. Defaults to UTC. |
| `sort` | `-utc_date` (default) is newest first. `utc_date` is soonest first. |
| `matchday`, `stage`, `group` | A league round, regular season vs play-offs, or a tournament group. |
| `page`, `per_page` | 20 per page by default, 100 at most. |
The JavaScript examples run on your server and read your key from the `THESTATSAPI_API_KEY` environment variable.
## Next fixture for a team
Ask for scheduled matches, soonest first, and take the first one.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const params = new URLSearchParams({
team_id: "tm_9145",
status: "scheduled",
sort: "utc_date",
per_page: "1",
});
const res = await fetch(`https://api.thestatsapi.com/api/football/matches?${params}`, {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
const { data } = await res.json();
const next = data[0];
console.log(`${next.home_team.name} v ${next.away_team.name}, ${next.utc_date}`);
```
Response (trimmed):
```json Response theme={null}
{
"data": [
{
"id": "mt_200199332",
"competition_id": "comp_3039",
"season_id": "sn_8406098",
"matchday": 6,
"status": "scheduled",
"utc_date": "2026-10-10T11:30:00.000Z",
"home_team": { "id": "tm_9145", "name": "Arsenal" },
"away_team": { "id": "tm_0256", "name": "Leeds United" },
"score": { "home": null, "away": null, "winner": null },
"odds_available": true,
"live_odds_available": false
}
],
"meta": { "page": 1, "per_page": 1, "total": 9, "total_pages": 9 }
}
```
Use `per_page=5` for the next five. Kickoff times (`utc_date`) are always UTC.
Without `sort=utc_date` you get the newest kickoff first, which for scheduled matches is the one furthest in the future.
## Latest results
Results come newest first by default, so you don't need `sort`.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=finished&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=finished&per_page=1",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
const last = data[0];
console.log(`${last.home_team.name} ${last.score.home}-${last.score.away} ${last.away_team.name}`);
```
Response (trimmed):
```json Response theme={null}
{
"data": [
{
"id": "mt_155834755",
"status": "finished",
"utc_date": "2026-09-19T14:00:00.000Z",
"home_team": { "id": "tm_1552", "name": "Brighton & Hove Albion" },
"away_team": { "id": "tm_9145", "name": "Arsenal" },
"score": {
"home": 3,
"away": 0,
"regulation": { "home": 3, "away": 0 },
"went_to_extra_time": false,
"went_to_penalties": false,
"winner": "home"
},
"xg_available": true,
"shotmap_available": true
}
],
"meta": { "page": 1, "per_page": 1, "total": 561, "total_pages": 561 }
}
```
`score.winner` is `home`, `away` or `draw` once the match has finished, and counts extra time and penalties. `after_extra_time` and `penalty_shootout` are filled only when a match went that far.
## One day's matches in your time zone
Set `date_from` and `date_to` to the same day. Add `utc_offset` so "a day" means your local day, not the UTC one.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches?competition_id=comp_3039&date_from=2026-10-10&date_to=2026-10-10&utc_offset=%2B10:00&sort=utc_date" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const params = new URLSearchParams({
competition_id: "comp_3039",
date_from: "2026-10-10",
date_to: "2026-10-10",
utc_offset: "+10:00", // URLSearchParams encodes "+" as %2B for you
sort: "utc_date",
});
const res = await fetch(`https://api.thestatsapi.com/api/football/matches?${params}`, {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
const { data } = await res.json();
```
In UTC, the Premier League has 6 matches on 10 October 2026. At `+10:00` that day runs from 14:00 UTC on 9 October to 14:00 UTC on 10 October, so only Arsenal v Leeds United (11:30 UTC) comes back.
Encode `+` as `%2B` in a raw URL. A bare `+` reads as a space and returns `400 BAD_REQUEST` ("Invalid utc\_offset"). Negative offsets such as `-05:00` need no encoding.
If you send `date` by mistake, the API tells you what to use instead:
```json theme={null}
{
"error": {
"code": "UNKNOWN_PARAMETER",
"message": "Unknown query parameter 'date' (did you mean 'date_from' or 'date_to'?). Supported parameters: page, per_page, limit, competition_id, season_id, team_id, date_from, date_to, utc_offset, matchday, status, stage, group, sort.",
"status_code": 400
}
}
```
## A matchday, a group or a knockout round
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/matches?competition_id=comp_3039&season_id=sn_8406098&matchday=6" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Returns the 10 matches of Premier League 26/27 matchday 6.
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/matches?competition_id=comp_6107&season_id=sn_326766&group=A&sort=utc_date" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Returns the 6 Group A matches of the 2022 World Cup. `group` needs `competition_id`, otherwise you get 400. Get the group letters from [List groups in a season](/docs/api-reference/competitions/groups).
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/matches?competition_id=comp_6107&season_id=sn_326766&stage=playoff" \
-H "Authorization: Bearer YOUR_API_KEY"
```
`stage=playoff` returns knockout and play-off matches only (16 for the 2022 World Cup). `stage=regular` returns league and group matches only.
## Check which fixtures are loaded
A season's later rounds may not be loaded yet, so don't assume every fixture of the season is there. To see the furthest-ahead fixture that is loaded, ask for scheduled matches newest first:
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/matches?competition_id=comp_3039&season_id=sn_8406098&status=scheduled&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The first row is the last loaded fixture, and `meta.total` is how many scheduled matches are loaded. [Get coverage for a competition](/docs/api-reference/coverage/league) shows finished and total matches per season. See [Coverage](/docs/concepts/coverage) for more.
## Get every page
Lists return 20 rows per page by default and 100 at most (`per_page=101` returns 400). Read `meta.total_pages` and request each page. Matches with the same kickoff are ordered by match ID, so pages stay stable.
```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const all = [];
for (let page = 1; ; page++) {
const url = `https://api.thestatsapi.com/api/football/matches?competition_id=comp_3039&season_id=sn_8406098&per_page=100&page=${page}`;
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`Page ${page} failed with ${res.status}`);
const { data, meta } = await res.json();
all.push(...data);
if (page >= meta.total_pages) break;
}
```
Each page is one request against your [rate limits](/docs/concepts/rate-limits), so cache results you don't need fresh.
## API reference
* [List matches](/docs/api-reference/matches/list): every filter and field
* [Get a match](/docs/api-reference/matches/get): venue, referee, half-time score
* [List groups in a season](/docs/api-reference/competitions/groups)
* [Get coverage for a competition](/docs/api-reference/coverage/league)
# Build live scores and in-play stats
Source: https://www.thestatsapi.com/docs/guides/live-matches
Find live matches, then poll live stats, the live timeline, live player stats and in-play odds. Covers polling frequency and what each endpoint returns after full time.
Use this page to show scores, stats and events while matches are being played.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text Prompt wrap theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), build a live scores page: a server route lists matches with status=live, and for a selected match polls /live-stats every 30 seconds until it stops being live. Keep the API key server-side.
```
## How live data works
There are no webhooks, WebSockets or push messages. You poll: ask for live matches, then call the live endpoints for the matches you show.
| You want | Call |
| - | - |
| Which matches are live, with the clock | `GET /football/matches?status=live` |
| Score and team stats | `GET /football/matches/{match_id}/live-stats` |
| Goals, cards, substitutions and other events | `GET /football/matches/{match_id}/live-timeline` |
| Per-player stats | `GET /football/matches/{match_id}/live-player-stats` |
| In-play odds | `GET /football/matches/{match_id}/odds/live` (see [Odds](/docs/guides/odds)) |
The JavaScript examples run on your server and read your key from the `THESTATSAPI_API_KEY` environment variable. The match IDs below were live when this page was written; use one that is live when you try it.
## 1. Find live matches
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches?status=live" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches?status=live",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
for (const m of data) {
const minute = m.live?.elapsed_minutes ?? "-";
console.log(`${minute}' ${m.home_team.name} ${m.score.home}-${m.score.away} ${m.away_team.name}`);
}
```
Response (one match, trimmed):
```json Response theme={null}
{
"data": [
{
"id": "mt_899703080",
"competition_id": "comp_8321",
"status": "live",
"utc_date": "2026-10-09T19:00:00.000Z",
"home_team": { "id": "tm_3809", "name": "West Ham United" },
"away_team": { "id": "tm_2949", "name": "Queens Park Rangers" },
"score": { "home": 0, "away": 1 },
"live": {
"match_status": "first_half",
"elapsed_minutes": 29,
"period": "first_half",
"updated_at": "2026-10-09T19:29:14.093Z"
},
"live_odds_available": true
}
],
"meta": { "page": 1, "per_page": 20, "total": 45, "total_pages": 3 }
}
```
`live` is `null` unless `status` is `live`. Just after kickoff, every field inside `live` can be `null` until the first clock update arrives. On a busy evening there can be dozens of live matches, so add `per_page=100`, or filter with `competition_id` or `team_id` to watch only the matches you care about.
## 2. Poll live stats
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches/mt_899703080/live-stats" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const url = "https://api.thestatsapi.com/api/football/matches/mt_899703080/live-stats";
async function poll() {
const res = await fetch(url, { headers });
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? 60);
return setTimeout(poll, wait * 1000);
}
if (!res.ok) return; // 409: the match isn't live (any more). Use /stats for final totals
const { data } = await res.json();
console.log(data.meta.elapsed_minutes, data.meta.home_goals, data.meta.away_goals);
if (data.meta.match_status === "finished") return;
setTimeout(poll, 30_000);
}
poll();
```
Response (trimmed):
```json Response theme={null}
{
"data": {
"match_id": "mt_899703080",
"meta": {
"match_status": "first_half",
"elapsed_minutes": 29,
"home_goals": 0,
"away_goals": 1,
"period": "first_half"
},
"stats": {
"ball_possession": { "all": { "home": 55, "away": 45 } },
"total_shots": { "all": { "home": 1, "away": 2 } },
"shots_on_target": { "all": { "home": 0, "away": 1 } },
"expected_goals": { "all": { "home": 0.06, "away": 0.85 } }
}
}
}
```
Which stats appear depends on the match. Check that a key exists before you read it.
## 3. Poll the live timeline
Filter with `event_type` (e.g. `goal`, `penalty_scored`, `yellow_card`, `substitution`), `period` or `team_id`.
Penalty goals are `penalty_scored` events, not `goal`. To get every goal, ask for both: `event_type=goal,penalty_scored`. Penalty shoot-out kicks are also `penalty_scored`, with `"period": "penalties"`, so leave those out when you count match goals.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches/mt_899703080/live-timeline?event_type=goal,penalty_scored,yellow_card" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches/mt_899703080/live-timeline?event_type=goal,penalty_scored,yellow_card",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data, meta } = await res.json();
for (const e of data.events) console.log(`${e.minute}' ${e.type} ${e.player?.name ?? ""}`);
```
Response (trimmed):
```json Response theme={null}
{
"data": {
"match_id": "mt_899703080",
"coverage": "full",
"events": [
{
"sequence": 1,
"minute": 17,
"extra_time": 0,
"period": "first_half",
"type": "yellow_card",
"team": { "id": "tm_3809", "name": "West Ham United" },
"player": { "id": "pl_30432621", "name": "Arne Engels" }
},
{
"sequence": 2,
"minute": 18,
"extra_time": 0,
"period": "first_half",
"type": "penalty_scored",
"team": { "id": "tm_2949", "name": "Queens Park Rangers" },
"player": { "id": "pl_61568182", "name": "Nicolas Madsen" }
}
]
},
"meta": {
"total": 6,
"coverage": "full",
"last_updated": "2026-10-09T20:30:03.372Z",
"match_status": "live"
}
}
```
Stoppage time is kept separate: 45+3' comes back as `"minute": 45, "extra_time": 3`.
## 4. Live player stats
`/live-player-stats` returns one entry per player with rating, minutes, passing, shooting (including xG), duels and defending. Touches, fouls and cards are under `general`. Add `player_ids=pl_...,pl_...` to get only some players.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches/mt_899703080/live-player-stats" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches/mt_899703080/live-player-stats",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data, meta } = await res.json(); // meta.match_status === "live"
```
One entry (trimmed):
```json Response theme={null}
{
"player_id": "pl_45450148",
"player_name": "Ronnie Edwards",
"team_id": "tm_2949",
"position": "M",
"rating": 6.84,
"started": true,
"minutes_played": 25,
"passing": { "total_passes": 7, "accurate_passes": 7, "key_passes": 0 },
"shooting": { "goals": 0, "total_shots": 0, "expected_assists": 0.01 }
}
```
## How often to poll
* Every 30 seconds is enough. Live stats responses can be cached for up to 30 seconds, so polling faster doesn't get you fresher data.
* Only poll matches that are live and on screen. Every call counts against your [rate limits and quota](/docs/concepts/rate-limits).
* On `429`, wait the number of seconds in the `Retry-After` header before trying again.
* Refresh the live match list every minute or two to pick up kickoffs and full-time whistles.
## Before kickoff and after full time
| Endpoint | Before kickoff | While live | After full time |
| - | - | - | - |
| `/live-stats` | `409` | `200` | `200` with `meta.match_status: "finished"` for up to 5 minutes, then `409` |
| `/live-timeline`, `/live-player-stats` | `409` | `200` | `409` |
| `/odds/live` | `404` | `200` when `live_odds_available` is `true` | `404` ("Match is already completed") |
| `/stats`, `/player-stats`, `/timeline` | No data yet: `/stats` and `/player-stats` return `404`, `/timeline` returns an empty `events` list | `409` (code `MATCH_IS_LIVE` on `/stats`) | `200` once the stats are in |
The `409` for a match that isn't live looks like this:
```json theme={null}
{ "error": { "code": "CONFLICT", "message": "Match is not live", "status_code": 409 } }
```
When you get it after a match, switch to the post-match endpoints: [match stats](/docs/api-reference/matches/stats), [player stats](/docs/api-reference/matches/player-stats) and [timeline](/docs/api-reference/matches/timeline).
Lineups come before kickoff: [Get match lineups](/docs/api-reference/matches/lineups) returns a predicted XI, then the official team sheet once it's announced (about an hour before kickoff). Check `type` and `confirmed` to know which you have.
## API reference
* [List matches](/docs/api-reference/matches/list) (`status=live`)
* [Get live match stats](/docs/api-reference/matches/live-stats)
* [Get live match timeline](/docs/api-reference/matches/live-timeline)
* [Get live match player stats](/docs/api-reference/matches/live-player-stats)
* [Get live match odds](/docs/api-reference/matches/live-odds)
# Get pre-match, live and player-prop odds
Source: https://www.thestatsapi.com/docs/guides/odds
Get pre-match odds with opening and latest prices, in-play odds and player-prop odds for a match, filter by bookmaker, and check odds coverage per season.
Use this page to get betting odds for a match: before kickoff, in play, and for player props.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text Prompt wrap theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), build a server route that takes a match_id, checks odds_available on the match, and returns the match result odds (home, draw, away) from each bookmaker with opening and latest prices. Keep the API key server-side.
```
## Bookmakers
| Bookmaker | `bookmaker` slug |
| - | - |
| Bet365 | `bet365` |
| Paddy Power | `paddy-power` |
| BetMGM UK | `betmgm-uk` |
| Pinnacle | `pinnacle` |
| Betfair Exchange | `betfair-exchange` |
On pre-match and player-prop odds, pass one or more slugs, comma-separated, to filter (`?bookmaker=bet365,pinnacle`). Leave it out to get every bookmaker. An unknown slug returns `400`. Live odds come from Bet365, Paddy Power, BetMGM UK and Betfair Exchange.
Prices are decimal odds only; convert them yourself if you need fractional or American. Pre-match and live prices are strings such as `"1.380"`; player-prop prices are numbers.
## Check the flags first
| Flag | On | Meaning |
| - | - | - |
| `odds_available` | Match | The match has pre-match odds. Always `false` for postponed and cancelled matches. |
| `live_odds_available` | Match | The match has in-play odds right now. |
| `odds_available` | Competition | The competition's matches have odds. |
| `live_odds_available` | Competition | At least one match in the competition has in-play odds right now. |
`/odds` and `/odds/live` return `404` when the match has no odds of that kind.
The JavaScript examples run on your server and read your key from the `THESTATSAPI_API_KEY` environment variable.
## Pre-match odds
Each selection has an `opening` price (the first price seen before kickoff, or `null` if none was recorded) and a `last_seen` price (the latest one).
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches/mt_200199332/odds?bookmaker=bet365,pinnacle" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches/mt_200199332/odds?bookmaker=bet365,pinnacle",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
for (const b of data.bookmakers) {
const { home, draw, away } = b.markets.match_odds ?? {};
console.log(b.bookmaker, home?.last_seen, draw?.last_seen, away?.last_seen);
}
```
Response (Arsenal v Leeds United, trimmed):
```json Response theme={null}
{
"data": {
"match_id": "mt_200199332",
"bookmakers": [
{
"bookmaker": "Bet365",
"markets": {
"match_odds": {
"home": { "opening": "1.380", "last_seen": "1.380" },
"draw": { "opening": "4.500", "last_seen": "5.000" },
"away": { "opening": "7.500", "last_seen": "7.500" }
},
"btts": {
"yes": { "opening": "2.000", "last_seen": "2.050" },
"no": { "opening": "1.750", "last_seen": "1.700" }
},
"total_goals": {
"2.5": {
"over": { "opening": "1.725", "last_seen": "1.800" },
"under": { "opening": "2.075", "last_seen": "2.000" }
}
}
}
},
{
"bookmaker": "Pinnacle",
"markets": {
"match_odds": {
"home": { "opening": "1.377", "last_seen": "1.413" },
"draw": { "opening": "4.910", "last_seen": "4.870" },
"away": { "opening": "7.620", "last_seen": "7.760" }
}
}
}
]
}
}
```
Markets include match result (full time and half time), both teams to score, goals, corners and cards lines, Asian and European handicaps, team totals, shots, correct score, to qualify and penalty specials. A market only appears when that bookmaker prices it, so check that a key exists before you read it. The endpoint works for upcoming and finished matches.
## Live (in-play) odds
Poll this during the match when `live_odds_available` is `true`. Bookmakers and markets come and go as books suspend and reopen.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches/mt_899703080/odds/live" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches/mt_899703080/odds/live",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
if (res.status === 404) {
console.log("No live odds for this match");
} else {
const { data } = await res.json();
for (const b of data.bookmakers) console.log(b.bookmaker, b.markets.match_odds?.home?.live);
}
```
Response (West Ham United v Queens Park Rangers, in play, trimmed):
```json Response theme={null}
{
"data": {
"match_id": "mt_899703080",
"bookmakers": [
{
"bookmaker": "Bet365",
"markets": {
"match_odds": {
"home": { "live": "3.400" },
"draw": { "live": "3.100" },
"away": { "live": "2.250" }
}
}
},
{
"bookmaker": "Betfair Exchange",
"markets": {
"match_odds": {
"home": {
"live": "3.650",
"available_to_back": [{ "price": 3.65, "size": 1209.44 }, { "price": 3.6, "size": 1795.66 }],
"available_to_lay": [{ "price": 3.7, "size": 480.78 }, { "price": 3.75, "size": 2184.81 }]
}
}
}
}
]
}
}
```
Each price has a single `live` value. Betfair Exchange also returns the order book: up to 3 levels of `available_to_back` and `available_to_lay`, best price first. After full time this endpoint returns `404` ("Match is already completed"). See [Live matches](/docs/guides/live-matches) for polling tips.
## Player-prop odds
Use the v2 endpoint. It returns the latest prices grouped by bookmaker (Bet365 first), then by market.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/v2/football/matches/mt_200199332/odds/players?bookmaker=bet365" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/v2/football/matches/mt_200199332/odds/players?bookmaker=bet365",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
const bet365 = data.bookmakers[0];
const anytime = bet365?.markets.find((m) => m.name === "anytime_goalscorer");
console.log(anytime?.players.slice(0, 3));
```
Response (trimmed):
```json Response theme={null}
{
"data": {
"match_id": "mt_200199332",
"home_team": { "id": "tm_9145", "name": "Arsenal" },
"away_team": { "id": "tm_0256", "name": "Leeds United" },
"kickoff_at": "2026-10-10T11:30:00.000Z",
"bookmakers": [
{
"bookmaker": "Bet365",
"markets": [
{
"name": "anytime_goalscorer",
"players": [
{ "id": "pl_45857110", "name": "Kai Havertz", "odd": 2.4 },
{ "id": "pl_84500047", "name": "Viktor Gyokeres", "odd": 2.4 }
]
},
{
"name": "player_shots_on_target",
"players": [
{ "id": "pl_45126714", "name": "Bukayo Saka", "line": 0.5, "market_type": "Over", "odd": 1.2857 }
]
}
]
}
]
}
}
```
* Markets cover goalscorers, shots, assists, cards, tackles, fouls, passes, goalkeeper saves and player of the match. Which ones you get depends on the bookmaker and the match.
* Line markets (such as `player_shots_on_target` or `player_passes`) have one entry per player, line and direction, with `line` and `market_type`. Other markets have neither field.
* Join players to other endpoints on `id`, not `name`: names can be spelled differently (here "Viktor Gyokeres", but "Viktor Gyökeres" in `/football/players`). `id` can be `null` when the player can't be matched to one ID; fall back to `name` then.
`/football/matches/{match_id}/odds/players` (v1, Bet365 only, 5 markets) is deprecated. Use `/v2/football/matches/{match_id}/odds/players`.
## Check odds coverage before you build
Odds history varies by competition and season. Use the coverage endpoints to see what each season has. `opening_odds` and `closing_odds` are separate data types.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/coverage/leagues/comp_3039" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch("https://api.thestatsapi.com/api/coverage/leagues/comp_3039", {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
const { data } = await res.json();
for (const s of data.seasons) {
console.log(s.year, s.data_types.opening_odds.available, s.data_types.closing_odds.available);
}
```
Response (two seasons, trimmed):
```json Response theme={null}
{
"data": {
"id": "comp_3039",
"name": "Premier League",
"seasons": [
{
"id": "sn_8406098",
"year": "26/27",
"status": "in_progress",
"events": { "finished": 50, "total": 100 },
"data_types": {
"odds": { "available": true, "covered_events": 50, "coverage_pct": 100 },
"opening_odds": { "available": true, "covered_events": 50, "coverage_pct": 100 },
"closing_odds": { "available": true, "covered_events": 50, "coverage_pct": 100 }
}
},
{
"id": "sn_6125938",
"year": "25/26",
"status": "complete",
"events": { "finished": 380, "total": 380 },
"data_types": {
"odds": { "available": true, "covered_events": 380, "coverage_pct": 100 },
"opening_odds": { "available": false, "covered_events": 0, "coverage_pct": 0 },
"closing_odds": { "available": true, "covered_events": 380, "coverage_pct": 100 }
}
}
]
}
}
```
So for the Premier League, 26/27 has opening and closing prices, while 25/26 has closing prices only. Older matches mostly have Bet365 closing prices only: `opening` is `null` and `last_seen` holds the closing price. Counts only include finished matches. See [Odds history](/docs/concepts/coverage#odds-history) for more.
To list every competition with a given odds data type, use `GET /coverage/leagues?data_type=closing_odds` (or `odds`, `opening_odds`). See [Coverage](/docs/concepts/coverage).
## API reference
* [Get match odds](/docs/api-reference/matches/odds)
* [Get live match odds](/docs/api-reference/matches/live-odds)
* [Get player-prop odds](/docs/api-reference/matches/player-odds)
* [Get coverage for a competition](/docs/api-reference/coverage/league) and [List coverage by competition](/docs/api-reference/coverage/leagues)
# Get player and team stats, xG, shotmaps and heatmaps
Source: https://www.thestatsapi.com/docs/guides/player-and-team-stats
Get season stats for players and teams, per-match team and player stats with xG, shotmaps and heatmaps, and check which matches have that data.
Use this page to get season totals and per-match stats for players and teams, including xG, shots and heatmaps.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text Prompt wrap theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), build a player page for Bukayo Saka: his Premier League season stats, and for his last match his rating, shots, xG and a heatmap. Check the availability flags before calling optional endpoints. Keep the API key server-side.
```
## Which endpoint to use
| You want | Endpoint | Needs |
| - | - | - |
| A player's season totals | `GET /football/players/{player_id}/stats` | `season_id` (required), `competition_id` (optional) |
| A team's season record | `GET /football/teams/{team_id}/stats` | `season_id` (required) |
| Team stats for one match, with xG | `GET /football/matches/{match_id}/stats` | A finished match |
| Player stats for one match, with xG | `GET /football/matches/{match_id}/player-stats` | A finished match |
| Every shot with its xG | `GET /football/matches/{match_id}/shotmap` | `shotmap_available: true` on the match |
| A player's heatmap for one match | `GET /football/matches/{match_id}/players/{player_id}/heatmap` | The player played |
| A player's heatmap for a season | `GET /football/players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap` | The player played in that competition season |
For matches in progress, use the live endpoints instead. See [Live matches](/docs/guides/live-matches).
The JavaScript examples run on your server and read your key from the `THESTATSAPI_API_KEY` environment variable.
## Player season stats
`season_id` is required. Add `competition_id` to count one competition only, and `stage=regular` or `stage=playoff` to split league matches from play-offs.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/players/pl_45126714/stats?season_id=sn_6125938&competition_id=comp_3039" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const params = new URLSearchParams({ season_id: "sn_6125938", competition_id: "comp_3039" });
const res = await fetch(
`https://api.thestatsapi.com/api/football/players/pl_45126714/stats?${params}`,
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
console.log(data.appearances, data.scoring.goals, data.scoring.assists);
```
Response (Bukayo Saka, Premier League 25/26, trimmed):
```json Response theme={null}
{
"data": {
"player_id": "pl_45126714",
"season_id": "sn_6125938",
"competition_id": "comp_3039",
"team_id": "tm_9145",
"rating": 7.64,
"appearances": 31,
"starts": 21,
"minutes_played": 2225,
"scoring": { "goals": 7, "assists": 5, "big_chances_created": 12 },
"shooting": { "total_shots": 71, "shots_on_target": 27 },
"passing": { "total_passes": 773, "pass_accuracy": 76.7, "key_passes": 63 },
"defending": { "tackles": 41, "interceptions": 16 },
"discipline": { "yellow_cards": 2, "red_cards": 0 }
}
}
```
Leave out `season_id` and you get:
```json theme={null}
{ "error": { "code": "BAD_REQUEST", "message": "season_id is required", "status_code": 400 } }
```
## Team season stats
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/teams/tm_9145/stats?season_id=sn_6125938" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/teams/tm_9145/stats?season_id=sn_6125938",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"data": {
"team_id": "tm_9145",
"season_id": "sn_6125938",
"competition_id": "comp_3039",
"matches_played": 38,
"wins": 26,
"draws": 7,
"losses": 5,
"points": 85,
"position": 1,
"goals_for": 71,
"goals_against": 27,
"goal_difference": 44,
"form": "WWWWW"
}
}
```
You get `404` when the team has no stats for that season.
## One match: team and player stats with xG
The examples use Brighton 3-0 Arsenal (`mt_155834755`). Team stats come split into `all`, `first_half` and `second_half`.
```bash curl theme={null}
# Team stats
curl "https://api.thestatsapi.com/api/football/matches/mt_155834755/stats" \
-H "Authorization: Bearer YOUR_API_KEY"
# One player's stats (comma-separate several IDs)
curl "https://api.thestatsapi.com/api/football/matches/mt_155834755/player-stats?player_ids=pl_45126714" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const match = "https://api.thestatsapi.com/api/football/matches/mt_155834755";
const { data: stats } = await (await fetch(`${match}/stats`, { headers })).json();
console.log(stats.overview.expected_goals?.all); // { home: 1.31, away: 1.63 }
const { data: players } = await (
await fetch(`${match}/player-stats?player_ids=pl_45126714`, { headers })
).json();
console.log(players[0].rating, players[0].shooting.expected_goals);
```
Team stats (trimmed):
```json Response theme={null}
{
"data": {
"match_id": "mt_155834755",
"overview": {
"ball_possession": { "all": { "home": 40, "away": 60 } },
"expected_goals": {
"all": { "home": 1.31, "away": 1.63 },
"first_half": { "home": 0.36, "away": 0.39 },
"second_half": { "home": 0.95, "away": 1.24 }
},
"total_shots": { "all": { "home": 17, "away": 11 } }
}
}
}
```
The response also has `shots`, `attack`, `passes`, `duels`, `defending`, `goalkeeping` and `np_expected_goals` groups. `expected_goals` is `null` when the match has no xG.
Player stats (trimmed):
```json Response theme={null}
{
"data": [
{
"player_id": "pl_45126714",
"player_name": "Bukayo Saka",
"team_id": "tm_9145",
"position": "F",
"rating": 7.06,
"minutes_played": 81,
"shooting": {
"total_shots": 2,
"shots_on_target": 1,
"expected_goals": 0.35,
"expected_assists": 0.53
}
}
]
}
```
## Shotmap
Every shot with its xG, result, body part, situation and pitch position. Add `player_id` for one player's shots.
Every shot is drawn as if attacking the same goal. `x` runs from 0 at that goal line to 100 at the shooter's own goal line (the edge of the box is 16.5). `y` runs across the pitch, with 50 at the centre of the goal.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches/mt_155834755/shotmap?player_id=pl_45126714" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/matches/mt_155834755/shotmap?player_id=pl_45126714",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data: shots } = await res.json();
for (const s of shots) console.log(s.minute, s.result, s.expected_goals, s.x, s.y);
```
One shot (trimmed):
```json Response theme={null}
{
"id": "sh_575269648",
"player_id": "pl_45126714",
"player_name": "Bukayo Saka",
"team_id": "tm_9145",
"x": 7.9,
"y": 45.3,
"minute": 45,
"result": "save",
"expected_goals": 0.303,
"situation": "fast_break",
"body_part": "right_foot",
"is_goal": false,
"is_on_target": true,
"is_penalty": false
}
```
## Heatmaps
Heatmap points use `x` and `y` from 0 to 100.
```bash curl theme={null}
# One match
curl "https://api.thestatsapi.com/api/football/matches/mt_155834755/players/pl_45126714/heatmap" \
-H "Authorization: Bearer YOUR_API_KEY"
# A whole season in one competition
curl "https://api.thestatsapi.com/api/football/players/pl_45126714/competitions/comp_3039/seasons/sn_6125938/heatmap" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const base = "https://api.thestatsapi.com/api/football";
const { data: matchMap } = await (
await fetch(`${base}/matches/mt_155834755/players/pl_45126714/heatmap`, { headers })
).json();
const { data: seasonMap } = await (
await fetch(`${base}/players/pl_45126714/competitions/comp_3039/seasons/sn_6125938/heatmap`, { headers })
).json();
```
```json theme={null}
{
"data": {
"match_id": "mt_155834755",
"player": { "id": "pl_45126714", "name": "Bukayo Saka" },
"team": { "id": "tm_9145", "name": "Arsenal", "side": "away" },
"points": [{ "x": 69, "y": 9 }, { "x": 31, "y": 7 }, { "x": 55, "y": 13 }]
}
}
```
One point per touch (46 in this match).
```json theme={null}
{
"data": {
"player": { "id": "pl_45126714", "name": "Bukayo Saka" },
"competition": { "id": "comp_3039", "name": "Premier League" },
"season": { "id": "sn_6125938", "name": "Premier League 25/26" },
"points": [{ "x": 59, "y": 3, "count": 1 }, { "x": 59, "y": 8, "count": 3 }]
}
}
```
Touches are grouped into pitch cells, with a `count` per cell.
Both return `404` when there's no heatmap, for example for an unused substitute, or a match or competition without positional data.
## Check what's available first
Not every competition or match has every kind of stat. Check these flags before you call an optional endpoint:
| Where | Flag | Meaning |
| - | - | - |
| Competition | `has_team_stats`, `has_player_stats` | The competition has team or player stats. |
| Competition | `xg_available` | The competition has xG data. |
| Match | `xg_available` | The match has xG (team-level or per-shot). |
| Match | `shotmap_available` | `/shotmap` has shots with xG values. Can be `false` while `xg_available` is `true`. |
| Match | `xg_quality` | `full` when shots carry per-shot xG, `none` otherwise. Treat unknown values as `none`. |
To find competitions with player stats or xG, use [List coverage by competition](/docs/api-reference/coverage/leagues) with `data_type=player_stats` or `data_type=xg`. To see which seasons of one competition have them, use [Get coverage for a competition](/docs/api-reference/coverage/league). See [Coverage](/docs/concepts/coverage).
`/stats` and `/player-stats` return `404` in two cases. `STATS_NOT_AVAILABLE_AT_SOURCE` means the match was only covered at score and lineup level, so detailed stats will never exist. `NOT_FOUND` means the match doesn't exist or its stats aren't in yet.
## API reference
* [Get a player's season stats](/docs/api-reference/players/stats)
* [Get a team's season stats](/docs/api-reference/teams/stats)
* [Get match stats](/docs/api-reference/matches/stats) and [Get match player stats](/docs/api-reference/matches/player-stats)
* [Get match shotmap](/docs/api-reference/matches/shotmap)
* [Get a player's match heatmap](/docs/api-reference/matches/player-heatmap) and [Get a player's season heatmap](/docs/api-reference/players/season-heatmap)
# Get league tables and group standings
Source: https://www.thestatsapi.com/docs/guides/standings
Get the current league table, a past season's table, tournament group tables and a team's standings history.
Use this page to show league tables, including past seasons and tournament groups.
**Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:
```text Prompt wrap theme={null}
Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), build a server route that returns the current Premier League table (position, team, played, goal difference, points) using the competition's current_season_id. Keep the API key server-side.
```
## How it fits together
Standings belong to a season of a competition, so you need two IDs: `competition_id` and `season_id`.
`GET /football/competitions?search=Premier` returns `comp_3039`. See [Find IDs](/docs/guides/finding-ids).
Use `current_season_id` from `GET /football/competitions/comp_3039`, or pick an older one from `GET /football/competitions/comp_3039/seasons`.
`GET /football/competitions/comp_3039/seasons/{season_id}/standings`
The JavaScript examples run on your server and read your key from the `THESTATSAPI_API_KEY` environment variable.
## Current league table
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/competitions/comp_3039/seasons/sn_8406098/standings" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const base = "https://api.thestatsapi.com/api/football/competitions/comp_3039";
const { data: comp } = await (await fetch(base, { headers })).json();
const { data: table } = await (
await fetch(`${base}/seasons/${comp.current_season_id}/standings`, { headers })
).json();
for (const row of table) {
console.log(row.position, row.team.name, row.matches_played, row.goal_difference, row.points);
}
```
Response (first two rows of 20):
```json Response theme={null}
{
"data": [
{
"team": { "id": "tm_3039", "name": "Manchester City" },
"position": 1,
"matches_played": 5,
"wins": 5,
"draws": 0,
"losses": 0,
"goals_for": 13,
"goals_against": 5,
"goal_difference": 8,
"points": 15,
"group_label": null
},
{
"team": { "id": "tm_9145", "name": "Arsenal" },
"position": 2,
"matches_played": 5,
"wins": 4,
"draws": 0,
"losses": 1,
"goals_for": 8,
"goals_against": 4,
"goal_difference": 4,
"points": 12,
"group_label": null
}
]
}
```
Rows are sorted by `position`. The table isn't paginated, so one call returns every team.
## A past season
Swap in an older `season_id` from the seasons list. Premier League 25/26 is `sn_6125938`:
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/competitions/comp_3039/seasons/sn_6125938/standings" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The first row is Arsenal: 38 played, 26 wins, 85 points.
How far back tables go depends on the competition. Check [Coverage](/docs/concepts/coverage) before you promise a season to your users. If the `season_id` belongs to a different competition, you get a `400` error: "season\_id does not belong to competition\_id".
## Tournament groups
For tournaments with a group stage (World Cup, EURO, AFCON and so on), list the groups first, then fetch one group's table.
```bash curl theme={null}
# 1. Groups in the 2022 World Cup
curl "https://api.thestatsapi.com/api/football/competitions/comp_6107/seasons/sn_326766/groups" \
-H "Authorization: Bearer YOUR_API_KEY"
# 2. Group A table
curl "https://api.thestatsapi.com/api/football/competitions/comp_6107/seasons/sn_326766/standings?group=A" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const season = "https://api.thestatsapi.com/api/football/competitions/comp_6107/seasons/sn_326766";
const { data: groups } = await (await fetch(`${season}/groups`, { headers })).json();
// [{ group_label: "A", name: "Group A" }, ... { group_label: "H", name: "Group H" }]
const { data: groupA } = await (await fetch(`${season}/standings?group=A`, { headers })).json();
```
Group A response (first row of 4):
```json Response theme={null}
{
"data": [
{
"team": { "id": "tm_41792", "name": "Netherlands" },
"position": 1,
"matches_played": 3,
"wins": 2,
"draws": 1,
"losses": 0,
"goals_for": 5,
"goals_against": 1,
"goal_difference": 4,
"points": 7,
"group_label": "A"
}
]
}
```
Without `group`, you get every group's table, sorted by `group_label` and then `position`.
Some competitions have no table. Knockout-only cups (such as the FA Cup) return an empty list, and `/groups` returns an empty list for leagues without groups.
## A team's standings history
To build a "season by season" view for one team, list the competitions and seasons where it has a table entry. Then fetch each table you need.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/teams/tm_9145/standings" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch("https://api.thestatsapi.com/api/football/teams/tm_9145/standings", {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
const { data } = await res.json();
for (const { competition, seasons } of data) {
console.log(competition.name, seasons.map((s) => s.id));
}
```
Response (trimmed):
```json Response theme={null}
{
"data": [
{
"competition": { "id": "comp_3039", "name": "Premier League" },
"seasons": [
{ "id": "sn_8406098", "name": "Premier League 26/27", "group_label": null },
{ "id": "sn_6125938", "name": "Premier League 25/26", "group_label": null }
]
},
{
"competition": { "id": "comp_7739", "name": "UEFA Europa League" },
"seasons": [
{ "id": "sn_706199", "name": "UEFA Europa League 22/23", "group_label": "A" }
]
}
]
}
```
This endpoint lists where the team has standings, not its positions. For the position, fetch the full table, using `group_label` as the `group` filter when it isn't `null`. For one season's record, [Get a team's season stats](/docs/api-reference/teams/stats) returns position, points and form in one call.
## API reference
* [Get season standings](/docs/api-reference/competitions/standings)
* [List groups in a season](/docs/api-reference/competitions/groups)
* [List a competition's seasons](/docs/api-reference/competitions/seasons)
* [List a team's standings history](/docs/api-reference/teams/standings)
# TheStatsAPI docs
Source: https://www.thestatsapi.com/docs/index
Start here to build with TheStatsAPI, by hand or with an AI coding tool like Claude Code, Codex, Cursor or Lovable.
TheStatsAPI is a REST API for football data: fixtures, results, live scores, league tables, team and player stats, xG shot maps and betting odds.
Give Claude Code, Codex, Cursor or Lovable our full API reference and a starter prompt, and let it write the code.
Already using an AI coding tool? Give it [`https://api.thestatsapi.com/llms.txt`](https://api.thestatsapi.com/llms.txt). It's every endpoint, parameter and response field in one file.
## Start here
Get a key and make your first useful requests in about five minutes.
Step-by-step recipes for fixtures, live scores, tables, stats and odds.
Every endpoint, with its parameters and response fields.
Check which competitions and seasons have stats, xG and odds.
## What you can build
A team's next match, last result, or every match on a date.
Live scores, minute, stats and events, by polling during a match.
Current and past standings for leagues and group stages.
Season and match stats, xG, shot maps and heatmaps.
Pre-match and in-play odds from several bookmakers, where available.
Look up competition, season, team, player and match IDs.
## How it works
1. Get an API key from your [dashboard](https://www.thestatsapi.com/api-keys).
2. Send it in the `Authorization: Bearer YOUR_API_KEY` header.
3. Call endpoints under `https://api.thestatsapi.com/api` and get JSON back.
```bash theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
That request returns Arsenal's next fixture. See the [Quickstart](/docs/quickstart) for more.
# Introduction
Source: https://www.thestatsapi.com/docs/introduction
What TheStatsAPI is, what football data it has, and how requests and responses work.
This page explains what football data TheStatsAPI has and how its requests and responses work. The API is read-only: you send HTTPS `GET` requests with your API key and get JSON back.
**Fastest way to build:** give your AI coding tool [`https://api.thestatsapi.com/llms.txt`](https://api.thestatsapi.com/llms.txt) and a starter prompt. See [Build with AI](/docs/build-with-ai).
## What data you can get
| Data | What you get | Start with |
| - | - | - |
| Competitions | Leagues, cups and international tournaments, with their seasons, groups and league tables | [List competitions](/docs/api-reference/competitions/list) |
| Teams | Profiles, squads, season stats, standings history, injuries and suspensions | [List teams](/docs/api-reference/teams/list) |
| Players | Profiles, season stats, injuries and suspensions, season heatmaps | [List players](/docs/api-reference/players/list) |
| Matches | Fixtures, results and live scores, with team and player stats, lineups, event timelines and referees | [List matches](/docs/api-reference/matches/list) |
| xG | Shot maps with xG for every shot, plus player heatmaps | [Shotmap](/docs/api-reference/matches/shotmap) |
| Odds | Pre-match, in-play and player-prop odds from Bet365, Paddy Power, BetMGM UK, Pinnacle and Betfair Exchange, where available | [Match odds](/docs/api-reference/matches/odds) |
| Coverage | Which competitions and seasons have which data types | [Coverage by competition](/docs/api-reference/coverage/leagues) |
Not every competition has every data type, and how far back the data goes varies by competition. Check [Coverage](/docs/concepts/coverage) before you build a feature. Competitions and matches also carry flags such as `xg_available` and `odds_available`, so you can tell before you call an endpoint.
## Base URL and authentication
Every endpoint lives under this base URL:
```text theme={null}
https://api.thestatsapi.com/api
```
Send your API key in the `Authorization` header on every request:
```text theme={null}
Authorization: Bearer YOUR_API_KEY
```
Get a key on the [API keys page](https://www.thestatsapi.com/api-keys). See [Authentication](/docs/authentication) for details.
## Requests
* **Filters are query parameters.** For example, `/football/matches?team_id=tm_9145&status=finished`.
* **Only documented parameters are accepted.** A misspelled or unknown parameter returns `400 UNKNOWN_PARAMETER`. The message suggests the closest name and lists the parameters the endpoint accepts.
* **IDs are prefixed strings.** Use them exactly as the API returns them.
| Prefix | Entity | Example |
| - | - | - |
| `comp_` | Competition | `comp_3039` (Premier League) |
| `sn_` | Season | `sn_8406098` (Premier League 26/27) |
| `tm_` | Team | `tm_9145` (Arsenal) |
| `pl_` | Player | `pl_45126714` (Bukayo Saka) |
| `mt_` | Match | `mt_200199332` |
* **Dates are `YYYY-MM-DD`** in `date_from` and `date_to`. There is no `date` parameter. Kickoff times in responses are UTC.
## Responses
List endpoints return a `data` array and a `meta` object for paging. For example, `GET /football/teams?search=Arsenal&is_mens_team=true&per_page=1` returns:
```json theme={null}
{
"data": [
{
"id": "tm_9145",
"name": "Arsenal",
"short_name": "Arsenal",
"country": "England",
"is_mens_team": true,
"primary_competition": { "id": "comp_3039", "name": "Premier League" }
}
],
"meta": { "page": 1, "per_page": 1, "total": 13, "total_pages": 13 }
}
```
Single-item endpoints return one object in `data`. For example, `GET /football/competitions/comp_3039` returns (trimmed):
```json theme={null}
{
"data": {
"id": "comp_3039",
"name": "Premier League",
"country": "England",
"type": "league",
"xg_available": true,
"current_season_id": "sn_8406098",
"total_teams": 20
}
}
```
Errors always use the same shape. Branch on `code`:
```json theme={null}
{
"error": {
"code": "UNKNOWN_PARAMETER",
"message": "Unknown query parameter 'date' (did you mean 'date_from' or 'date_to'?). Supported parameters: ...",
"status_code": 400
}
}
```
Learn more in [Pagination](/docs/concepts/pagination), [Errors](/docs/concepts/errors) and [Rate limits](/docs/concepts/rate-limits).
Data is pull-only. There are no webhooks or streaming connections. For live matches, poll the live endpoints. See [Live matches](/docs/guides/live-matches).
## Versioning and changes
* We add new endpoints, parameters and response fields over time. Write code that ignores fields it doesn't recognise.
* Deprecated endpoints are labelled in the API reference. For example, player-prop odds moved to `/v2/football/matches/{match_id}/odds/players`, and the old v1 path is marked deprecated.
# Quickstart
Source: https://www.thestatsapi.com/docs/quickstart
Get an API key and make your first useful requests in about five minutes: find IDs, get a team's next fixture and pull a league table.
Use this page to make your first requests: find the IDs you need, get a team's next fixture and pull a league table. It takes about five minutes.
**Fastest way: ask your AI.** Set up your tool with [Build with AI](/docs/build-with-ai), then paste: "Read [https://api.thestatsapi.com/llms.txt](https://api.thestatsapi.com/llms.txt), then write a server-side script that prints Arsenal's next fixture and the current Premier League table from TheStatsAPI. Read my key from the THESTATSAPI\_API\_KEY env var."
To run the examples yourself: the JavaScript uses the built-in `fetch` in Node 18 or later (save it as a `.mjs` file so top-level `await` works), and the Python uses the `requests` package.
[Create an account](https://www.thestatsapi.com/auth?mode=signup), then make a key on the [API keys page](https://www.thestatsapi.com/api-keys). Copy it straight away: it's only shown once.
Save it as an environment variable so it stays out of your code:
```bash theme={null}
export THESTATSAPI_API_KEY="YOUR_API_KEY"
```
Search competitions to find the Premier League's ID.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/competitions?search=Premier%20League&per_page=3" \
-H "Authorization: Bearer $THESTATSAPI_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/competitions?search=Premier%20League&per_page=3",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const body = await res.json();
console.log(body.data[0].id); // comp_3039
```
```python Python theme={null}
import os
import requests
res = requests.get(
"https://api.thestatsapi.com/api/football/competitions",
params={"search": "Premier League", "per_page": 3},
headers={"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"},
)
print(res.json()["data"][0]["id"]) # comp_3039
```
Response (trimmed):
```json theme={null}
{
"data": [
{ "id": "comp_3039", "name": "Premier League", "country": "England", "type": "league", "xg_available": true },
{ "id": "comp_84287", "name": "Egyptian Premier League", "country": "Egypt", "type": "league", "xg_available": true },
{ "id": "comp_5824", "name": "Russian Premier League", "country": "Russia", "type": "league", "xg_available": true }
],
"meta": { "page": 1, "per_page": 3, "total": 6, "total_pages": 2 }
}
```
The best match comes first, so `comp_3039` is the Premier League. Lists always return `data` plus a `meta` object for [pagination](/docs/concepts/pagination).
Search teams the same way. Add `is_mens_team=true` to leave out women's teams.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/teams?search=Arsenal&is_mens_team=true&per_page=3" \
-H "Authorization: Bearer $THESTATSAPI_API_KEY"
```
```javascript JavaScript theme={null}
const res = await fetch(
"https://api.thestatsapi.com/api/football/teams?search=Arsenal&is_mens_team=true&per_page=3",
{ headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` } }
);
const { data } = await res.json();
console.log(data.map((team) => `${team.id} ${team.name}`));
```
```python Python theme={null}
import os
import requests
res = requests.get(
"https://api.thestatsapi.com/api/football/teams",
params={"search": "Arsenal", "is_mens_team": "true", "per_page": 3},
headers={"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"},
)
for team in res.json()["data"]:
print(team["id"], team["name"])
```
Response (trimmed):
```json theme={null}
{
"data": [
{ "id": "tm_9145", "name": "Arsenal", "country": "England", "is_mens_team": true },
{ "id": "tm_033813", "name": "Arsenal U21", "country": "England", "is_mens_team": true },
{ "id": "tm_270010", "name": "Arsenal Tula", "country": "Russian Federation", "is_mens_team": true }
],
"meta": { "page": 1, "per_page": 3, "total": 13, "total_pages": 5 }
}
```
`tm_9145` is Arsenal's first team. You can also look up IDs in the [ID finder](https://www.thestatsapi.com/id-finder) in your dashboard. More in [Finding IDs](/docs/guides/finding-ids).
`/football/matches` returns the newest kickoff first. Add `sort=utc_date` to get the soonest first, so the first scheduled match is the next one.
```bash curl theme={null}
curl "https://api.thestatsapi.com/api/football/matches?team_id=tm_9145&status=scheduled&sort=utc_date&per_page=1" \
-H "Authorization: Bearer $THESTATSAPI_API_KEY"
```
```javascript JavaScript theme={null}
const params = new URLSearchParams({
team_id: "tm_9145",
status: "scheduled",
sort: "utc_date",
per_page: "1",
});
const res = await fetch(`https://api.thestatsapi.com/api/football/matches?${params}`, {
headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
});
const { data } = await res.json();
const next = data[0];
console.log(`${next.home_team.name} v ${next.away_team.name}, ${next.utc_date}`);
```
```python Python theme={null}
import os
import requests
res = requests.get(
"https://api.thestatsapi.com/api/football/matches",
params={"team_id": "tm_9145", "status": "scheduled", "sort": "utc_date", "per_page": 1},
headers={"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"},
)
match = res.json()["data"][0]
print(match["home_team"]["name"], "v", match["away_team"]["name"], match["utc_date"])
```
Example response (trimmed):
```json theme={null}
{
"data": [
{
"id": "mt_200199332",
"competition_id": "comp_3039",
"season_id": "sn_8406098",
"matchday": 6,
"status": "scheduled",
"utc_date": "2026-10-10T11:30:00.000Z",
"home_team": { "id": "tm_9145", "name": "Arsenal" },
"away_team": { "id": "tm_0256", "name": "Leeds United" },
"odds_available": true,
"xg_available": false
}
],
"meta": { "page": 1, "per_page": 1, "total": 9, "total_pages": 9 }
}
```
`utc_date` is always UTC. For a team's latest result, use `status=finished` and leave out `sort`. More in [Fixtures and results](/docs/guides/fixtures-and-results).
Standings belong to a season. First get the competition's `current_season_id`, then ask for that season's table.
```bash curl theme={null}
# 1. Current season ID
curl "https://api.thestatsapi.com/api/football/competitions/comp_3039" \
-H "Authorization: Bearer $THESTATSAPI_API_KEY"
# 2. Standings for that season
curl "https://api.thestatsapi.com/api/football/competitions/comp_3039/seasons/sn_8406098/standings" \
-H "Authorization: Bearer $THESTATSAPI_API_KEY"
```
```javascript JavaScript theme={null}
const base = "https://api.thestatsapi.com/api";
const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
const comp = await fetch(`${base}/football/competitions/comp_3039`, { headers }).then((r) => r.json());
const seasonId = comp.data.current_season_id; // sn_8406098
const table = await fetch(
`${base}/football/competitions/comp_3039/seasons/${seasonId}/standings`,
{ headers }
).then((r) => r.json());
for (const row of table.data) {
console.log(row.position, row.team.name, row.points);
}
```
```python Python theme={null}
import os
import requests
base = "https://api.thestatsapi.com/api"
headers = {"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"}
comp = requests.get(f"{base}/football/competitions/comp_3039", headers=headers).json()
season_id = comp["data"]["current_season_id"] # sn_8406098
table = requests.get(
f"{base}/football/competitions/comp_3039/seasons/{season_id}/standings",
headers=headers,
).json()
for row in table["data"]:
print(row["position"], row["team"]["name"], row["points"])
```
Example standings response (trimmed; your numbers will differ):
```json theme={null}
{
"data": [
{
"team": { "id": "tm_3039", "name": "Manchester City" },
"position": 1,
"matches_played": 5,
"wins": 5,
"draws": 0,
"losses": 0,
"goals_for": 13,
"goals_against": 5,
"goal_difference": 8,
"points": 15,
"group_label": null
},
{
"team": { "id": "tm_9145", "name": "Arsenal" },
"position": 2,
"matches_played": 5,
"wins": 4,
"draws": 0,
"losses": 1,
"goals_for": 8,
"goals_against": 4,
"goal_difference": 4,
"points": 12,
"group_label": null
}
]
}
```
More in [League tables](/docs/guides/standings).
## Common mistakes
| Mistake | What happens | Fix |
| - | - | - |
| Using `date=2026-10-10` | `400 UNKNOWN_PARAMETER`: "did you mean 'date\_from' or 'date\_to'?" | Set `date_from` and `date_to` to the same day. |
| Leaving out `sort=utc_date` for upcoming fixtures | The first row is the scheduled match furthest in the future | Add `sort=utc_date`. |
| Sending `bearer` (lowercase) or no space after `Bearer` | `401 UNAUTHORIZED` | Send exactly `Authorization: Bearer YOUR_API_KEY`. |
| Guessing IDs | `404 NOT_FOUND`, `400 BAD_REQUEST` ("Invalid team\_id") or data for the wrong team | Look IDs up with `search=`, as above. |
## Next steps
Let Claude Code, Codex, Cursor or Lovable write the rest for you.
Look up competitions, seasons, teams, players and matches.
Show live scores and stats by polling during a match.
Every endpoint, parameter and response field.