# PitchAPI documentation > A read-only REST API for football data: shot-level detail with pitch and goal-line coordinates, lineups, momentum, match stats, and advanced analytics derived from the event feed. 70 leagues, full history back to 2021. This is the complete reference: 24 endpoints and 88 metrics. It is generated from the same source as https://pitchapi.dev, so the two cannot disagree. ## Quickstart 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. Base URL: https://api.pitchapi.dev All requests must use HTTPS. Plain HTTP is refused rather than redirected, so a misconfigured client never leaks its key. curl https://api.pitchapi.dev/v1/date/2025-11-09 -H "X-API-KEY: $PITCH_KEY" ## SDKs Official client libraries wrap the REST API so you work with typed objects instead of raw JSON: they handle the response envelope, map errors to typed exceptions, and retry on 429 and 5xx with the `Retry-After` delay honoured. Python (3.9+) is available today; more languages are on the way. pip install pitchapi from pitchapi import PitchAPI with PitchAPI(api_key="pk_live_...") as client: day = client.date.get("2025-11-09") match = client.matches.get("m_B8x2K9") shots = client.matches.shots(match.id) advanced = client.matches.advanced(match.id) The key can also be read from the `PITCHAPI_API_KEY` environment variable. `AsyncPitchAPI` is the exact twin of `PitchAPI` — same names, same arguments, same models, awaited. Two behaviours to know. Match listings return played matches by default: pass `status="upcoming"` or `status="all"` to `client.date.get()` or `client.leagues.matches()` for fixtures, whose scores are None until played. And a lineup published before kickoff may be a prediction, so check `side.confirmed` before trusting the XI — `side.lineup_type` names the prediction and is None once confirmed. Every exception derives from `PitchAPIError` and carries `code`, `status_code` and `request_id` where the API supplied them. Namespaces, identical on both clients: - `client.date` (1 method) — get(date, status=None) — every match on a day. date takes a YYYY-MM-DD string or a datetime.date. - `client.matches` (18 methods) — get, shots, shot, events, lineups, momentum, stats, players, player, players_halves, player_halves, player_shots, h2h, advanced, advanced_network, advanced_players, advanced_player, heatmaps. - `client.leagues` (3 methods) — list, get, matches(id, season=None, status=None) — season defaults to the latest one with data. - `client.teams` (1 method) — get — the team profile. - `client.players` (1 method) — get — the player profile. ## Authentication Pass your API key in the `X-API-KEY` header on every request. Production keys use the `pk_live_` prefix, test keys `pk_test_`. The 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. Only a SHA-256 hash of the key is stored, so a lost key must be replaced rather than recovered. ## 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. { "data": { "id": "m_B8x2K9", "status": "finished" } } { "error": { "code": "RESOURCE_NOT_FOUND", "message": "match not found" } } Timestamps are RFC 3339 in UTC, dates are YYYY-MM-DD. Optional fields are omitted when unavailable, and 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. ## Resource IDs IDs are opaque and prefixed by resource type. Do not parse them, and do not assume they are numeric or sortable. - `m_` — Match, for example `m_B8x2K9` - `s_` — Shot, for example `s_Q1pR6z` - `p_` — Player, for example `p_7YtX4q` - `t_` — Team, for example `t_9aB2xQ` - `l_` — League, for example `l_4Kd0Wq` A shot ID is scoped to its match rather than globally unique. ## Plans and coverage - **Free** (No charge) — 70 leagues, 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. ## Error codes - `UNAUTHORIZED` (401) — The X-API-KEY header is missing or the key is invalid. - `RATE_LIMIT_EXCEEDED` (429) — 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. - `INVALID_PARAMETER` (400) — A path or query parameter isn't in the expected format (date, ID pattern, etc.). - `RESOURCE_NOT_FOUND` (404) — No resource exists with that ID. - `ANALYTICS_UNAVAILABLE` (404) — 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. - `HALVES_UNAVAILABLE` (404) — Player-halves routes only. The match exists but has no per-half player stats. Kept separate from RESOURCE_NOT_FOUND so you can tell it from a bad match ID. - `INTERNAL_SERVER_ERROR` (500) — Something went wrong on our side. Retry with backoff. ## 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 = 105 regardless of which side took it, so the axis never needs flipping. Every distance in the advanced analytics uses the same pitch and the same units. `goal_crossed_y` is measured across the full pitch width, not the goal opening alone. 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, so a client that plots cells without reading `grid.frame` draws one side mirrored. On the pitch: - `x` (number, 0 to 105) — Distance along the pitch in metres, normalised so the attacking goal is always at 105. - `y` (number, 0 to 68) — Lateral position across the pitch in metres. The centre line is at 34. On the goal line: - `goal_crossed_y` (number, 0 to 68) — Where the shot crossed the goal line on the same y axis as the pitch. - `goal_crossed_z` (number, 0 to 7.6) — Height above the ground where the shot crossed the goal line. Constants: - 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) ## Shot fields - `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. ## Player stats Player stats arrive in groups, and each stat carries a type that says how to read its value. - `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. Stat types: - `integer` (value) — A whole number (goals, touches, clearances). - `double` (value) — A decimal number (ratings, xG). - `fractionWithPercentage` (value + total) — A made-attempted pair (accurate passes, duels won). Carries both value and total. - `distanceWithPercentage` (value + total) — Metres covered out of a total distance (running splits). - `distance` (value) — A distance in metres. - `speed` (value) — A speed in km/h. - `fantasyPoints` (value) — A fantasy football score. - `boolean` (value) — A presence flag, not a measurement. Skip it when parsing. ## Lineups - `formation` (string) — Formation at kick-off (e.g. 4-2-3-1). - `confirmed` (boolean) — 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` (string) — Label for a predicted lineup (e.g. lastStarting11). Omitted once the lineup is confirmed. - `pitch_x / pitch_y` (number, 0 to 1) — Position in the formation, normalised to 0 to 1. Both are 0 for every substitute. - `position_id` (number) — 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` (boolean) — Whether the player was captain at kick-off. - `coach` (object) — The coach (name). Omitted when unavailable. ## Momentum - `minute` (number, 0 to 90.75) — Match minute. Fractional values mark stoppage and added extra time. - `value` (number, -100 to 100) — Pressure index at this minute. Positive favours home, negative favours away. ## Events - `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). ## Team stats Team stats are strings carrying a format that says how to parse them. - `integer` ("14") — A whole number as a string. - `integerWithPercentage` ("329 (82%)") — A count and its percentage in one string. The number precedes the space. - `double` ("2.92") — A decimal as a string. - `distance` ("115804") — A distance in metres as a string. - `""` ("37") — An empty format_type but still a plain number. ## Metrics 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. Two pairs 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. #### `actions` Response path: `actions` (team and player level) 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_played` Response path: `minutes_played` (player level) 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_total` Response path: `possession_value.xt_total` (team and player level) 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_total` Response path: `possession_value.vaep_total` (team and player level) 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_offensive` Response path: `possession_value.vaep_offensive` (team and player level) 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_defensive` Response path: `possession_value.vaep_defensive` (team and player level) 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_total` Response path: `possession_value.pv_total` (team and player level) 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_offensive` Response path: `possession_value.pv_offensive` (team and player level) 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_defensive` Response path: `possession_value.pv_defensive` (team and player level) 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_x` Response path: `networks[].nodes[].avg_x` (team level) 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_y` Response path: `networks[].nodes[].avg_y` (team level) 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. #### `degree` Response path: `networks[].nodes[].degree` (team level) 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. #### `strength` Response path: `networks[].nodes[].strength` (team level) 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. #### `betweenness` Response path: `networks[].nodes[].betweenness` (team level) 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. #### `clustering` Response path: `networks[].nodes[].clustering` (team level) 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. #### `centralization` Response path: `networks[].centralization` (team level) 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. #### `passes` Response path: `passing.passes` (team and player level) 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_accuracy` Response path: `passing.pass_accuracy` (team and player level) 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_passes` Response path: `passing.progressive_passes` (team and player level) 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_distance` Response path: `passing.progressive_pass_distance` (team and player level) 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_box` Response path: `passing.passes_into_box` (team and player level) 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_passes` Response path: `passing.key_passes` (team and player level) 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. #### `assists` Response path: `passing.assists` (team and player level) 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_balls` Response path: `passing.through_balls` (team and player level) 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. #### `crosses` Response path: `passing.crosses` (team and player level) 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. #### `switches` Response path: `passing.switches` (team and player level) 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. #### `carries` Response path: `carrying.carries` (team and player level) 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_distance` Response path: `carrying.carry_distance` (team and player level) 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_carries` Response path: `carrying.progressive_carries` (team and player level) 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_distance` Response path: `carrying.progressive_carry_distance` (team and player level) 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_third` Response path: `carrying.carries_into_final_third` (team and player level) 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_box` Response path: `carrying.carries_into_box` (team and player level) 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_ons` Response path: `carrying.take_ons` (team and player level) 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_won` Response path: `carrying.take_ons_won` (team and player level) 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. #### `miscontrols` Response path: `carrying.miscontrols` (team and player level) 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. #### `dispossessed` Response path: `carrying.dispossessed` (team and player level) 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. #### `sca` Response path: `creation.sca` (team and player level) 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. #### `gca` Response path: `creation.gca` (team and player level) 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_breakdown` Response path: `creation.sca_breakdown` (team and player level) 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_assists` Response path: `creation.second_assists` (team and player level) 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_created` Response path: `creation.chances_created` (team and player level) 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. #### `xag` Response path: `creation.xag` (team and player level) 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_chain` Response path: `creation.xg_chain` (player level) 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_buildup` Response path: `creation.xg_buildup` (player level) 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. #### `tackles` Response path: `defending.tackles` (team and player level) 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. #### `interceptions` Response path: `defending.interceptions` (team and player level) 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. #### `blocks` Response path: `defending.blocks` (team and player level) 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. #### `clearances` Response path: `defending.clearances` (team and player level) 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_won` Response path: `defending.duels_won` (team and player level) 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. #### `aerials` Response path: `defending.aerials` (team and player level) 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_won` Response path: `defending.aerials_won` (team and player level) Aerial duels won. Aerial duels won — the player who wins the ball wins the duel. Divided by aerials this gives the aerial success rate. #### `ppda` Response path: `defending.ppda` (team level) 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_passes` Response path: `defending.opponent_passes` (team level) 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_actions` Response path: `defending.defensive_actions` (team level) 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. #### `challenges` Response path: `defending.challenges` (team level) 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. #### `avg_defensive_action_x` Response path: `defending.avg_defensive_action_x` (team level) 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_turnovers` Response path: `defending.high_turnovers` (team level) 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. #### `counterpress_regains_5s` Response path: `defending.counterpress_regains_5s` (team level) 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. #### `ball_recovery_time` Response path: `defending.ball_recovery_time` (team level) 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_pct` Response path: `territory.possession_pct` (team level) 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_tilt` Response path: `territory.field_tilt` (team level) 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. #### `final_third_entries` Response path: `territory.final_third_entries` (team level) 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_entries` Response path: `territory.box_entries` (team level) 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_x` Response path: `territory.avg_action_x` (team level) 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_sequence` Response path: `tempo.passes_per_sequence` (team level) 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_time` Response path: `tempo.sequence_time` (team level) 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_speed` Response path: `tempo.direct_speed` (team level) 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_attacks` Response path: `tempo.buildup_attacks` (team level) 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_attacks` Response path: `tempo.direct_attacks` (team level) 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. #### `claims` Response path: `goalkeeping.claims` (player level) 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_won` Response path: `goalkeeping.claims_won` (player level) Claims the keeper held. The successful subset of claims. #### `claim_rate` Response path: `goalkeeping.claim_rate` (player level) 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_actions` Response path: `goalkeeping.sweeper_actions` (player level) 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. #### `distributions` Response path: `goalkeeping.distributions` (player level) Passes and kicks made by the keeper. Every attempted distribution, from a short roll to a goal kick. #### `launches` Response path: `goalkeeping.launches` (player level) 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_pct` Response path: `goalkeeping.launch_pct` (player level) 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_length` Response path: `goalkeeping.avg_pass_length` (player level) 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. #### `distribution_accuracy` Response path: `goalkeeping.distribution_accuracy` (player level) Share of distributions that found a teammate. 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. #### `xg` Response path: `shots[].expected_goals` (team and player level) 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. #### `npxg` Response path: `stats → Expected goals` (team level) 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. #### `shots` Response path: `stats → Shots` (team level) Total goal attempts. On target, off target and blocked by an outfield player. Own goals are not shots by the scoring player. #### `shots_on_target` Response path: `stats → Shots` (team level) 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_shot` Response path: `stats → Expected goals` (team level) 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_xg` Response path: `stats → Expected goals` (team level) 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_chances` Response path: `stats → Big chances` (team level) 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_missed` Response path: `stats → Big chances` (team level) 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_rate` Response path: `stats → Shots` (team level) 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. #### `saves` Response path: `players[].stats → top_stats` (player level) 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 Every endpoint takes an `X-API-KEY` header and returns the envelope described above. ### 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "id": "m_B8x2K9", "league": { "id": "l_4Kd0Wq", "name": "Premier League", "image_url": "https://cdn.pitchapi.dev/leagues/47.webp" }, "home_team": { "id": "t_9aB2xQ", "name": "Manchester City", "image_url": "https://cdn.pitchapi.dev/teams/8456.webp" }, "away_team": { "id": "t_2mLp7C", "name": "Manchester United", "image_url": "https://cdn.pitchapi.dev/teams/10260.webp" }, "date": "2025-11-09", "time_utc": "2025-11-09T16:30:00Z", "status": "finished", "score_home": 3, "score_away": 1, "round_name": "11", "has_playoff": false, "referee": "Michael Oliver", "stadium": "Etihad Stadium" } } ``` #### GET /v1/matches/{id}/shots List shots. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "periods": [ { "period": "FirstHalf", "shots": [ { "id": "s_Q1pR6z", "player": { "id": "p_7YtX4q", "name": "E. Haaland", "position_id": 4, "image_url": "https://cdn.pitchapi.dev/players/994226.webp" }, "team_id": "t_9aB2xQ", "x": 88.5, "y": 42.3, "expected_goals": 0.38, "expected_goals_on_target": 0.32, "is_on_target": true, "goal_crossed_y": 35.98, "goal_crossed_z": 0.8, "is_inside_box": true, "shot_type": "RightFoot", "situation": "RegularPlay", "minute": 34, "event_type": "Goal" } ] } ] } } ``` #### GET /v1/matches/{id}/shots/{shot_id} Retrieve a shot. One shot. Shot IDs are match-scoped, so the same value in a different match is a different shot. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 - `shot_id` (string, required) — Shot ID scoped to the match, for example s_Q1pR6z Response: ```json { "data": { "id": "s_Q1pR6z", "player": { "id": "p_7YtX4q", "name": "E. Haaland" }, "team_id": "t_9aB2xQ", "x": 88.5, "y": 42.3, "expected_goals": 0.38, "is_on_target": true, "shot_type": "RightFoot", "situation": "RegularPlay", "minute": 34, "event_type": "Goal" } } ``` #### GET /v1/matches/{id}/momentum Retrieve momentum. A minute-by-minute pressure curve. Positive values favour home, negative favour away. Some matches return an empty array. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "points": [ { "minute": 1.00, "value": 0.02 }, { "minute": 2.00, "value": 0.15 }, { "minute": 34.00, "value": 0.87 }, { "minute": 45.75, "value": 0.18 } ] } } ``` #### GET /v1/matches/{id}/events List events. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "events": [ { "event_type": "Goal", "minute": 34, "minute_added": 0, "period": "FirstHalf", "team_id": "t_9aB2xQ", "player": { "id": "p_7YtX4q", "name": "E. Haaland" }, "score_home": 1, "score_away": 0, "is_own_goal": false, "is_penalty": false }, { "event_type": "Substitution", "minute": 62, "minute_added": 0, "period": "SecondHalf", "team_id": "t_2mLp7C", "player": { "id": "p_3XvN8w", "name": "M. Mount" }, "sub_in_player": { "id": "p_5RtY2k", "name": "A. Garnacho" }, "is_own_goal": false, "is_penalty": false } ] } } ``` #### GET /v1/matches/{id}/players List player stats. 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`. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": [ { "player": { "id": "p_7YtX4q", "name": "E. Haaland", "position_id": 4, "image_url": "https://cdn.pitchapi.dev/players/994226.webp" }, "team_id": "t_9aB2xQ", "stats": [ { "key": "top_stats", "stats": { "Rating": { "key": "rating_title", "stat": { "type": "double", "value": 8.9 } }, "Goals": { "key": "goals", "stat": { "type": "integer", "value": 2 } }, "Assists": { "key": "assists", "stat": { "type": "integer", "value": 0 } }, "xG + xA": { "key": "xg_and_xa", "stat": { "type": "double", "value": 1.95 } }, "Total shots": { "key": "total_shots", "stat": { "type": "integer", "value": 5 } }, "Shot accuracy": { "key": "shot_accuracy", "stat": { "type": "fractionWithPercentage", "value": 3, "total": 5 } } } }, { "key": "attack", "stats": { "Touches": { "key": "touches", "stat": { "type": "integer", "value": 41 } } } }, { "key": "duels", "stats": {} }, { "key": "defense", "stats": {} } ] } ] } ``` #### GET /v1/matches/{id}/players/{player_id} Retrieve a player's match stats. The stat line for one player. Same grouped shape as the list endpoint. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 - `player_id` (string, required) — Player ID, for example p_7YtX4q Response: ```json { "data": { "player": { "id": "p_7YtX4q", "name": "E. Haaland", "position_id": 4 }, "team_id": "t_9aB2xQ", "position_id": 4, "stats": [ { "key": "top_stats", "stats": { "Rating": { "key": "rating_title", "stat": { "type": "double", "value": 8.9 } }, "Goals": { "key": "goals", "stat": { "type": "integer", "value": 2 } } } } ] } } ``` #### GET /v1/matches/{id}/players/halves List player stats per half. Every player's stat line for the first and the second half. Each half's stats has the grouped shape of the list endpoint, though not every stat is available per half — the rating, for one, is match-level only. periods lists only the halves a player took part in, so a half-time substitute has just SecondHalf. Extra time belongs to neither half, and the halves need not add up exactly to the match line. Returns HALVES_UNAVAILABLE for a match we hold without per-half player stats. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "players": [ { "player": { "id": "p_7YtX4q", "name": "E. Haaland", "position_id": null, "image_url": "https://cdn.pitchapi.dev/players/994226.webp" }, "team_id": "t_9aB2xQ", "periods": [ { "period": "FirstHalf", "stats": [ { "key": "top_stats", "title": "Top stats", "stats": { "Minutes played": { "key": "minutes_played", "stat": { "type": "integer", "value": 45 } }, "Goals": { "key": "goals", "stat": { "type": "integer", "value": 1 } }, "Accurate passes": { "key": "accurate_passes", "stat": { "type": "fractionWithPercentage", "value": 9, "total": 12 } }, "Expected goals (xG)": { "key": "expected_goals", "stat": { "type": "double", "value": 0.71 } } } }, { "key": "attack", "title": "Attack", "stats": {} }, { "key": "defense", "title": "Defense", "stats": {} }, { "key": "duels", "title": "Duels", "stats": {} } ] }, { "period": "SecondHalf", "stats": [ { "key": "top_stats", "title": "Top stats", "stats": { "Minutes played": { "key": "minutes_played", "stat": { "type": "integer", "value": 45 } }, "Goals": { "key": "goals", "stat": { "type": "integer", "value": 1 } } } } ] } ] } ] } } ``` #### GET /v1/matches/{id}/players/{player_id}/halves Retrieve a player's stats per half. The halves of one player: the same object as an entry of the list endpoint, with the match ID. A player who took no part in a match that has halves returns RESOURCE_NOT_FOUND, which is distinct from HALVES_UNAVAILABLE for a match that has none. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 - `player_id` (string, required) — Player ID, for example p_7YtX4q Response: ```json { "data": { "match_id": "m_B8x2K9", "player": { "id": "p_7YtX4q", "name": "E. Haaland", "position_id": null }, "team_id": "t_9aB2xQ", "periods": [ { "period": "FirstHalf", "stats": [ { "key": "top_stats", "title": "Top stats", "stats": { "Minutes played": { "key": "minutes_played", "stat": { "type": "integer", "value": 45 } } } } ] }, { "period": "SecondHalf", "stats": [ { "key": "top_stats", "title": "Top stats", "stats": { "Minutes played": { "key": "minutes_played", "stat": { "type": "integer", "value": 45 } } } } ] } ] } } ``` #### GET /v1/matches/{id}/players/{player_id}/shots List a player's shots. All of one player's shots in a match. Use this to build a single-player shotmap without filtering the full shot list. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 - `player_id` (string, required) — Player ID, for example p_7YtX4q Response: ```json { "data": { "player": { "id": "p_7YtX4q", "name": "E. Haaland" }, "shots": [ { "id": "s_Q1pR6z", "x": 88.5, "y": 42.3, "expected_goals": 0.38, "is_on_target": true, "goal_crossed_y": 35.98, "goal_crossed_z": 0.8, "shot_type": "RightFoot", "minute": 34, "event_type": "Goal" } ] } } ``` #### GET /v1/matches/{id}/lineups Retrieve lineups. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "home_team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "away_team": { "id": "t_2mLp7C", "name": "Manchester United" }, "home": { "formation": "4-2-3-1", "confirmed": true, "coach": { "name": "Pep Guardiola" }, "starters": [ { "player_id": "p_7YtX4q", "name": "E. Haaland", "shirt_number": "9", "position_id": 4, "is_captain": false, "pitch_x": 0.5, "pitch_y": 0.89 } ], "subs": [] }, "away": { "formation": "4-3-3", "confirmed": true, "starters": [], "subs": [] } } } ``` #### GET /v1/matches/{id}/h2h Retrieve head-to-head. Head-to-head history plus recent and upcoming meetings. Unplayed fixtures have null scores and `finished: false` — check `finished` before trusting a score. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "home_team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "away_team": { "id": "t_2mLp7C", "name": "Manchester United" }, "home_wins": 12, "draws": 5, "away_wins": 7, "total_matches": 24, "recent_matches": [ { "time_utc": "2025-04-06T15:30:00.000Z", "home": { "id": "t_2mLp7C", "name": "Manchester United" }, "away": { "id": "t_9aB2xQ", "name": "Manchester City" }, "score_home": 1, "score_away": 2, "finished": true }, { "time_utc": "2026-01-25T14:00:00.000Z", "home": { "id": "t_9aB2xQ", "name": "Manchester City" }, "away": { "id": "t_2mLp7C", "name": "Manchester United" }, "score_home": null, "score_away": null, "finished": false } ] } } ``` #### GET /v1/matches/{id}/stats Retrieve team stats. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "home_team": { "id": "t_9aB2xQ", "name": "Marseille" }, "away_team": { "id": "t_2mLp7C", "name": "Rennes" }, "periods": [ { "period": "All", "groups": [ { "group_name": "Expected goals (xG)", "items": [ { "title": "Expected goals (xG)", "key": "expected_goals", "home": "2.92", "away": "2.49", "format_type": "double" }, { "title": "xG on target (xGOT)", "key": "expected_goals_on_target", "home": "2.79", "away": "1.17", "format_type": "double" }, { "title": "xG open play", "key": "expected_goals_open_play", "home": "2.85", "away": "2.23", "format_type": "double" }, { "title": "xG set play", "key": "expected_goals_set_play", "home": "0.07", "away": "0.26", "format_type": "double" } ] }, { "group_name": "Top stats", "items": [ { "title": "Ball possession", "key": "BallPossesion", "home": "45", "away": "55", "format_type": "integer" }, { "title": "Accurate passes", "key": "accurate_passes", "home": "329 (82%)", "away": "394 (85%)", "format_type": "integerWithPercentage" }, { "title": "Touches in opposition box", "key": "touches_opp_box", "home": "37", "away": "51", "format_type": "" } ] } ] } ] } } ``` #### GET /v1/matches/{id}/advanced Retrieve advanced team analytics. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "teams": [ { "team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "actions": 1357, "possession_value": { "xt_total": 1.216, "vaep_total": 0.2989, "vaep_offensive": 0.5976, "vaep_defensive": -0.2987, "pv_total": 0.5123, "pv_offensive": 0.7148, "pv_defensive": -0.2025 }, "passing": { "passes": 896, "pass_accuracy": 89.5, "progressive_passes": 62, "progressive_pass_distance": 859.9, "passes_into_box": 33, "key_passes": 26, "assists": 1, "through_balls": 4, "crosses": 21, "switches": 8 }, "carrying": { "carries": 259, "carry_distance": 2833.5, "progressive_carries": 34, "progressive_carry_distance": 1754.7, "carries_into_final_third": 28, "carries_into_box": 15, "take_ons": 30, "take_ons_won": 14, "miscontrols": 20, "dispossessed": 12 }, "creation": { "sca": 38, "gca": 2, "chances_created": 26, "second_assists": 3, "sca_breakdown": { "pass_live": 30, "pass_dead": 4, "take_on": 1, "shot": 1, "foul_drawn": 2, "defensive": 0 }, "xag": 1.21 }, "defending": { "tackles": 11, "interceptions": 16, "blocks": 5, "clearances": 12, "duels_won": 20, "aerials": 24, "aerials_won": 12, "ppda": 5.10, "opponent_passes": 279, "defensive_actions": 55, "challenges": 6, "avg_defensive_action_x": 33.4, "high_turnovers": 10, "counterpress_regains_5s": 14, "ball_recovery_time": 16.7 }, "territory": { "possession_pct": 74.2, "field_tilt": 75.6, "final_third_entries": 126, "box_entries": 41, "avg_action_x": 55.3 }, "tempo": { "passes_per_sequence": 5.96, "sequence_time": 22.12, "direct_speed": 1.35, "buildup_attacks": 16, "direct_attacks": 9 } } ] } } ``` #### GET /v1/matches/{id}/advanced/players List advanced player analytics. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "sorted_by": "possession_value.vaep_total", "players": [ { "player": { "id": "p_7YtX4q", "name": "E. Haaland", "shirt_number": 9 }, "team_id": "t_9aB2xQ", "minutes_played": 90, "actions": 33, "possession_value": { "xt_total": 0.0051, "vaep_total": 0.8104, "vaep_offensive": 0.8257, "vaep_defensive": -0.0154, "pv_total": 0.2871, "pv_offensive": 0.4126, "pv_defensive": -0.1255 }, "passing": { "passes": 12, "pass_accuracy": 75.0, "progressive_passes": 1, "progressive_pass_distance": 8.4, "passes_into_box": 0, "key_passes": 2, "assists": 0, "through_balls": 0, "crosses": 0, "switches": 0 }, "carrying": { "carries": 7, "carry_distance": 61.3, "progressive_carries": 1, "progressive_carry_distance": 34.8, "carries_into_final_third": 1, "carries_into_box": 2, "take_ons": 3, "take_ons_won": 1, "miscontrols": 2, "dispossessed": 1 }, "creation": { "sca": 3, "gca": 1, "chances_created": 2, "second_assists": 0, "sca_breakdown": { "pass_live": 2, "take_on": 1 }, "xag": 0.34, "xg_chain": 1.02, "xg_buildup": 0.18 }, "defending": { "tackles": 1, "interceptions": 0, "blocks": 0, "clearances": 1, "duels_won": 2, "aerials": 6, "aerials_won": 3 }, "goalkeeping": null } ] } } ``` #### GET /v1/matches/{id}/advanced/network Retrieve both teams' pass networks. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 Response: ```json { "data": { "match_id": "m_B8x2K9", "networks": [ { "team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "window": { "kind": "first_substitution", "until_seconds": 4225.0 }, "centralization": 0.2333, "nodes": [ { "player": { "id": "p_7YtX4q", "name": "E. Haaland" }, "avg_x": 64.85, "avg_y": 54.37, "passes": 27, "passes_received": 26, "degree": 10, "strength": 52, "betweenness": 0.1333, "clustering": 0.2181 } ], "edges": [ { "from": { "id": "p_7YtX4q", "name": "E. Haaland" }, "to": { "id": "p_2Mk8Rw", "name": "K. De Bruyne" }, "passes": 9 } ] } ] } } ``` #### GET /v1/matches/{id}/advanced/players/{player_id} Retrieve one player's advanced analytics. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 - `player_id` (string, required) — Player ID, for example p_7YtX4q Response: ```json { "data": { "match_id": "m_B8x2K9", "player": { "id": "p_3Nc8Vw", "name": "Ederson", "shirt_number": 31 }, "team_id": "t_9aB2xQ", "minutes_played": 90, "actions": 41, "possession_value": { "xt_total": 0.0142, "vaep_total": 0.0311, "vaep_offensive": 0.0418, "vaep_defensive": -0.0107 }, "passing": { "passes": 34, "pass_accuracy": 79.4 }, "carrying": { "carries": 4, "carry_distance": 22.8 }, "creation": { "sca": 1, "gca": 0, "xag": null }, "defending": { "tackles": 0, "interceptions": 1 }, "goalkeeping": { "claims": 5, "claims_won": 5, "claim_rate": 100.0, "sweeper_actions": 9, "distributions": 38, "launches": 14, "launch_pct": 36.8, "avg_pass_length": 31.4, "distribution_accuracy": 64.9 } } } ``` #### GET /v1/matches/{id}/heatmaps Retrieve team and player heatmaps. 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. Parameters: - `id` (string, required) — Match ID, for example m_B8x2K9 - `frame` (string, optional) — 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. Response: ```json { "data": { "match_id": "m_B8x2K9", "grid": { "length": 16, "width": 12, "frame": "acting_ltr", "cell_length_m": 6.5625, "cell_width_m": 5.6667 }, "teams": [ { "team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "side": "home", "actions": 812, "cells": [[3, 6, 14], [4, 6, 11], [8, 5, 9]] } ], "players": [ { "player": { "id": "p_2Mk8Rw", "name": "K. De Bruyne", "shirt_number": 17 }, "team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "side": "home", "actions": 125, "cells": [[3, 6, 2], [5, 4, 9], [9, 7, 4]] } ] } } ``` ### Leagues 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. Response: ```json { "data": { "leagues": [ { "id": "l_4Kd0Wq", "name": "Premier League", "country_code": "ENG", "image_url": "https://cdn.pitchapi.dev/leagues/47.webp", "seasons": ["2025/2026", "2024/2025"], "is_free": true } ] } } ``` #### GET /v1/leagues/{id} Retrieve a league. One league and its current season. Parameters: - `id` (string, required) — League ID, for example l_4Kd0Wq Response: ```json { "data": { "id": "l_4Kd0Wq", "name": "Premier League", "country_code": "ENG", "season": "2025/2026", "image_url": "https://cdn.pitchapi.dev/leagues/47.webp" } } ``` #### GET /v1/leagues/{id}/matches List league matches. 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. Parameters: - `id` (string, required) — League ID, for example l_4Kd0Wq - `season` (string, optional) — Season to fetch, for example 2024/2025. Defaults to the current season. - `status` (string, optional) — 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. Response: ```json { "data": { "league": { "id": "l_4Kd0Wq", "name": "Premier League", "season": "2025/2026", "image_url": "https://cdn.pitchapi.dev/leagues/47.webp" }, "matches": [ { "id": "m_B8x2K9", "date": "2025-11-09", "time_utc": "2025-11-09T16:30:00Z", "status": "finished", "home_team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "away_team": { "id": "t_2mLp7C", "name": "Manchester United" }, "score_home": 3, "score_away": 1 } ] } } ``` ### Teams & players Reference records for teams and players, resolvable from any ID returned elsewhere in the API. #### GET /v1/teams/{id} Retrieve a team. A team's name and image URL. Parameters: - `id` (string, required) — Team ID, for example t_9aB2xQ Response: ```json { "data": { "id": "t_9aB2xQ", "name": "Manchester City", "image_url": "https://cdn.pitchapi.dev/teams/8456.webp" } } ``` #### GET /v1/players/{id} Retrieve a player. A player's name, shirt number, position, country, and image URL. Parameters: - `id` (string, required) — Player ID, for example p_7YtX4q Response: ```json { "data": { "id": "p_7YtX4q", "name": "Erling Haaland", "short_name": "E. Haaland", "shirt_number": "9", "position_id": 4, "country_code": "NOR", "image_url": "https://cdn.pitchapi.dev/players/994226.webp" } } ``` ### Schedule 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. Parameters: - `date` (string, required) — Calendar date as YYYY-MM-DD - `status` (string, optional) — 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. Response: ```json { "data": { "date": "2025-11-09", "matches": [ { "id": "m_B8x2K9", "league": { "id": "l_4Kd0Wq", "name": "Premier League" }, "home_team": { "id": "t_9aB2xQ", "name": "Manchester City" }, "away_team": { "id": "t_2mLp7C", "name": "Manchester United" }, "time_utc": "2025-11-09T16:30:00Z", "status": "finished", "score_home": 3, "score_away": 1 } ] } } ```