Building with AI? Give your coding tool the whole reference in one file: https://soccer-data.co/llms.txt
SOCCER DATA Docs
Sign in Get API key
// Guides

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_played and 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.

  • 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:

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_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.