Player and team stats
Season totals for players and teams, match stats, xG shot maps and heatmaps.
Stats come at two levels: season totals for a player or a team, and the numbers for a single finished match. On top of that, shot maps and heatmaps show where things happened on the pitch. This guide covers each one.
Every example sends your key in the X-Api-Key header. Keep that key on your server. The examples use Bukayo Saka (player_ul3vc6om97u5), Arsenal (team_f0ibhxdw7hs5), the 25/26 Premier League season (season_98l95mxugqs9 in competition_705uv6jm8myn) and Brighton 3-0 Arsenal (match_nbqbxh7kv0q8).
Which endpoint do I need?
| You want | Endpoint |
|---|---|
| A player's season totals | /players/{player_id}/stats |
| A team's season record | /teams/{team_id}/stats |
| Team stats for one match, with xG | /matches/{match_id}/stats |
| Player stats for one match | /matches/{match_id}/player-stats |
| Every shot in a match, with xG | /matches/{match_id}/shotmap |
| Where a player touched the ball in one match | /matches/{match_id}/players/{player_id}/heatmap |
| Where a player touched the ball over a season | /players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap |
The match endpoints are for finished matches. While a match is still being played, use the live endpoints from Live matches.
A player's season
season_id is required. Add competition_id to count only one competition, and stage=regular or stage=playoff to split the regular season from play-offs.
curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/stats?season_id=season_98l95mxugqs9&competition_id=competition_705uv6jm8myn" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": {
"player_id": "player_ul3vc6om97u5",
"season_id": "season_98l95mxugqs9",
"competition_id": "competition_705uv6jm8myn",
"team_id": "team_f0ibhxdw7hs5",
"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 }
}
}
The numbers are grouped into scoring, shooting, passing, defending and discipline; the example shows a few from each. Without season_id you get 400 BAD_REQUEST.
Tip Want per-90 figures? Divide a total by
minutes_playedand multiply by 90.
A team's season
curl "https://api.soccer-data.co/v1/teams/team_f0ibhxdw7hs5/stats?season_id=season_98l95mxugqs9" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": {
"team_id": "team_f0ibhxdw7hs5",
"season_id": "season_98l95mxugqs9",
"competition_id": "competition_705uv6jm8myn",
"matches_played": 38,
"wins": 26,
"draws": 7,
"losses": 5,
"points": 85,
"position": 1,
"goals_for": 71,
"goals_against": 27,
"goal_difference": 44,
"form": "WWWWW"
}
}
This is the quickest way to get one team's position, points and recent form. If the team has no stats for that season, you get a 404.
Team stats for a match
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/stats" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": {
"match_id": "match_nbqbxh7kv0q8",
"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 } }
}
}
}
Each stat has an all value for the whole match, and first_half and second_half where they exist, each split into home and away. Besides overview, you'll find groups for shots, attack, passes, duels, defending and goalkeeping, plus np_expected_goals (xG without penalties). When a match has no xG, expected_goals is null.
Player stats for a match
Leave out player_ids for everyone who played, or pass one or more ids, comma-separated:
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/player-stats?player_ids=player_ul3vc6om97u5" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": [
{
"player_id": "player_ul3vc6om97u5",
"player_name": "Bukayo Saka",
"team_id": "team_f0ibhxdw7hs5",
"position": "F",
"rating": 7.06,
"minutes_played": 81,
"shooting": {
"total_shots": 2,
"shots_on_target": 1,
"expected_goals": 0.35,
"expected_assists": 0.53
}
}
]
}
Match player stats and season stats are shaped differently, so don't reuse the same parser for both.
Shot maps
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/shotmap" \
-H "X-Api-Key: YOUR_API_KEY"
Add player_id to get one player's shots only. Each shot looks like this:
{
"id": "shot_as8bbsa4f1mq",
"player_id": "player_ul3vc6om97u5",
"player_name": "Bukayo Saka",
"team_id": "team_f0ibhxdw7hs5",
"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
}
Drawing shots on a pitch
Every shot is placed as if the team were attacking the same goal, so you can draw both teams on one half.
xruns from 0 at the goal line being attacked to 100 at the shooter's own goal line. The edge of the penalty area is at 16.5.yruns across the pitch, with 50 in the middle of the goal.
Heatmaps
A match heatmap has one point for every touch the player made. This one has 46:
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/players/player_ul3vc6om97u5/heatmap" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": {
"match_id": "match_nbqbxh7kv0q8",
"player": { "id": "player_ul3vc6om97u5", "name": "Bukayo Saka" },
"team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "side": "away" },
"points": [{ "x": 69, "y": 9 }, { "x": 31, "y": 7 }, { "x": 55, "y": 13 }]
}
}
A season heatmap groups touches into cells on the pitch and gives each cell a count:
curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/competitions/competition_705uv6jm8myn/seasons/season_98l95mxugqs9/heatmap" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": {
"player": { "id": "player_ul3vc6om97u5", "name": "Bukayo Saka" },
"competition": { "id": "competition_705uv6jm8myn", "name": "Premier League" },
"season": { "id": "season_98l95mxugqs9", "name": "Premier League 25/26" },
"points": [{ "x": 59, "y": 3, "count": 1 }, { "x": 59, "y": 8, "count": 3 }]
}
}
Both use x and y from 0 to 100. Both return 404 when there's nothing to show, for example for a substitute who never came on, or when there's no positional data for that match or competition.
Check before you call
Not every competition or match has every kind of stat. Flags tell you in advance:
| Flag | Where | Means |
|---|---|---|
has_team_stats |
Competition | Team stats exist in this competition. |
has_player_stats |
Competition | Player stats exist in this competition. |
xg_available |
Competition and match | Some xG exists: team totals, per-shot values or both. |
shotmap_available |
Match | The shot map has shots with xG values. |
xg_quality |
Match | full when there's xG for every shot, otherwise none. Treat any other value as none. |
xg_available can be true while shotmap_available is false: the match has team xG, but no shot-by-shot data. Check shotmap_available before drawing a shot map.
To find competitions with a kind of data, use /coverage/leagues with data_type=player_stats or data_type=xg. Coverage explains how.
When stats aren't there
/stats and /player-stats can return 404 for two different reasons, and the code tells you which:
STATS_NOT_AVAILABLE: the match only has a score and lineups. Detailed stats will never exist for it, so don't retry.NOT_FOUND: the stats aren't in yet, or the match doesn't exist. Check the id, and try again later if the match has only just finished.
Common mistakes
- Leaving out
season_idon season stats endpoints. - Asking for
/statsor/player-statsduring a match. They return409; use the live endpoints until full time. - Calling
/shotmapwithout checkingshotmap_available. - Reading
xg_available: trueas "every shot has xG". Checkshotmap_availableorxg_quality. - Calling from the browser. Keep your key on the server.
Related endpoints
# Player and team stats
Season totals for players and teams, match stats, xG shot maps and heatmaps.
Stats come at two levels: season totals for a player or a team, and the numbers for a single finished match. On top of that, shot maps and heatmaps show where things happened on the pitch. This guide covers each one.
Every example sends your key in the `X-Api-Key` header. Keep that key on your server. The examples use Bukayo Saka (`player_ul3vc6om97u5`), Arsenal (`team_f0ibhxdw7hs5`), the 25/26 Premier League season (`season_98l95mxugqs9` in `competition_705uv6jm8myn`) and Brighton 3-0 Arsenal (`match_nbqbxh7kv0q8`).
## Which endpoint do I need?
| You want | Endpoint |
|---|---|
| A player's season totals | [`/players/{player_id}/stats`](/docs/reference/players/stats) |
| A team's season record | [`/teams/{team_id}/stats`](/docs/reference/teams/stats) |
| Team stats for one match, with xG | [`/matches/{match_id}/stats`](/docs/reference/matches/stats) |
| Player stats for one match | [`/matches/{match_id}/player-stats`](/docs/reference/matches/player-stats) |
| Every shot in a match, with xG | [`/matches/{match_id}/shotmap`](/docs/reference/matches/shotmap) |
| Where a player touched the ball in one match | [`/matches/{match_id}/players/{player_id}/heatmap`](/docs/reference/matches/player-heatmap) |
| Where a player touched the ball over a season | [`/players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap`](/docs/reference/players/season-heatmap) |
The match endpoints are for finished matches. While a match is still being played, use the live endpoints from [Live matches](/docs/guides/live-matches).
## A player's season
`season_id` is required. Add `competition_id` to count only one competition, and `stage=regular` or `stage=playoff` to split the regular season from play-offs.
```bash
curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/stats?season_id=season_98l95mxugqs9&competition_id=competition_705uv6jm8myn" \
-H "X-Api-Key: YOUR_API_KEY"
```
```json
{
"data": {
"player_id": "player_ul3vc6om97u5",
"season_id": "season_98l95mxugqs9",
"competition_id": "competition_705uv6jm8myn",
"team_id": "team_f0ibhxdw7hs5",
"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 }
}
}
```
The numbers are grouped into `scoring`, `shooting`, `passing`, `defending` and `discipline`; the example shows a few from each. Without `season_id` you get `400 BAD_REQUEST`.
> **Tip:** Want per-90 figures? Divide a total by `minutes_played` and multiply by 90.
## A team's season
```bash
curl "https://api.soccer-data.co/v1/teams/team_f0ibhxdw7hs5/stats?season_id=season_98l95mxugqs9" \
-H "X-Api-Key: YOUR_API_KEY"
```
```json
{
"data": {
"team_id": "team_f0ibhxdw7hs5",
"season_id": "season_98l95mxugqs9",
"competition_id": "competition_705uv6jm8myn",
"matches_played": 38,
"wins": 26,
"draws": 7,
"losses": 5,
"points": 85,
"position": 1,
"goals_for": 71,
"goals_against": 27,
"goal_difference": 44,
"form": "WWWWW"
}
}
```
This is the quickest way to get one team's position, points and recent form. If the team has no stats for that season, you get a `404`.
## Team stats for a match
```bash
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/stats" \
-H "X-Api-Key: YOUR_API_KEY"
```
```json
{
"data": {
"match_id": "match_nbqbxh7kv0q8",
"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 } }
}
}
}
```
Each stat has an `all` value for the whole match, and `first_half` and `second_half` where they exist, each split into `home` and `away`. Besides `overview`, you'll find groups for `shots`, `attack`, `passes`, `duels`, `defending` and `goalkeeping`, plus `np_expected_goals` (xG without penalties). When a match has no xG, `expected_goals` is `null`.
## Player stats for a match
Leave out `player_ids` for everyone who played, or pass one or more ids, comma-separated:
```bash
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/player-stats?player_ids=player_ul3vc6om97u5" \
-H "X-Api-Key: YOUR_API_KEY"
```
```json
{
"data": [
{
"player_id": "player_ul3vc6om97u5",
"player_name": "Bukayo Saka",
"team_id": "team_f0ibhxdw7hs5",
"position": "F",
"rating": 7.06,
"minutes_played": 81,
"shooting": {
"total_shots": 2,
"shots_on_target": 1,
"expected_goals": 0.35,
"expected_assists": 0.53
}
}
]
}
```
Match player stats and season stats are shaped differently, so don't reuse the same parser for both.
## Shot maps
```bash
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/shotmap" \
-H "X-Api-Key: YOUR_API_KEY"
```
Add `player_id` to get one player's shots only. Each shot looks like this:
```json
{
"id": "shot_as8bbsa4f1mq",
"player_id": "player_ul3vc6om97u5",
"player_name": "Bukayo Saka",
"team_id": "team_f0ibhxdw7hs5",
"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
}
```
### Drawing shots on a pitch
Every shot is placed as if the team were attacking the same goal, so you can draw both teams on one half.
- `x` runs from 0 at the goal line being attacked to 100 at the shooter's own goal line. The edge of the penalty area is at 16.5.
- `y` runs across the pitch, with 50 in the middle of the goal.
## Heatmaps
A match heatmap has one point for every touch the player made. This one has 46:
```bash
curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/players/player_ul3vc6om97u5/heatmap" \
-H "X-Api-Key: YOUR_API_KEY"
```
```json
{
"data": {
"match_id": "match_nbqbxh7kv0q8",
"player": { "id": "player_ul3vc6om97u5", "name": "Bukayo Saka" },
"team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "side": "away" },
"points": [{ "x": 69, "y": 9 }, { "x": 31, "y": 7 }, { "x": 55, "y": 13 }]
}
}
```
A season heatmap groups touches into cells on the pitch and gives each cell a `count`:
```bash
curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/competitions/competition_705uv6jm8myn/seasons/season_98l95mxugqs9/heatmap" \
-H "X-Api-Key: YOUR_API_KEY"
```
```json
{
"data": {
"player": { "id": "player_ul3vc6om97u5", "name": "Bukayo Saka" },
"competition": { "id": "competition_705uv6jm8myn", "name": "Premier League" },
"season": { "id": "season_98l95mxugqs9", "name": "Premier League 25/26" },
"points": [{ "x": 59, "y": 3, "count": 1 }, { "x": 59, "y": 8, "count": 3 }]
}
}
```
Both use `x` and `y` from 0 to 100. Both return `404` when there's nothing to show, for example for a substitute who never came on, or when there's no positional data for that match or competition.
## Check before you call
Not every competition or match has every kind of stat. Flags tell you in advance:
| Flag | Where | Means |
|---|---|---|
| `has_team_stats` | Competition | Team stats exist in this competition. |
| `has_player_stats` | Competition | Player stats exist in this competition. |
| `xg_available` | Competition and match | Some xG exists: team totals, per-shot values or both. |
| `shotmap_available` | Match | The shot map has shots with xG values. |
| `xg_quality` | Match | `full` when there's xG for every shot, otherwise `none`. Treat any other value as `none`. |
`xg_available` can be `true` while `shotmap_available` is `false`: the match has team xG, but no shot-by-shot data. Check `shotmap_available` before drawing a shot map.
To find competitions with a kind of data, use [`/coverage/leagues`](/docs/reference/coverage/leagues) with `data_type=player_stats` or `data_type=xg`. [Coverage](/docs/concepts/coverage) explains how.
## When stats aren't there
`/stats` and `/player-stats` can return `404` for two different reasons, and the `code` tells you which:
- **`STATS_NOT_AVAILABLE`:** the match only has a score and lineups. Detailed stats will never exist for it, so don't retry.
- **`NOT_FOUND`:** the stats aren't in yet, or the match doesn't exist. Check the id, and try again later if the match has only just finished.
## Common mistakes
- **Leaving out `season_id`** on season stats endpoints.
- **Asking for `/stats` or `/player-stats` during a match.** They return `409`; use the live endpoints until full time.
- **Calling `/shotmap` without checking `shotmap_available`.**
- **Reading `xg_available: true` as "every shot has xG".** Check `shotmap_available` or `xg_quality`.
- **Calling from the browser.** Keep your key on the server.
## Related endpoints
- [Team injuries and suspensions](/docs/reference/teams/injuries) and [player injuries and suspensions](/docs/reference/players/injuries)
- [Match lineups](/docs/reference/matches/lineups)
- [Match timeline](/docs/reference/matches/timeline)
- [Match referee](/docs/reference/matches/referee)