PitchAPI is a read-only REST API for football data.
Grab a day of fixtures, take a match ID, and drill into shots, momentum, events, player stats, lineups, and the advanced analytics built on the raw event feed. You just need an API key.
All requests must use HTTPS. Requests over plain HTTP are refused rather than redirected so that a misconfigured client never leaks its key.
Fundamentals
SDKs
Official client libraries wrap the REST API so you work with typed objects instead of raw JSON. They unwrap the response envelope, map errors to typed exceptions, and retry on 429 and 5xx with the Retry-After delay honoured.
Python is available today; more languages are on the way. Every method returns the same fields documented in this reference, so the endpoint pages below are the field-level reference for the SDK too.
Install — Python
pipinstallpitchapi
Requirements
Python 3.9+, one dependency (httpx). The key can be passed as api_key= or read from the PITCHAPI_API_KEY environment variable — keep it server-side, never in client code. Responses are plain dataclasses and the package ships type information, so editors complete every field.
from pitchapi import PitchAPI
with PitchAPI(api_key="pk_live_...") as client:
# A day of fixtures
day = client.date.get("2025-11-09")
for m in day.matches:
print(m.home_team.name, m.score_home, "-", m.score_away, m.away_team.name)
# Drill into a match
match = client.matches.get("m_B8x2K9")
shots = client.matches.shots(match.id)
advanced = client.matches.advanced(match.id) # PPDA, field tilt, xG timeline
Asynchronous
import asyncio
from pitchapi import AsyncPitchAPI
async def main():
async with AsyncPitchAPI() as client: # key from PITCHAPI_API_KEY
league = await client.leagues.get("l_0bfbkO")
matches = await client.leagues.matches(league.id, season="2025/2026")
asyncio.run(main())
Both clients expose the same five namespaces. AsyncPitchAPI is the exact twin of PitchAPI: same names, same arguments, same models, awaited.
Namespaces
client.date
get(date, status=None) — every match on a day. date takes a YYYY-MM-DD string or a datetime.date.
list, get, matches(id, season=None, status=None) — season defaults to the latest one with data.
client.teams
get — the team profile.
client.players
get — the player profile.
Two behaviours are worth knowing before you reach for them. Match listings are historical by default, and a lineup published before kickoff may still be a prediction.
Upcoming fixtures
# Listings return played matches by default. Ask for scheduled ones:
for m in client.date.get("2025-11-16", status="upcoming").matches:
print(m.time_utc, m.home_team.name, "vs", m.away_team.name)
# m.score_home is None until the match is played
# Within a season — "all" returns results and fixtures together
client.leagues.matches("l_0bfbkO", season="2025/2026", status="all")
Predicted lineups
lineups = client.matches.lineups("m_B8x2K9")
# Before kickoff the XI may be a prediction rather than the real one
if lineups.home.confirmed:
print(lineups.home.formation, [p.name for p in lineups.home.starters])
else:
print("predicted only:", lineups.home.lineup_type)
Every exception derives from PitchAPIError and carries code, status_code and request_id where the API supplied them — quote the request ID in any support thread. Retries are automatic, so a RateLimitError means the client already backed off and tried again.
Error handling
from pitchapi import NotFoundError, PlanUpgradeRequiredError, RateLimitError
try:
client.matches.advanced("m_B8x2K9")
except NotFoundError as e:
print(e.code, e.request_id) # RESOURCE_NOT_FOUND or ANALYTICS_UNAVAILABLE
except PlanUpgradeRequiredError:
... # the league is Pro-only
except RateLimitError as e:
print("retry after", e.retry_after, "s")
Fundamentals
Authentication
Pass your API key in the X-API-KEY header on every request. Production keys use pk_live_, test keys use pk_test_. You get a key when you sign up and can view it in your dashboard.
Your API key is a bearer credential: anyone holding it can spend your quota. Keep it server-side and never ship it in client code or a public repository. We only store a SHA-256 hash of your key, so a lost key must be replaced.
Fundamentals
Responses
Every response is wrapped in a single JSON envelope. Success carries a data key, failure carries an error key. The two never appear together, so branch on whichever is present.
Timestamps are RFC 3339 in UTC, dates are YYYY-MM-DD. Optional fields are omitted when unavailable. Lists return an empty array when there is nothing to report. A null is never a zero: it means the value is undefined, which for a rate means an empty denominator and for an average means nothing to average.
Fundamentals
Resource IDs
Every public ID is a short opaque token: a resource prefix, an underscore, and exactly six base62 characters. IDs are stable and non-sequential. Treat them as opaque strings and never parse them for meaning.
Prefix
Resource
Example
m_
Match
m_B8x2K9
s_
Shot
s_Q1pR6z
p_
Player
p_7YtX4q
t_
Team
t_9aB2xQ
l_
League
l_4Kd0Wq
Shot IDs are the one exception to global uniqueness: they are scoped to their match, so the same value in different matches refers to different shots.
Fundamentals
Plans and coverage
There is one plan. It reaches every endpoint, every competition in the catalogue and the full history back to 2021, and it carries no request allowance and no charge.
There is no per-second or per-minute rate to design around, no daily total to budget for, and no resource your key cannot reach.
FreeNo charge
42leagues · unlimited requests
Every league the service covers, including second tiers, cups and UEFA competitions. Every endpoint, advanced analytics included, with the full history back to 2021. No request allowance, no card, no trial period.
There are no X-RateLimit headers, because there is no allowance for them to describe. The only limit the API enforces is an unadvertised fair-use burst guard, far above anything a normal client does.
Sustaining it returns 429 with the code RATE_LIMIT_EXCEEDED and a Retry-After header. There is never an overage charge, and it is the only limit a well-behaved client will ever meet.
Fundamentals
Error codes
Errors use conventional HTTP status codes alongside a stable machine-readable code. Branch on the code, not the message — messages are for humans and may be reworded.
Two of them are 404s and mean different things. RESOURCE_NOT_FOUND says the ID does not exist; ANALYTICS_UNAVAILABLE says the match does, and we never rated it.
Status
Code
Meaning
401
UNAUTHORIZED
The X-API-KEY header is missing or the key is invalid.
429
RATE_LIMIT_EXCEEDED
The fair-use burst ceiling was hit — the API serves requests without a per-day allowance, so this only appears far above anything a normal client does. Retry-After gives the seconds to back off for.
400
INVALID_PARAMETER
A path or query parameter isn't in the expected format (date, ID pattern, etc.).
404
RESOURCE_NOT_FOUND
No resource exists with that ID.
404
ANALYTICS_UNAVAILABLE
Advanced analytics routes only. The match exists but was never rated — event-feed coverage is not universal, so this is an ordinary answer rather than an error on your side. Kept separate from RESOURCE_NOT_FOUND so you can tell it from a bad match ID.
500
INTERNAL_SERVER_ERROR
Something went wrong on our side. Retry with backoff.
Data model
Coordinate systems
Shots carry two coordinate pairs: where the ball was struck on the pitch and where it crossed the goal line. Both are in metres on a real 105 x 68 pitch, not a normalised grid.
Pitch coordinates are normalised for direction: every shot attacks the goal at x equals 105 regardless of which side took it. You never need to flip the axis for direction. Every distance in the advanced analytics uses the same pitch and the same units.
Heatmaps use the same pitch but count cells rather than metres, on a 16 x 12 grid the response describes in its grid block. They inherit the direction normalisation: cell_x 0 is the acting team's own goal line and 15 the opponent's, for both teams and in both halves. It is not the left end of the pitch. Each grid is therefore right on its own pitch, but the two are not on a shared one. To draw both teams together, request frame equals home_ltr and the server turns the away side for you — there is nothing to mirror by hand. Every team and player also carries a side field, home or away, and grid.frame echoes the orientation actually served.
On the pitch
x0 to 105
Distance along the pitch in metres, normalised so the attacking goal is always at 105.
y0 to 68
Lateral position across the pitch in metres. The centre line is at 34.
On the goal line
goal_crossed_y0 to 68
Where the shot crossed the goal line on the same y axis as the pitch.
goal_crossed_z0 to 7.6
Height above the ground where the shot crossed the goal line.
Goal width
7.32 m
posts at y = 30.34 and 37.66
Crossbar height
2.44 m
z above this is over
Penalty area
16.5 m deep
x >= 88.5
Penalty spot
11 m out
x = 94
The goal_crossed_y field is measured across the full pitch width, not the goal opening alone.
Data model
Shot fields
Every shot response includes the fields below. A few need care: is_on_target is also true for blocked shots heading on target, and expected_goals_on_target is omitted when unavailable.
id
string
Shot identifier, scoped to this match. Not globally unique.
player
object
The player who took the shot (id, name, position_id, image_url).
team_id
string
The team that took the shot.
x
number
0 to 105
Pitch x coordinate where the shot was struck, in metres.
y
number
0 to 68
Pitch y coordinate where the shot was struck, in metres.
expected_goals
number
0 to 1
Chance quality before the shot, as a probability from 0 to 1.
expected_goals_on_target
number
0 to 1
Chance quality after the shot, given where it crossed the line. Omitted when unavailable.
is_on_target
boolean
Whether the shot was on target.
goal_crossed_y
number
0 to 68
Y coordinate where the shot crossed the goal line. Omitted when the crossing position isn't available.
goal_crossed_z
number
0 to 7.6
Height in metres where the shot crossed the goal line. Omitted when the crossing position isn't available.
is_inside_box
boolean
Whether the shot was taken from inside the penalty area.
event_type
string
The outcome: Goal, AttemptSaved, Miss, or Post.
situation
string
How the chance arose: RegularPlay, FromCorner, SetPiece, FastBreak, FreeKick, ThrowInSetPiece, Penalty, or IndividualPlay.
shot_type
string
Body part used: RightFoot, LeftFoot, Header, or OtherBodyParts.
minute
integer
Match minute the shot was taken, not counting stoppage time.
minute_added
integer
Stoppage-time minutes added to minute. Omitted when none was added.
is_blocked
boolean
Whether a defender blocked the shot before it reached the goal.
blocked_x
number
Pitch x coordinate where the ball was intercepted or blocked. Omitted when not blocked.
blocked_y
number
Pitch y coordinate where the ball was intercepted or blocked. Omitted when not blocked.
is_own_goal
boolean
Whether the goal was an own goal.
is_saved_off_line
boolean
Whether the ball was cleared off the goal line.
keeper
object
The goalkeeper facing the shot (id, name, position_id, image_url). Omitted when unavailable.
Data model
Player stats
Each stat line is an array of groups. Read entries by their inner key field, not the display label — the key is stable across matches, the label may change.
Which groups appear depends on position and involvement. Every player who featured gets top_stats. Outfielders also get attack, defense, and duels. Goalkeepers get shot-stopping figures inside top_stats instead.
Group
Appears on
Contains
top_stats
Every stat line
Rating, minutes, goals, assists, xG, and xA. Present for every player who featured.
attack
Outfielders
Touches, dribbles, crosses, passes into the final third, and non-penalty xG. Outfielders only.
defense
Outfielders
Clearances, interceptions, recoveries, tackles, blocks, and times dribbled past. Outfielders only.
duels
Outfielders
Aerial and ground duels, fouls committed, and fouls won. Outfielders only.
physical_metrics
Rarely available
Distance covered, sprints, and top speed. From tracking data.
Value shapes: branch on stat.type
integer
A whole number (goals, touches, clearances).
double
A decimal number (ratings, xG).
fractionWithPercentage
A made-attempted pair (accurate passes, duels won). Carries both value and total.
distanceWithPercentage
Metres covered out of a total distance (running splits).
distance
A distance in metres.
speed
A speed in km/h.
fantasyPoints
A fantasy football score.
boolean
A presence flag, not a measurement. Skip it when parsing.
Data model
Lineups
Formations, starters, and benches for both sides. Two fields trip people up: pitch coordinates (normalised 0 to 1, not metres) and position_id (different meaning for starters versus substitutes).
Lineup fields
formation
Formation at kick-off (e.g. 4-2-3-1).
confirmed
False while the lineup is a pre-match prediction (available up to 48h before kickoff), true once confirmed near kickoff and for the actual lineup of a played match.
lineup_type
Label for a predicted lineup (e.g. lastStarting11). Omitted once the lineup is confirmed.
pitch_x / pitch_y0 to 1
Position in the formation, normalised to 0 to 1. Both are 0 for every substitute.
position_id
For a starter, a slot in the formation grid. For a substitute, a positional category: 0 goalkeeper, 1 defender, 2 midfielder, 3 attacker, -1 unclassified.
is_captain
Whether the player was captain at kick-off.
coach
The coach (name). Omitted when unavailable.
Data model
Momentum
A minute-by-minute pressure curve as a single signed series. Some matches have no momentum data, so check the array length before plotting.
Momentum points
minute0 to 90.75
Match minute. Fractional values mark stoppage and added extra time.
value-100 to 100
Pressure index at this minute. Positive favours home, negative favours away.
Data model
Events
The match timeline with four event types: goals, cards, substitutions, and penalties. Shots are not here — use the shots endpoint for shot data and this one for the narrative.
The period field is derived from the minute, not reported by upstream. Treat it as a convenience around half time.
goal
A goal, with the running score and own-goal / penalty flags.
substitution
A substitution. player comes off, sub_in_player comes on.
yellowcard
A yellow card.
redcard
A red card (straight red or second yellow).
Data model
Team stats
Team-versus-team statistics for each half and the match as a whole. Values are display strings, so parse them with format_type. An empty format_type still holds a plain number.
The Expected goals group splits xG into open play and set play, which shot-level data does not provide. Groups come back in alphabetical order. For typed numeric analytics on the same match, use the advanced endpoints instead — those return numbers, not display strings.
format_type values
integer
A whole number as a string.
integerWithPercentage
A count and its percentage in one string. The number precedes the space.
double
A decimal as a string.
distance
A distance in metres as a string.
""
An empty format_type but still a plain number.
Metrics
How to read these
Everything the advanced endpoints return, defined. Each entry gives the published definition of the metric — the industry standard where one exists, otherwise FBref’s, StatsBomb’s or the originating paper’s — along with what the count contains and the thresholds it uses. A handful of these metrics are defined by nobody, and those entries say so.
Distances are metres on a 105 x 68 pitch, times are seconds, percentages run 0 to 100. Where a metric is a rate or an average, null means undefined — an empty denominator, or nothing to average — and never zero. A team that attempted no pass has no pass accuracy, which is not an accuracy of 0.
Two pairs that must never be added together. passing.passes already contains passing.crosses, and creation.chances_created is the same number as passing.key_passes. Both are published under two names because both names are asked for.
Player figures do not always sum to the team figure. creation.sca_breakdown.foul_drawn belongs to the team and to no player, so the players’ sca sums short by exactly that amount.
Totals
The two fields that sit at the top level of every team and player object, outside the metric groups.
actionsactionsteam + player
On-the-ball actions in the converted event stream.
Every pass, cross, carry, take-on, shot, tackle, interception, clearance and keeper action, after conversion into a uniform action stream. Carries are synthesised and counted here — roughly 400 a match — so this is a denominator for the metrics below, not a raw event or touch count.
minutes_playedminutes_playedplayer
Minutes the player was on the pitch.
Derived from the lineup and substitution record for the match.
Possession value
What an action did to the probability of scoring. Both models are fitted on closed historical seasons and frozen, so a match is never scored by a model that saw it.
xt_totalpossession_value.xt_totalteam + player
Threat added by moving the ball into more dangerous space.
Expected Threat, as defined by Karun Singh: a 16 x 12 grid values each zone by how likely the possessing team is to score from it, and a ball-moving action is worth the destination's value minus the origin's. Only successful actions that move the ball and keep possession can be rated — shots, tackles and failed moves are null, not zero.
vaep_totalpossession_value.vaep_totalteam + player
Value of every action, weighing what it gained against what it risked.
VAEP, as defined by Decroos et al. (KDD 2019): two classifiers estimate the probability the team scores and the probability it concedes within the next 10 actions, and an action is worth the rise in the first minus the rise in the second. Unlike xT every action type is valued.
vaep_offensivepossession_value.vaep_offensiveteam + player
The attacking half of the VAEP decomposition.
The change in the team's probability of scoring within the next 10 actions, from the state immediately before the action to the state immediately after. Positive when an action improves the attack, negative when it squanders one.
vaep_defensivepossession_value.vaep_defensiveteam + player
The risk half: what the action did to the chance of conceding.
The change in the probability the team concedes within the next 10 actions, over the same window. The term is subtracted, so the two components do not reach vaep_total by adding their magnitudes.
pv_totalpossession_value.pv_totalteam + player
Value of every action measured over the next 10 seconds.
Possession Value in Stats Perform's framing: the same two classifiers over the next 10 seconds rather than the next 10 actions. Our game state is three actions rather than the up-to-five possession events of the published framing.
pv_offensivepossession_value.pv_offensiveteam + player
The attacking half of the PV decomposition.
The change in the probability the team scores within the next 10 seconds, before the action against after. The short fixed horizon makes it dominated by proximity to a shot rather than by build-up.
pv_defensivepossession_value.pv_defensiveteam + player
The risk half over the same ten-second window.
The change in the probability the team concedes within the next 10 seconds. Subtract it from pv_offensive and add the baseline to recover pv_total.
Pass network
The passing graph of each team, served by GET /v1/matches/{id}/advanced/network. The window closes at the team's first substitution, so these describe the starting eleven. Receivers are inferred — the player of the next action by the same team — so a pass whose receiver cannot be resolved counts in the node totals but contributes no edge.
avg_xnetworks[].nodes[].avg_xteam
Where the player passed from, x coordinate.
The mean origin of that player's completed passes in the window — not their average position and not a centroid of all touches. Null for a player who received passes but never played one.
avg_ynetworks[].nodes[].avg_yteam
Where the player passed from, y coordinate.
The mean origin of that player's completed passes. With avg_x it places the node on the 105 x 68 pitch. Null when the player attempted no pass in the window.
degreenetworks[].nodes[].degreeteam
Distinct passing partners.
Distinct teammates the player exchanged at least one pass with. Nullable as a group with strength, betweenness and clustering: the centrality library is optional and the measures are skipped without it, which is not the same claim as a player being unconnected.
strengthnetworks[].nodes[].strengthteam
Weighted degree — total passes to and from the player.
The sum of edge weights incident to the node: passes made plus passes received. Nullable as a group with the other centrality measures.
betweennessnetworks[].nodes[].betweennessteam
How much passing flow between others routes through this player.
Computed on an edge distance of 1/passes, since betweenness needs distances and the graph holds volumes. Nullable as a group with the other centrality measures.
clusteringnetworks[].nodes[].clusteringteam
How often a player's partners also pass to each other.
Local clustering coefficient: the fraction of the player's passing pairs that are themselves connected. Nullable as a group with the other centrality measures.
centralizationnetworks[].centralizationteam
How far the team's graph is dominated by a few hubs.
Freeman centralization on degree: 0 when every player is equally connected, approaching 1 when one hub touches everything. Null when the graph had no edges.
Passing
Volume, accuracy and progression. One overlap to keep in mind throughout: passes counts crosses as well, so the two must never be added together.
passespassing.passesteam + player
Attempted passes, including crosses.
The standard definition: an attempted delivery of the ball from one player to another on the same team. Crosses are a separate action type but are counted here too, so passes and crosses must never be summed; throw-ins, corners and free kicks are not folded in.
pass_accuracypassing.pass_accuracyteam + player
Successful passes over attempted, as a percentage.
Successful over attempted, a pass being successful when the next touch is by a teammate. The denominator is passes, so it includes crosses. Null rather than zero when no pass was attempted.
progressive_passespassing.progressive_passesteam + player
Completed open-play passes that moved the ball meaningfully closer to goal.
The standard published rule: a completed open-play pass beginning in the attacking two thirds that moves the ball at least 25% closer to the centre of the goal. Set pieces are excluded, which keeps this a strict subset of passes.
progressive_pass_distancepassing.progressive_pass_distanceteam + player
Metres of goal-ward progress over those passes.
The reduction in straight-line distance to the goal centre, over qualifying progressive passes only. A pass away from goal contributes nothing rather than a negative.
passes_into_boxpassing.passes_into_boxteam + player
Completed passes ending inside the opponent's penalty area.
Completed passes whose end point falls inside the opponent's penalty area, crosses included. The ball must enter: a pass beginning inside the box and staying there does not count.
key_passespassing.key_passesteam + player
Passes that directly led to a teammate's shot.
A completed pass paired with the shot immediately following it for the same team, looking through any carry in between; open play, corners and free kicks qualify. Ours counts passes that produced goals too, where the narrower standard key pass covers only a player who shoots without scoring — which is why chances_created is the same number as this.
assistspassing.assiststeam + player
Passes that directly produced a goal.
The final pass before a goal, as flagged by the source event feed rather than derived by us.
through_ballspassing.through_ballsteam + player
Passes that split the defensive line for a runner.
A pass played between defenders into the path of a teammate running through. It arrives as a qualifier on the pass event, so it is the provider's judgement and cannot be recomputed from the x/y data.
crossespassing.crossesteam + player
Balls played from wide, targeting a teammate centrally near goal.
A ball played from a wide position towards a teammate centrally near goal, stored as its own action type. Open play only: a crossed corner resolves to a corner and a crossed free kick to a free kick.
switchespassing.switchesteam + player
Completed passes moving the ball a long way across the pitch.
No provider emits a switch-of-play event, so this is derived from lateral displacement on a 68 m-wide pitch. The threshold is 36.6 m, more than half the width.
Carrying
Carries are synthesised, not observed: the event feed contains no carry event, and one is inserted wherever the ball moves between two consecutive actions by the same team, gated at the 5 m minimum of the standard published definition. That is roughly 400 movements a match that do not exist in the source.
carriescarrying.carriesteam + player
Movements of the ball at a player's feet, over 5 metres.
The standard definition treats a carry as any movement of the ball by a player greater than five metres from where they received it. Ours runs from where the ball was last touched to where the next action by the same team begins, credited to the player of that next action.
carry_distancecarrying.carry_distanceteam + player
Total straight-line metres covered while carrying.
Start-to-end displacement summed over a player's carries — net displacement between two recorded events, not the path actually run.
progressive_carriescarrying.progressive_carriesteam + player
Carries that advanced play meaningfully towards goal.
The Wyscout rule: a carry reducing the distance to the goal centre by 30 m in the own half, 15 m crossing the halfway line, or 10 m in the opponent's half. Not the rule behind progressive_passes; the standard published carry rule asks only for five metres of gain in the opposition half.
progressive_carry_distancecarrying.progressive_carry_distanceteam + player
Metres of goal-ward progress over a player's carries.
The reduction in distance to the goal centre over a player's carries, with movement away from goal contributing nothing rather than a negative.
carries_into_final_thirdcarrying.carries_into_final_thirdteam + player
Carries that crossed into the attacking third.
The carry must start outside and end inside, crossing x = 70. One that begins and ends there does not count.
carries_into_boxcarrying.carries_into_boxteam + player
Carries that entered the opponent's penalty area.
Ends inside the opponent's penalty area having started outside it, the box beginning at x = 88.5 m.
take_onscarrying.take_onsteam + player
Attempts to beat an opponent while in possession.
The standard take on, historically labelled a dribble: an attempt to beat an opponent while in possession. A real recorded event rather than a reconstruction.
take_ons_woncarrying.take_ons_wonteam + player
Take-ons where the player beat their opponent and kept the ball.
Take-ons where the player beat the defender and retained possession. Divided by take_ons this gives the dribble success rate.
miscontrolscarrying.miscontrolsteam + player
A poor touch that lost the ball.
The standard unsuccessful touch: a poor touch that loses the ball, attributed to the player rather than to opposition pressure. Own goals sit on the same underlying action type internally and are excluded here by result.
dispossessedcarrying.dispossessedteam + player
Losing the ball to an opponent without attempting to beat them.
Losing the ball to an opponent while not attempting to beat them. Excludes losses during a take-on, which are failed take-ons, and losses from a poor first touch, which are miscontrols.
Creation
Who made the chance. The expected-goals members attribute a per-shot xG joined in from the shot data rather than modelled by us.
scacreation.scateam + player
The two offensive actions immediately preceding a shot.
FBref's Shot-Creating Actions, a construction of theirs rather than an industry standard: the two offensive actions immediately preceding a shot — passes, take-ons, fouls drawn, rebounding shots and ball-winning defensive actions. Our backward walk stops at the edge of the possession containing the shot, which FBref's does not.
gcacreation.gcateam + player
The same, restricted to shots that were scored.
The two offensive actions directly leading to a goal, on the same qualifying types as sca. Goals are far rarer than shots, so read it alongside sca rather than alone.
sca_breakdowncreation.sca_breakdownteam + player
Shot-creating actions split by what kind of action created the shot.
FBref's six categories: pass_live, pass_dead (free kick, corner, throw-in, kick-off, goal kick), take_on, shot, foul_drawn and defensive. foul_drawn appears on teams only — a foul never names who won it — so the players' sca sums short of the team's by exactly that amount. Player breakdowns omit categories with no occurrences rather than writing zeros.
second_assistscreation.second_assiststeam + player
The pass before the assist.
The standard term: a pass or cross instrumental in creating a goalscoring opportunity. Anchored to creating an attempt rather than to a scored goal and defined judgementally, so not every goal carries one.
chances_createdcreation.chances_createdteam + player
Assists plus key passes.
A standard term for assists plus key passes. Numerically identical to passing.key_passes, because our key pass already includes the passes that became goals; both names are published, but they are one number.
xagcreation.xagteam + player
The expected goals of the shots a player's passes actually produced.
Expected assisted goals: the xG of the shots a player's passes produced, credited to the passer. Not xA, the modelled likelihood that any completed pass becomes an assist, which is not served. Penalties are excluded, since a penalty has no assist.
xg_chaincreation.xg_chainplayer
The full xG of every possession the player took part in.
Sum the xG of all shots in every possession the player touched, and assign the whole sum however peripheral the involvement. The value is not divided among participants, so summing players would multiply one chance by the number who touched it and there is no team analogue.
xg_buildupcreation.xg_buildupplayer
xg_chain with the shot and the assist removed.
xg_chain excluding chains where the player's only contribution was the shot or the pass that created it, which isolates deep-lying contribution. Always less than or equal to xg_chain.
Defending
Winning the ball back, and how high up the pitch. The first seven appear on players and teams; the rest are team level only.
tacklesdefending.tacklesteam + player
Challenges that dispossessed an opponent.
The standard definition: connecting with the ball in a legal ground-level challenge and taking it from an opponent in controlled possession. Counts toward the PPDA denominator.
interceptionsdefending.interceptionsteam + player
Reading a pass and cutting it out.
Moving into the line of an intended pass and stopping it before it reaches its target. Blocked passes are folded into this count by the action conversion, so it runs high against sources that report the two separately — and needs no separate handling in PPDA.
blocksdefending.blocksteam + player
Outfield players getting in the way of a shot.
A block is awarded when an outfield player blocks an attempt on goal; none is given if the attempt was going wide. Separated from goalkeeper saves by whether the actor started in goal, and excluded from the PPDA denominator.
clearancesdefending.clearancesteam + player
Hoofing the ball away from danger, with no intended recipient.
Playing the ball away from a dangerous zone with no intended recipient. Excluded from the PPDA denominator: counting it would credit a deep-sitting team with pressing it never applied.
duels_wondefending.duels_wonteam + player
Contests for the ball that ended in this player's favour.
A duel is a 50-50 contest between two opposing players, and every won duel has a corresponding lost duel for the opponent. Ground and aerial combined.
aerialsdefending.aerialsteam + player
Contests for the ball in the air.
Every aerial duel the player contested, won or lost. Two or more players must genuinely contest it, so an unchallenged header does not qualify.
aerials_wondefending.aerials_wonteam + player
Aerial duels won.
Aerial duels won — the player who wins the ball wins the duel. Divided by aerials this gives the aerial success rate.
ppdadefending.ppdateam
Opponent passes allowed per defensive action. Lower is more intense pressing.
Passes Per Defensive Action on the standard rule: opponent passes attempted divided by fouls, tackles, interceptions, challenges and blocked passes, both counted outside the pressing team's own defensive third. Clearances and shot blocks are excluded as reactions to pressure rather than applications of it. The zone is returned as press_zone_fraction, since the original 2014 definition used three fifths.
opponent_passesdefending.opponent_passesteam
The PPDA numerator, published on its own.
Passes the opponent attempted inside the pressing zone. Exposed so the ratio can be checked rather than trusted.
defensive_actionsdefending.defensive_actionsteam
The PPDA denominator, published on its own.
Fouls, tackles, interceptions, challenges and blocked passes inside the pressing zone. Fouls count because a tactical foul is an act of pressing.
challengesdefending.challengesteam
Attempts to win the ball that did not result in a tackle.
A failed attempt to stop an opponent dribbling past. Part of the standard PPDA denominator, counted off the raw event stream because the type has no equivalent in the converted action stream.
How high up the pitch the team defended, in metres.
The mean x coordinate of tackles, interceptions, fouls and clearances, measured from the team's own goal on a 105 m pitch. This set includes clearances, unlike the PPDA denominator. It records where interventions happened, which is not defensive line height.
high_turnoversdefending.high_turnoversteam
Possessions won within 40 metres of the opponent's goal.
This is published as possessions starting 40 metres or less from the opponent's goal. Where PPDA measures pressing intent, this measures pressing outcome.
Possessions won back within five seconds of losing them.
Regains within five seconds of losing the ball, measured on the wall clock. It times the regain itself rather than the pressure, so it is not StatsBomb's counterpressure metric.
Average seconds to win the ball back after losing it.
The FIFA Enhanced Football Intelligence formulation of pressing effectiveness, elite sides landing in the low teens of seconds. Measured in ball-in-play time, so a stoppage between loss and regain does not count against the team. Null when possession was never lost and regained.
Territory
Where the match was played. Team level only.
possession_pctterritory.possession_pctteam
Share of the ball, as a share of attempted deliveries.
Each team's attempted deliveries — passes, crosses, throw-ins, corners, free kicks and goal kicks — as a proportion of both teams', reported as possession_method: pass_share. Not the standard share of possessions and not share of ball-in-play time; the same match can read 56/44 on one method and 60/40 on another.
field_tiltterritory.field_tiltteam
Share of final-third possession, not of the whole pitch.
A team's share of the two teams' combined final-third activity. No provider defines it — sources use touches, passes or both, and the final-third line itself varies. Null when neither team entered the final third.
Times the ball was carried or passed into the attacking third.
The action must cross x = 70. One already starting in the final third is not an entry no matter how far it travels.
box_entriesterritory.box_entriesteam
Times the ball entered the opponent's penalty area.
The box is treated as a real rectangle — 16.5 m deep and 40.32 m wide — not as everything beyond x = 88.5.
avg_action_xterritory.avg_action_xteam
The average position of the team's actions, in metres.
Mean start_x across a team's actions, measured from its own goal on a 105 m pitch. Unweighted and blind to action type and game state.
Tempo
How a team moved the ball, and how quickly. Team level only. Sequences follow the standard published framework: a passage of play belonging to one team, beginning with a controlled action and ended by a defensive action, a stoppage or a shot.
passes_per_sequencetempo.passes_per_sequenceteam
Average passes in an uninterrupted passage of play.
One of the core style indicators: high values mark patient circulation, low values a more direct approach. Averaged over open-play sequences, and null when there was none.
sequence_timetempo.sequence_timeteam
Average seconds a passage of play lasted.
Averaged over open-play sequences. Read beside passes_per_sequence: the same pass count over more time is a slower build.
direct_speedtempo.direct_speedteam
Metres of goal-ward progress per second of possession.
The standard measure of how quickly a team moves the ball upfield: progress toward the opponent's goal divided by sequence time. Sideways and backwards circulation depresses it. Ours reads at or slightly above the top of the published band.
buildup_attackstempo.buildup_attacksteam
Attacks built through at least 10 passes.
The standard build-up attack: an open-play sequence of 10 or more passes that ends in a shot or produces at least one touch in the opponent's box.
direct_attackstempo.direct_attacksteam
Attacks that covered at least half the distance to goal quickly.
The standard direct attack: a sequence starting just inside a team's own half that moves at least 50% of the way toward the opponent's goal and ends in a shot or box touch. The two archetypes are not mutually exclusive.
Goalkeeping
Present only for the two players who started in goal, null for everyone else. Tell an outfield player from a quiet keeper by the null, not the zero — a quiet keeper has zeros.
claimsgoalkeeping.claimsplayer
Crosses the keeper came for and caught.
The standard Claim: catching a crossed ball, successful when the keeper holds it. Punches, crosses not claimed, smothers and pick-ups are each their own event type and none is a claim.
claims_wongoalkeeping.claims_wonplayer
Claims the keeper held.
The successful subset of claims.
claim_rategoalkeeping.claim_rateplayer
Handling reliability under aerial pressure, as a percentage.
Claims won over claims attempted. Deliberately not the standard Catch Success, which puts catches plus punches over every high ball the keeper came for. Null when the keeper came for no cross.
sweeper_actionsgoalkeeping.sweeper_actionsplayer
Defensive actions taken outside the penalty area.
A geometric count of keeper actions outside the penalty area. Read it as that, not as a sweeper-keeper count: the standard Keeper Sweeper triggers at the edge of the area or beyond and also requires opposition pressure, so neither set contains the other.
distributionsgoalkeeping.distributionsplayer
Passes and kicks made by the keeper.
Every attempted distribution, from a short roll to a goal kick.
launchesgoalkeeping.launchesplayer
Distributions longer than 36.6 metres.
Distributions longer than 36.6 metres, goal kicks included. There is no universal launch threshold, so ours is published rather than implied.
launch_pctgoalkeeping.launch_pctplayer
Share of distributions that went long.
Launches over distributions. A style indicator, not a quality one: high means direct, low means building from the back. Null when the keeper attempted no distribution.
avg_pass_lengthgoalkeeping.avg_pass_lengthplayer
Average distribution length in metres.
Mean straight-line distance start to end. Read beside launch_pct: a low average with a high launch share means a keeper doing both.
The share of distributions that reach a teammate. Strongly confounded by style, since long kicks complete far less often than short ones. Null when nothing was attempted.
Shooting
The one group that does not come from the event feed. These are served through the shots and team-stats endpoints rather than through advanced analytics, so the locators below point at those responses.
xgshots[].expected_goalsteam + player
The probability a shot becomes a goal.
Estimated from shot characteristics: location, angle, body part, assist type, pattern of play and defensive pressure. Every provider trains a different model, so these totals must never be mixed into one calculation with another source's.
npxgstats → Expected goalsteam
The same total with penalties excluded.
The same total with penalties excluded. A penalty carries a near-fixed 0.76 to 0.79 xG and is won rather than created.
shotsstats → Shotsteam
Total goal attempts.
On target, off target and blocked by an outfield player. Own goals are not shots by the scoring player.
shots_on_targetstats → Shotsteam
Attempts that would have gone in but for the keeper, plus goals.
Attempts that would have entered the goal but for a save, plus all goals. There is no single standard — one common definition also counts efforts stopped on the line by a last-man defender while excluding ordinary blocks.
xg_per_shotstats → Expected goalsteam
Average quality of the chances taken.
Total xG divided by shots. It separates volume from selectivity: two sides with identical xG can differ sharply in how they got there.
goals_minus_xgstats → Expected goalsteam
Finishing over- or underperformance.
Goals scored minus expected goals over the same shot set. Numerator and denominator must cover the same shots: if the xG figure includes penalties, the goals must too.
big_chancesstats → Big chancesteam
Situations a player would be expected to score from.
Typically a one-on-one, or a close-range attempt with a clear path to goal; penalties are always big chances. An editorial classification applied by analysts, not a model output.
big_chances_missedstats → Big chancesteam
Big chances that did not produce a goal.
Uses the broad reading, any big chance not converted, where the standard published wording covers only cases where the player fails to get a shot away at all.
conversion_ratestats → Shotsteam
Goals divided by shots.
Goals divided by shots over all attempts, which matches the standard Shot Conversion. Some sources compute it over shots on target instead, which is a different question.
savesplayers[].stats → top_statsplayer
Shots the keeper stopped from entering the goal.
Preventing the ball entering the goal with any part of the body, facing an intentional attempt from an opponent. Penalties are included; an intervention by a defender is a block, and routine collections of harmless balls are excluded.
API reference
Matches
Everything attached to one fixture: the summary, shots, momentum, events, player stats, lineups, head-to-head, and team stats.
GET/v1/matches/{id}
Retrieve a match
The match summary: league, teams, score, status, referee, and stadium.
Every shot in the match, grouped by half. Each shot has pitch coordinates for the strike point plus goal-line coordinates for where it crossed or missed.
Goals, cards, subs, and penalties in order, each with the running score at that moment. `period` is inferred from the minute, not reported by upstream, so treat it as a convenience.
Per-player stats grouped by category. Which groups appear depends on position and involvement. Read stats by `stat.key`, not the label. When parsing, check `stat.type`: integer and double carry a `value`, fraction types also carry a `total`.
Formations, starters, and benches for both sides. Starters carry normalised coordinates for drawing the formation; subs are always zero. For a fixture that has not kicked off the sides may still be a pre-match prediction, so check `confirmed` before trusting the XI; `lineup_type` names the prediction and is omitted once the lineup is confirmed.
Head-to-head history plus recent and upcoming meetings. Unplayed fixtures have null scores and `finished: false` — check `finished` before trusting a score.
Team-by-team totals per half and for the full match. Values are strings, so parse them with `format_type`. An empty `format_type` means a plain number. Groups come back in alphabetical order.
Team-level analytics derived from the raw event feed: possession value, passing, carrying, creation, defending, territory, and tempo. Exactly two objects, home first. Returns ANALYTICS_UNAVAILABLE for a match we hold but never rated.
The same groups per player, minus the members that only exist for a team. Sorted by possession value descending, falling back to actions. goalkeeping is null for outfield players.
The passing graph of each team as three layers: a per-team summary (window and centralization), one node per player, and one edge per ordered pair of teammates. Exactly two objects, home first. The window closes at the team's first substitution, so the numbers describe the starting eleven's circulation. Receivers are inferred, so edges can sum to less than nodes. Returns ANALYTICS_UNAVAILABLE for a match we hold but never rated.
The same player object as the list endpoint, unwrapped. The example below is a goalkeeper, so it carries the goalkeeping group; the passing, carrying, creation and defending groups are shortened here and hold exactly the fields shown in the list endpoint above. A player who took no part in a match we did rate returns RESOURCE_NOT_FOUND, which is distinct from ANALYTICS_UNAVAILABLE for a match that was never rated.
Where each side and each player acted, binned onto a 16 x 12 grid of the pitch. Two team grids, home first, then one grid per player who touched the ball, busiest first. Cells are sparse [cell_x, cell_y, actions] triples — a cell with no activity is absent rather than zero, which is most of the 192 — so expand to a dense grid yourself if you need one. Read grid.frame before plotting: in acting_ltr, cell_x 0 is the acting team's own goal line and 15 the opponent's, for both teams and in both halves. Each grid is therefore correct on its own pitch, but the two are not on a shared one. To draw both teams together, ask for frame=home_ltr and the server turns the away side for you — there is nothing to mirror client-side. Every team and player also carries side, home or away. grid.frame echoes the orientation actually served. actions is a raw count for the match and not per-90, so a substitute's grid is thinner than a starter's for reasons that are about minutes rather than involvement. This is match level: there is no season or cross-match aggregate. Returns ANALYTICS_UNAVAILABLE for a match we hold but never rated.
Path parameters
idstringrequired
Match ID, for example m_B8x2K9
Query parameters
framestring
acting_ltr (default) gives every team its own attacking frame, which is what makes a player comparable across home and away fixtures. home_ltr puts both teams on one pitch — home attacking cell_x 15, away attacking 0 — for a match view. The server applies the turn.
The leagues in the catalogue. Every one of them is reachable with any key.
GET/v1/leagues
List leagues
Every league, each with the seasons available. The is_free flag is now true for all of them and is kept only so clients written against the old paid tier keep parsing.
Results for a season, and its upcoming fixtures. Pass `season` to pick one: 2024/2025 for fall-spring leagues, 2024 for calendar-year leagues. Defaults to the current season. By default only played matches are returned; pass `status=upcoming` or `status=all` for the fixtures.
Path parameters
idstringrequired
League ID, for example l_4Kd0Wq
Query parameters
seasonstring
Season to fetch, for example 2024/2025. Defaults to the current season.
statusstring
Which matches to return: played (default — settled results only), upcoming (fixtures not yet played, with a null score and status not_started), or all. Omit it to keep the historical played-only response.
Find matches by date when you don't know the match ID in advance, and browse upcoming fixtures.
GET/v1/date/{date}
List matches by date
The covered matches on that day. The usual entry point for browsing: take a match ID and follow it into the match endpoints. By default only played matches are returned, so a future date is empty unless you pass `status=upcoming` or `status=all` for the fixtures.
Path parameters
datestringrequired
Calendar date as YYYY-MM-DD
Query parameters
statusstring
Which matches to return: played (default — settled results only), upcoming (fixtures not yet played, with a null score and status not_started), or all. Omit it to keep the historical played-only response.
Notable, caller-visible changes to the API — new endpoints, fields and parameters. Newest first.
September 19, 2026
26 leagues are back, rebuilt from Opta
The 26 leagues added on September 11 — among them the Eerste Divisie, HNL, Czech 1. Liga, MLS Next Pro, the USL Championship and League One, Brasileirão Série B and the Qatar, UAE, Colombian and Peruvian top flights — were withdrawn while we rebuilt them from Opta's own feeds. They are served again, from the 2024 seasons on, with every played match carrying lineups, events, shots with xG, team and player stats and advanced analytics.
Ids changed. Matches of these leagues, and the teams and players that appear only in them, have new ids. An id stored between September 11 and the withdrawal now answers 404; look the match up again by date or league. Clubs and players that also play in the other 44 leagues keep the ids they had.
What differs from the other leagues:
/momentum returns an empty points array.
Player stat lines carry no rating.
Lineups are confirmed ones only, published about 30 minutes before kickoff; there is no predicted lineup.
Many teams and players have no image yet.
For Latvia's Virsliga and Venezuela's Primera División, match and player stats are derived from the match events, and the few that events cannot reproduce (big chances, chances created) are left out.
September 6, 2026
Heatmaps: choose the frame, and read the side
Heatmap cells are stored with every team attacking towards cell_x 15 in its own frame. That is right for a player — it is what lets you compare them home and away — but it means the two grids are not on a shared pitch, and plotting them together drew one side backwards with nothing obviously wrong.
?frame=home_ltr now returns both teams on one pitch, the away side already turned. The default, acting_ltr, is unchanged, so existing calls return exactly what they did before; grid.frame echoes whichever you got.
Every team and player also carries side, "home" or "away", so a single grid can be oriented without a second call.
September 5, 2026
Team and player heatmaps
A new endpoint, GET /v1/matches/{id}/heatmaps, returns where each side and each player acted, binned onto a 16 x 12 grid of the pitch. Two team grids, home first, then one grid per player who touched the ball, busiest first.
Cells are sparse [cell_x, cell_y, actions] triples: a cell with no activity is absent rather than zero, which is most of the 192. Read grid.frame before plotting — in acting_ltr, cell_x0 is the acting team's own goal line and 15 the opponent's, for both teams and in both halves, so a client that assumes cell_x 0 is the left touchline draws one side mirrored.
Match level only. There is no season or cross-match aggregate behind this, and actionsis a raw count rather than per-90 — a substitute's grid is thinner than a starter's for reasons that are about minutes, not involvement.
August 31, 2026
Predicted lineups for upcoming matches
The lineups endpoint now answers before a match is played. For a fixture kicking off within 48 hours, GET /v1/matches/{id}/lineups returns each side's predicted lineup — formation, starting eleven, bench, and coach — instead of an empty result.
Every side now carries a confirmed boolean. false marks a prediction (with a lineup_type such as "lastStarting11"); it flips to true when the confirmed lineup is published near kickoff, and stays the actual lineup once the match is played. The match ID is stable throughout, so the same request follows a lineup from prediction to result.
Backward compatible. Played matches are unchanged, and confirmed reads true for every lineup returned before this change.
August 31, 2026
Upcoming fixtures and schedules
You can now read matches before they are played. The date listing and the league-matches listing accept a status filter:
played — the default, settled results only.
upcoming — fixtures not yet played. These carry status: "not_started" and a null score until the match is played.
all — both together.
Fixtures cover the whole current season for every league in the catalogue, and their kickoff times are kept in sync daily — a match the source reschedules updates in place. Once a match is played it fills in with the full result and analytics, so a match ID is stable from fixture to result.
Backward compatible. The default is played, so a request that omits status returns exactly what it returned before. Opt in only where you want the fixtures.
Something missing?
If a field you need is not documented here or a response does not match this reference, let us know. The documentation is part of the product and we treat any mismatch as a bug.