Soccer Data gives you football player stats at three levels: season totals from /players/{player_id}/stats, single-match numbers from /matches/{match_id}/player-stats (or /matches/{match_id}/live-player-stats while the match is on), and touch heatmaps for one match or a whole season. Each stats request is keyed by a player id plus a season id or a match id, which you look up once and can store for good.
Which endpoint do I need?
| You want | Endpoint |
|---|---|
| A player's id | /players?search=... |
| Season totals | /players/{player_id}/stats?season_id=... |
| Stats from one finished match | /matches/{match_id}/player-stats |
| Stats from a match in play | /matches/{match_id}/live-player-stats |
| Touches in one match | /matches/{match_id}/players/{player_id}/heatmap |
| Touches over a season | /players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap |
| Injuries and suspensions | /players/{player_id}/injuries-suspensions |
The examples use Bukayo Saka (player_ul3vc6om97u5), Arsenal (team_f0ibhxdw7hs5), the Premier League (competition_705uv6jm8myn), its 25/26 season (season_98l95mxugqs9) and Brighton 3-0 Arsenal (match_nbqbxh7kv0q8). Every request sends your key in the X-Api-Key header; keep that key on your server.
The ids you need
| Id | Looks like | Where to get it |
|---|---|---|
| Player | player_... |
/players?search=, a squad from /teams/{team_id}/players, or a match's lineups |
| Season | season_... |
current_season_id on /competitions/{competition_id}, or /competitions/{competition_id}/seasons |
| Competition | competition_... |
/competitions?search= |
| Match | match_... |
/matches filtered by team, season, date or status |
Ids never change, so store them rather than looking them up on every request. Find any ID has every lookup.
Step 1: find the player id
Search by name. You need at least three characters, and the search ignores case and accents:
curl "https://api.soccer-data.co/v1/players?search=saka&per_page=2" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": [
{
"id": "player_ul3vc6om97u5",
"name": "Bukayo Saka",
"position": "F",
"current_team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "jersey_number": 7 }
}
]
}
You can also list a club's players with /players?team_id=team_f0ibhxdw7hs5, get a whole squad in one response from /teams/team_f0ibhxdw7hs5/players, or take ids from /matches/{match_id}/lineups.
Step 2: find the season id
A season id belongs to one competition. For the season running now, read current_season_id from /competitions/competition_705uv6jm8myn. For older ones, list them, newest first:
curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn/seasons" \
-H "X-Api-Key: YOUR_API_KEY"
Pairing a season with a competition it doesn't belong to returns 400 SEASON_COMPETITION_MISMATCH.
Season totals
season_id is required; without it you get 400 BAD_REQUEST. competition_id is optional and counts only that competition, and stage=regular or stage=playoff splits league and group games from knockouts.
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",
"team_id": "team_f0ibhxdw7hs5",
"rating": 7.64,
"appearances": 31,
"starts": 21,
"minutes_played": 2225,
"scoring": {
"goals": 7,
"assists": 5,
"expected_goals": 5.31,
"expected_assists": 2.04,
"xg_matches": 9
},
"passing": { "pass_accuracy": 76.7, "key_passes": 63 }
}
}
The numbers are grouped into scoring, shooting, passing, defending, duels and discipline. Season expected_goals and expected_assists only add up appearances that had xG data; xg_matches says how many that was, and it can be lower than appearances. For per-90 figures, divide a total by minutes_played and multiply by 90.
Stats from one 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
}
}
]
}
Each entry also has started and played, and groups for passing, duels, defending, goalkeeping and general (touches, fouls, cards). rating and the xG fields can be null, which means not recorded, not zero. Match and season stats are shaped differently, so parse them separately.
This endpoint returns 409 MATCH_IS_LIVE while the match is in play. A 404 with STATS_NOT_AVAILABLE means detailed stats were never collected for that match, so don't retry; NOT_FOUND means the stats aren't in yet or the id is wrong.
A player's match log
There's no single "every match a player played" endpoint, but two calls build one: list the team's finished matches in the season, then ask each one for that player's stats. This Python script takes the club from the season totals' team_id rather than the player's current team:
import os, requests
API = "https://api.soccer-data.co/v1"
HEADERS = {"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}
PLAYER, COMP, SEASON = "player_ul3vc6om97u5", "competition_705uv6jm8myn", "season_98l95mxugqs9"
def get(path, **params):
return requests.get(f"{API}{path}", params=params, headers=HEADERS)
season = get(f"/players/{PLAYER}/stats", season_id=SEASON, competition_id=COMP).json()["data"]
matches = get("/matches", team_id=season["team_id"], competition_id=COMP, season_id=SEASON,
status="finished", sort="utc_date", per_page=100).json()["data"]
for match in matches:
res = get(f"/matches/{match['id']}/player-stats", player_ids=PLAYER)
rows = res.json()["data"] if res.ok else []
if not rows or not rows[0]["played"]:
continue
p = rows[0]
print(match["utc_date"][:10], p["minutes_played"], "min,",
p["shooting"]["goals"], "goals,", p["shooting"]["expected_goals"], "xG")
Each match is one request, so cache the results: finished matches don't change.
Live player stats
While a match is in play, /matches/{match_id}/live-player-stats returns the same fields as the finished-match endpoint, with each player's numbers so far. Find matches in play with /matches?status=live, then poll. This follows Ronnie Edwards in West Ham United v Queens Park Rangers, the live example from the Live matches guide:
curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/live-player-stats?player_ids=player_5lxcg0mhwqx3" \
-H "X-Api-Key: YOUR_API_KEY"
It returns 409 when the match isn't live, which is your signal to switch to /matches/{match_id}/player-stats after full time. On the paid plans (Pro and Business), live data is refreshed every 15 seconds, so polling faster only repeats the same answer and still counts as a request. That guide has a ready-made polling loop.
Heatmaps
A match heatmap has one point for every touch the player made:
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 adds up every match the player played in that competition season, grouped into cells with a count of touches:
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" },
"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 a match or competition without positional data.
Injuries and suspensions
curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/injuries-suspensions?current=false" \
-H "X-Api-Key: YOUR_API_KEY"
By default you get only what keeps the player out now; current=false adds past records. The answer has two lists:
| List | Fields |
|---|---|
injuries |
status (such as out or day_to_day), reason, start_date, expected_return, active |
suspensions |
reason (such as red_card_suspension), matches, competition, start_date, end_date, active |
A suspension only counts in its own competition, and expected_return is an estimate. A reason of national_team means the player is away with their country, not banned. To check a whole squad in one request, use /teams/{team_id}/injuries-suspensions.
Check coverage first
Player stats don't exist for every competition. has_player_stats on /competitions/{competition_id} gives a yes or no for the whole competition; /coverage/leagues?data_type=player_stats lists every competition that has them; and /coverage/leagues/{competition_id} gives coverage_pct season by season. In the Premier League, player stats go back to 2014/15. How much history you can use also depends on your plan: Pro includes 5 years and Business 10.
Next steps
- Player and team stats: the full guide, with team stats and shot maps.
- Player season stats reference: every field.
- Find any ID: look up players, seasons and matches.
- Comparing providers? See the checklist for choosing a football data API.
- Create an account to get your API key.
Frequently asked questions
Which API has football player stats and heatmaps?
Soccer Data has player season totals, per-match stats, live in-match stats, match and season heatmaps, and injuries and suspensions, all over one REST API. Depth varies by competition, so check has_player_stats or /coverage/leagues?data_type=player_stats first.
How do I get a football player's season stats from an API?
With Soccer Data, call /players/{player_id}/stats?season_id=..., optionally with competition_id to count one competition. season_id is required, and the totals come grouped into scoring, shooting, passing, defending, duels and discipline.
How do I find a player id?
Search with /players?search= and at least three characters of the name, or list a squad with /teams/{team_id}/players. Soccer Data ids never change, so look each one up once and store it.
Can I get live player stats during a match?
Yes. Soccer Data's /matches/{match_id}/live-player-stats returns each player's numbers so far while the match is in play, and you can pass player_ids to follow only some players. After full time, switch to /matches/{match_id}/player-stats.
Does the player stats data include xG and expected assists?
Yes, where the match has xG. Match player stats have shooting.expected_goals and shooting.expected_assists, and season totals have scoring.expected_goals, scoring.expected_assists and xg_matches, the number of appearances that had xG.
How do I get a player's injuries and suspensions?
Call /players/{player_id}/injuries-suspensions on the Soccer Data API for current absences, or add current=false for past ones too. For a whole squad, use /teams/{team_id}/injuries-suspensions.