# Understat Scraper: Football xG & Player Stats (`recordsdata/understat-xg-scraper`) Actor

Scrape Understat expected goals (xG), xA, npxG, shots and key passes per player for the Premier League, La Liga, Bundesliga, Serie A, Ligue 1 and RFPL since 2014. Computed per-90 rates. Export CSV, Excel, JSON. No login or API key. La Liga 2025/26 returns 600 players.

- **URL**: https://apify.com/recordsdata/understat-xg-scraper.md
- **Developed by:** [RecordsData](https://apify.com/recordsdata) (community)
- **Categories:** Sports, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

<p align="center">
  <img src="https://api.apify.com/v2/key-value-stores/AAm3a1h3Z9nYfrvh9/records/banner?v=3" alt="RecordsData" width="100%" />
</p>

## ⚽ Understat Scraper: Football xG, Team Tables, Matches & Shots

> **The Understat xG Scraper exports expected goals data from understat.com as CSV, Excel or JSON. For any season since 2014/15 it returns player rows, team season tables with xG, xGA and xPoints, match results with match xG, shot-level data with pitch X/Y, and full player careers for the Premier League, La Liga, Bundesliga, Serie A, Ligue 1 and RFPL. No login or API key.**

This Understat scraper pulls a full league player table (La Liga 2025/26 returns 600 player rows), a team season table (Arsenal 2025/26: 85 points, xG 77.49, xGA 33.13), the matches of a season, or every shot of a match. It adds columns Understat does not show: xG/90, xA/90, shots/90 and goals minus xG. An optional only-new mode delivers just the rows that changed since your last run.

### ⚽ What does the Understat xG Scraper do?

Pick what to export with the `entity` input:

- **players**: league player tables and club squad tables, with a minimum-minutes filter and seven sort keys.
- **teams**: one row per team and season with rank, W/D/L, points, goals, xG, xGA, xGD, npxG, xPoints, PPDA and deep passes. Optional situation splits per club.
- **matches**: results and upcoming fixtures with match xG, xPoints, PPDA and Understat win probabilities. Filter by dates, clubs or match IDs.
- **playerCareer**: one row per season and club for a player, found by Understat ID, player URL or exact name.
- **Shots** (`includeShots`): one row per shot with minute, X, Y, xG, result, situation, shot type, last action and assisting player.
- **Only-new mode** (`onlyNew`): skips rows already delivered, so repeat runs cost less.
- Covers **six leagues and every season from 2014/15**. Invalid input, unknown teams and empty results cost nothing.

### 📊 What data can you extract from Understat?

The dataset schema has 96 columns in one fixed order. A row carries only the keys that have a value, and the `recordType` column tells you which kind of row it is (league-player, team-player, player-season, team-season, match, shot or team-split).

| Group | Fields |
|---|---|
| Player identity | `name`, `playerId`, `team`, `position`, `season`, `games`, `minutes` |
| Player metrics | `goals`, `nonPenaltyGoals`, `xG`, `npxG`, `xGDiff`, `assists`, `xA`, `xADiff`, `shots`, `keyPasses`, `yellowCards`, `redCards`, `xGChain`, `xGBuildup` |
| Per-90 rates | `xGPer90`, `xAPer90`, `npxGPer90`, `xGChainPer90`, `shotsPer90` |
| Team table | `rank`, `wins`, `draws`, `losses`, `points`, `goalsAgainst`, `goalDiff`, `xGA`, `xGD`, `npxGA`, `npxGD`, `xPoints`, `xPointsDiff`, `ppda`, `oppda`, `deep`, `deepAllowed` |
| Team splits | `splitGroup`, `split` (situation, formation, game state, timing, shot zone, attack speed, result) |
| Match | `matchId`, `date`, `kickoff`, `homeTeam`, `awayTeam`, goals, `homeXG`, `awayXG`, xPoints, PPDA, deep, shots on target, win, draw and loss probabilities |
| Shot | `shotId`, `minute`, `X`, `Y`, `xG`, `result`, `situation`, `shotType`, `lastAction`, `assistedBy` |
| Meta | `recordType`, `scope`, `scopeValue`, `league`, `changeType`, `url`, `scrapedAt` |

### 🧾 Understat scraper sample output

A real player row from a cloud run (La Liga, season 2025, 10 rows requested, 10 delivered):

```json
{
  "recordType": "league-player",
  "scope": "league",
  "scopeValue": "La_liga",
  "season": "2025/2026",
  "playerId": "3423",
  "name": "Kylian Mbappe-Lottin",
  "team": "Real Madrid",
  "position": "F S",
  "games": 31,
  "minutes": 2623,
  "goals": 25,
  "xG": 25.8,
  "xGDiff": -0.8,
  "assists": 5,
  "xA": 7.24,
  "shots": 146,
  "keyPasses": 65,
  "xGPer90": 0.89,
  "shotsPer90": 5.01,
  "url": "https://understat.com/player/3423",
  "scrapedAt": "2026-10-04T04:15:31.634Z"
}
```

A trimmed team row (EPL, season 2025). Only values read from the cloud run are shown, other columns are left out:

```json
{
  "recordType": "team-season",
  "league": "EPL",
  "team": "Arsenal",
  "points": 85,
  "xG": 77.49,
  "xGA": 33.13
}
```

Rows are never padded with empty columns. The player row comes from an earlier version without the newer columns.

### 💰 How much does it cost to scrape Understat?

Pricing is pay per event. Six events are live, at the Free plan rate shown below. Paid Apify plans get a lower per-row price on the player events, and the other four events keep the same price on every plan.

| Event | When it is charged | Price per row (Free plan) |
|---|---|---|
| League player row | One player-season from a league request or a player career | $0.015 |
| Team player row | One player-season from a club request | $0.015 |
| Team season row | One team-season table row | $0.004 |
| Match row | One match with result, xG and win probabilities | $0.002 |
| Team split row | One situation, formation, timing or zone split row | $0.001 |
| Shot row | One shot with position, xG, minute and result | $0.0001 |

On the Gold plan a player row costs $0.01275. Paid plans pay less per player row than the Free plan.

Examples at the Free plan rate: 10 player rows cost $0.15, a full 600-row La Liga season costs $9.00, and 1,000 rows cost 1,000 x $0.015 = $15.00. A 20-team EPL table costs 20 x $0.004 = $0.08. A match with its shots costs $0.002 plus $0.0001 per shot, so 3 matches and 57 shots cost $0.0117. You are not charged for failed, empty or "not found" lookups, and the actor stops when your **maximum cost per run** is reached. Use `maxItems` to cap the rows and the spend.

### 🚀 How to scrape Understat in 3 steps

1. Open the actor on Apify and click **Try for free**.
2. Choose what to export, then one or more leagues, or club names such as `Arsenal`, and set the season (2025 means 2025/26).
3. Click **Start**, then download the dataset as CSV, Excel, JSON or XML.

### ⚙️ Understat scraper input

| Field | Type | Default | What it does |
|---|---|---|---|
| `entity` | string | `players` | players, teams, matches or playerCareer |
| `leagues` | array | `["EPL"]` | EPL, La_liga, Bundesliga, Serie_A, Ligue_1, RFPL (max 6) |
| `teams` | array | `[]` | Club names; squad tables for players, club filter for teams and matches |
| `season` | string | `2025` | Season start year, 2014 or later |
| `matchStatus`, `dateFrom`, `dateTo` | string | `played` | Matches only: played, scheduled or all, and a date range |
| `matchIds` | array | `[]` | Matches only: Understat match IDs or URLs (up to 200) |
| `playerIds` | array | `[]` | Careers only: player IDs, URLs or exact names (up to 200) |
| `includeShots` | boolean | `false` | Matches and careers: one extra row per shot |
| `includeSplits` | boolean | `false` | Teams: situation, formation and timing splits per club |
| `minMinutes`, `sortBy` | integer, string | `0`, `xG` | Players only: minutes filter and sort (xG, goals, xA, assists, shots, time, xGChain) |
| `onlyNew`, `stateKey`, `resetSeen` | boolean, string | off | Deliver only new or changed rows; name or clear the memory |
| `maxItems` | integer | none | Maximum rows to export and be charged for |

Example input:

```json
{
  "entity": "matches",
  "leagues": ["EPL"],
  "season": "2025",
  "teams": ["Arsenal"],
  "dateFrom": "2026-05-01",
  "includeShots": true,
  "maxItems": 60
}
```

Free users get a 10-row preview; paid users export up to 1,000,000 rows. Lists take up to 200 entries, and a season below 2014 fails at once with a clear message.

### 📦 Understat scraper output

Each row is one player, team, match, shot or split, as set by `recordType`. Views are ready for players, teams, matches, shots and team splits. If a league, team or season returns no rows, the run still succeeds with a clear status message and charges nothing. If Understat cannot be read at all, the run fails with an explicit message instead of reporting an empty success. Understat's split figures do not sum to the table xG (Arsenal 2025: 83.98 against 77.49), so use each as published.

### 🆚 Understat scraper vs alternatives

Read on 2026-10-10 from the public Store pages.

| | This actor | scrapers_lat | jungle_synthesizer |
|---|---|---|---|
| Data | Players, teams, matches, shots, careers | League, player, team, match, shots | Team season tables only |
| Only-new mode | Yes | Not shown | Not shown |
| Situation splits | Team splits | Squad splits | No |
| Price per 1,000 | $15 players, $4 teams, $2 matches, $0.10 shots | From $8.32 | From $0.90 |

parseforge ($5 per 1,000) sells player tables only, with no team, match or shot data. scrapers_lat covers similar modes and adds an optional AI summary, which we do not have. For player rows we cost about 1.7 times scrapers_lat and 3 times parseforge. Our team rows cost $4 per 1,000, so a team table costs more than at jungle_synthesizer, but ours adds players, matches and shots in one actor. Check current prices before choosing.

### 💼 Understat xG use cases

- **Recruitment and scouting:** shortlist players by xG/90 and xA/90 across six leagues.
- **Betting models:** build features from goals minus xG, team xGA and match xG.
- **Fantasy football and media:** refresh xG and xA each matchday with only-new mode; source xPoints tables and shot maps.

### 🔌 Run the Understat scraper via API and integrations

Call it from any language with the Apify API, or schedule it after each matchday.

```bash
curl -X POST "https://api.apify.com/v2/acts/recordsdata~understat-xg-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"entity":"teams","leagues":["EPL"],"season":"2025","maxItems":20}'
```

Results flow to Google Sheets, Zapier, Make, n8n or Airbyte through the Apify integrations.

### 🤖 Understat Scraper MCP server for AI agents

AI agents can call this Actor through the Apify MCP server at `https://mcp.apify.com?tools=RecordsData/understat-xg-scraper`. The URL loads only this Actor as a tool.

Add this to your MCP client configuration (Claude Desktop, Cursor, VS Code):

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=RecordsData/understat-xg-scraper"
    }
  }
}
```

Example agent prompt:

> Use the Understat Scraper with entity teams for the EPL 2025 season, then list the three teams whose points are furthest above their xPoints and say who is over-performing.

Tool description: use it when an agent needs football xG, xA, team xGA and xPoints, match xG or shot coordinates for the top six leagues. It takes the same fields as the Input table.

### ⚖️ Is it legal to scrape Understat?

The actor reads only public aggregate football statistics at a polite request rate, with no login and no personal data beyond public player names. Check Understat's terms for your use case and credit Understat when you republish its numbers.

### ❓ Frequently asked questions

#### What is xG in football and how does Understat Scraper use it?

Expected goals (xG) is the probability-weighted value of the chances a player took. Understat Scraper exports xG from understat.com for every player, team, match and shot and computes `xGDiff`, which is goals minus xG. A positive value means a player scores more than their chances suggest.

#### How do I spell team names in Understat Scraper?

Use Understat's own spelling with underscores in the `teams` field, for example Manchester_City, Real_Madrid or Bayern_Munich. A spelling that Understat does not know returns no rows from Understat Scraper and no charge. If a club returns nothing, check the underscore spelling against the club name on understat.com.

#### Which seasons and leagues does Understat Scraper cover?

Understat Scraper covers the Premier League (EPL), La Liga, Bundesliga, Serie A, Ligue 1 and the Russian Premier League, from the 2014/15 season to the current one. Enter the start year of the season in the `season` field, for example 2025 for 2025/26, and pick one or more leagues in `leagues`.

#### Why did my Understat Scraper run return 0 results?

Usually the team spelling is wrong, the season is outside 2014 to the current year, or `minMinutes` is higher than any player reached. Understat Scraper then succeeds with a status message and costs nothing. An invalid season such as 1999 fails immediately with a clear message instead of an empty success.

#### How many player rows does an Understat league return?

When checked in October 2026, Understat Scraper returned these player rows for the 2025/26 season: 537 for the Premier League, 600 for La Liga, 499 for Bundesliga, 586 for Serie A, 553 for Ligue 1 and 470 for the Russian Premier League. A full season is one league request and costs one row price per player.

#### Am I charged for errors in Understat Scraper?

No. Understat Scraper charges only for valid priced rows, never for failed lookups. Error rows, empty results, unknown team lookups and duplicate rows are never billed, and the run stops cleanly when your maximum cost per run is reached. Use `maxItems` to cap spend.

#### Can I try Understat Scraper for free?

Yes. Free Apify users get a 10-row preview of Understat Scraper, which costs $0.15 at the Free-plan rate of $0.015 per player row. That is enough to check the columns before a full league export. Paid Apify plans unlock exports of up to 1,000,000 rows.

#### Can I scrape single matches or shots with Understat Scraper?

Yes. Set `entity` to `matches` and add `includeShots` to get one row per shot, with minute, X, Y, xG, result, situation, shot type and assisting player. Understat Scraper accepts match IDs or URLs, or a date range and clubs. For one player's shots over a whole career, use `playerCareer` with `includeShots`. Shots are off by default and cost $0.0001 per shot row.

#### Can I get team tables and xPoints with Understat Scraper?

Yes. Set `entity` to `teams` to get one row per team and season with rank, points, xG, xGA, xGD, npxG, xPoints and PPDA, computed from Understat's per-match history in the site's own order. Add `includeSplits` for situation, formation and timing splits. Team rows cost $0.004 each and split rows $0.001 each.

#### Can Understat Scraper return only new results?

Yes. Turn on `onlyNew` and Understat Scraper delivers only rows that are new or changed since the last run with the same filters, such as new match results, new shots and updated totals. Skipped rows are not charged. A second run on the same match returned 0 rows and charged 0. Use `stateKey` to name the memory and `resetSeen` to clear it.

#### Can I look up one player by ID or name in Understat Scraper?

Yes. Set `entity` to `playerCareer` and enter Understat player IDs, player URLs such as understat.com/player/8260, or a full name in `playerIds`. Understat Scraper returns one row per season and club, billed as player rows. An ambiguous name is rejected with the candidate IDs listed, so use the ID when two players share a name.

### 🔗 Want more football and sports data?

- [Sofascore Match Stats Scraper](https://apify.com/RecordsData/sofascore-match-stats-scraper)
- [Sports Reference Stats Scraper](https://apify.com/RecordsData/sports-reference-stats-scraper)
- [TheSportsDB Sports Scraper](https://apify.com/RecordsData/thesportsdb-sports-scraper)

### 🛟 Support

Found a bug or a missing field? Open the **Issues** tab on this actor's page or write to contact.punkrecordsdata@gmail.com. Custom solutions available on request.

Last updated: 2026-10-11

# Actor input Schema

## `entity` (type: `string`):

players: one row per player for a league season or a club squad (xG, xA, shots, minutes). teams: league table per season with xG, xGA, xPoints and PPDA (optional situation splits). matches: results and fixtures with match xG. playerCareer: one row per season of a player's career, found by player ID or name.

## `leagues` (type: `array`):

Leagues to export: Premier League (EPL), La Liga, Bundesliga, Serie A, Ligue 1 or the Russian Premier League. Used by the players, teams and matches exports.

## `teams` (type: `array`):

Club names, for example Arsenal, Manchester City or Real Madrid (spaces or underscores both work). Players export: adds a squad table per club. Teams and matches exports: keeps only these clubs inside the selected leagues. Up to 200 names.

## `season` (type: `string`):

Season start year as 4 digits: 2025 means the 2025/26 season. Data available from 2014 (2014/15) to the current season. Not used by playerCareer or by matches selected by match ID.

## `matchStatus` (type: `string`):

Matches export only. played: finished matches with goals and xG. scheduled: upcoming fixtures with Understat win probabilities. all: both.

## `dateFrom` (type: `string`):

Matches export only. Keep matches on or after this day, format YYYY-MM-DD (for example 2025-12-01). Leave empty for no start limit.

## `dateTo` (type: `string`):

Matches export only. Keep matches on or before this day, format YYYY-MM-DD. Leave empty for no end limit.

## `matchIds` (type: `array`):

Matches export only. Understat match IDs or match URLs (the number in understat.com/match/28778). When filled, only these matches are exported and the league, team, season and date filters are ignored. Up to 200.

## `playerIds` (type: `array`):

Player careers export only. Understat player IDs or player URLs (the number in understat.com/player/8260), or a full player name that matches exactly one player. Up to 200.

## `includeShots` (type: `boolean`):

Matches and player careers only. Adds one extra row per shot with pitch X/Y position, xG, minute, situation, body part and result. Billed per shot row. Off by default.

## `includeSplits` (type: `boolean`):

Teams export only. Adds rows per club with xG and shots for and against by situation, formation, game state, timing, shot zone, attack speed and result. Billed per split row. Off by default.

## `minMinutes` (type: `integer`):

Players export only. Only export players with at least this many minutes played in the season. 0 means every player.

## `sortBy` (type: `string`):

Players export only. Metric that orders the player rows, highest first: xG, goals, xA, assists, shots, minutes (time) or xGChain.

## `onlyNew` (type: `boolean`):

Deliver only rows that are new or changed since the last run with the same filters (new match results, new shots, updated player and team totals). Rows already delivered are skipped and not charged.

## `stateKey` (type: `string`):

Name for the memory of previous results. Leave empty to derive it from your filters. Use the same name to share memory between runs, or a different name to keep separate monitors.

## `resetSeen` (type: `boolean`):

Clear the memory for this monitor before the run, so every row counts as new again.

## `maxItems` (type: `integer`):

Maximum number of rows to export, which is also the cap on what you are charged. Free users: limited to 10 rows (preview). Paid users: optional, up to 1,000,000.

## Actor input object example

```json
{
  "entity": "players",
  "leagues": [
    "EPL"
  ],
  "teams": [],
  "season": "2025",
  "matchStatus": "played",
  "dateFrom": "",
  "dateTo": "",
  "matchIds": [],
  "playerIds": [],
  "includeShots": false,
  "includeSplits": false,
  "minMinutes": 0,
  "sortBy": "xG",
  "onlyNew": false,
  "stateKey": "",
  "resetSeen": false,
  "maxItems": 10
}
```

# Actor output Schema

## `overview` (type: `string`):

Every row with all fields

## `players` (type: `string`):

Player season rows (xG, xA, shots, minutes)

## `teams` (type: `string`):

League table rows with xG, xGA and xPoints

## `matches` (type: `string`):

Match rows with goals, xG and win probabilities

## `shots` (type: `string`):

Shot rows with pitch position and xG

## `fullData` (type: `string`):

Complete dataset with all fields

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "teams": [],
    "dateFrom": "",
    "dateTo": "",
    "matchIds": [],
    "playerIds": [],
    "stateKey": "",
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("recordsdata/understat-xg-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "teams": [],
    "dateFrom": "",
    "dateTo": "",
    "matchIds": [],
    "playerIds": [],
    "stateKey": "",
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("recordsdata/understat-xg-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "teams": [],
  "dateFrom": "",
  "dateTo": "",
  "matchIds": [],
  "playerIds": [],
  "stateKey": "",
  "maxItems": 10
}' |
apify call recordsdata/understat-xg-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,recordsdata/understat-xg-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/bWLsNW5yOczBgJG9k/builds/ZLojdPHYRep6DCeXt/openapi.json
