# Sofascore Season Stats Scraper (`incognito_mode/sofascore-season-stats-scraper`) Actor

Scrape season leaderboards and team statistics from Sofascore for football, basketball and ice hockey. Top scorers, assists, ratings and 30 more statistics per league, plus a complete team table and knockout brackets. No API key, no login.

- **URL**: https://apify.com/incognito\_mode/sofascore-season-stats-scraper.md
- **Developed by:** [Elena Vance](https://apify.com/incognito_mode) (community)
- **Categories:** Developer tools, News, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 player leaderboard rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Sofascore Season Stats Scraper

Season leaderboards and team statistics from [Sofascore](https://www.sofascore.com),
for any competition it covers. Top scorers, assist leaders and match ratings;
a complete team table with every statistic the league publishes; and the
knockout bracket where there is one.

No API key, no login, no browser. Calls Sofascore's own JSON API directly and
only reaches for a proxy if a request is actually blocked.

### What you get

Three row types in one dataset, told apart by the `recordType` column.

**`playerLeaderboard`** — one row per player per statistic, ranked. Football
publishes 34 leaderboards (goals, assists, rating, expected goals, key passes,
tackles, kilometres covered, top speed, saves…), basketball 24 (points,
rebounds, triple-doubles, true shooting, player efficiency…) and ice hockey 21
(points, save percentage, time on ice, power-play goals…). Each row carries
the rank, the value, the appearances behind it, the player and the club they
played for.

**`teamStats`** — one row per team with every team statistic merged into one
object: goals scored and conceded, expected goals, possession, passing
accuracy, corners, cards for football; offensive and defensive rating, pace,
true shooting for basketball. This is not a top-N cut — every team in the
competition is there, so a Premier League run is twenty complete rows.

**`cupBracket`** — one row per tie: round, both teams, the aggregate result and
who went through. Cups, tournament knockout stages and play-off leagues have
one; the NBA returns its fifteen-tie play-off bracket. Round-robin leagues
decided on points return nothing.

### What makes this different

- **Around thirty ranked tables merged into one row per team.** Sofascore
  publishes team statistics as dozens of separate leaderboards; this Actor
  fetches them all and joins them, so a season arrives as one table you can
  open rather than thirty you have to reconcile yourself.
- **Not a top-N cut.** Every team in the competition gets a row — 20 for a
  Premier League season, 30 for the NBA, 32 for the NHL.
- **34 football leaderboards, 24 basketball, 21 ice hockey**, each row
  carrying the rank, the value, the appearances behind it and the club the
  player played for.
- **Knockout brackets where a competition has one** — round, both teams, the
  aggregate result and who advanced. Round-robin leagues return nothing and
  are never charged for it.
- **Failures are never billed**, and unusable input returns an unbilled
  diagnostic row rather than failing the run.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `tournaments` | array | — | Sofascore tournament URLs or unique-tournament IDs. `17` = Premier League, `132` = NBA, `234` = NHL, `7` = Champions League. |
| `seasonId` | integer | latest | Which season. A `#id:` fragment in a pasted URL wins over this. |
| `seasonFallback` | boolean | `true` | Use the previous season when the newest one has been scheduled but not played. |
| `includePlayerLeaderboards` | boolean | `true` | The ranked player tables. |
| `includeTeamStats` | boolean | `true` | The complete team table. |
| `includeCupBracket` | boolean | `false` | The knockout bracket. |
| `playerCategories` | array | `rating, goals, assists` | Which leaderboards to keep, or `["all"]`. **This is the setting that decides the size of your bill.** |
| `topN` | integer | `50` | Keep the top N of each leaderboard. 50 is Sofascore's maximum. |
| `statisticsTypes` | array | auto | Leave empty. Set it only to reach a table the automatic choice skips — see below. |
| `maxItems` | integer | `10000` | Stop after this many rows. |
| `proxyConfiguration` | object | off | Only needed if your IP is blocked. |

Minimal input:

```json
{ "tournaments": ["https://www.sofascore.com/tournament/football/england/premier-league/17"] }
```

### What a run costs

Requests are fixed and small — the whole 34-category table arrives in one call,
so a competition-season is two to five requests whatever you ask for. What
varies is the **row count**, and `playerCategories` is the dial:

| Input | Player rows | Team rows |
| --- | ---: | ---: |
| Default three categories | 150 | 20 |
| `["goals", "assists"]` | 100 | 20 |
| `["all"]` | 1,560 | 20 |

### Sport coverage

Verified against the live API for **football, basketball and ice hockey**,
across the Premier League, the Champions League, the NBA, the NHL and the World
Cup. Tennis and other individual sports have no team or squad concept and
publish no leaderboards here. **Baseball has none at all** — Sofascore returns
nothing for MLB, which the Actor reports rather than failing.

### The two things that are not obvious

**Statistics have a type, and it belongs to the competition.** Football spells
it `overall`; the NBA and NHL answer 404 for that and want `regularSeason`, and
the NBA publishes `playoffs` as a separate table. Sofascore has no endpoint
that says which — so the Actor probes for it and remembers the answer per
competition, which is why a multi-league run doesn't pay for it repeatedly.
Set `statisticsTypes` yourself only to reach a table the probe skips: pinning
`["regularSeason", "playoffs"]` returns both NBA tables in one run.

**A "latest" season is often empty.** Sofascore lists a season as soon as it is
scheduled, but statistics appear only once matches have been played. Measured
in late August 2026, the NBA, NHL and Champions League all returned nothing for
their newest season while the previous one was complete. With `seasonFallback`
on (the default) the Actor walks back up to two seasons and records what it
used in `seasonYear`; `seasonRequested` is filled in only when a fallback
happened, so you can always tell.

### Notes

- **Player leaderboards are a top-50 cut, team tables are not.** That is why
  the two have different row shapes: 381 Premier League players spread over 34
  leaderboards fill 12% of the cells a pivoted table would need, so pivoting
  them would imply a striker made no tackles when he is simply outside the top
  50\. Team tables cover every team in every category, so they pivot losslessly.
- A category that doesn't exist for a sport (`goals` in basketball) is reported
  in the log along with the full list of the ones that do, rather than failing
  the run.
- Bad input is recorded as an unbilled `INVALID_INPUT` row rather than crashing
  the run.

### The Sofascore Actor suite

Six Actors from the same publisher, built on one shared HTTP layer and one ID
space — so the output of one is the input of the next.

| Actor | What it returns |
| --- | --- |
| [Live Scores & Fixtures](https://apify.com/incognito_mode/sofascore-live-scores-scraper) | Fixtures and live results by date across 19 sports. Emits `eventId`. |
| [Match Stats, xG & Lineups](https://apify.com/incognito_mode/sofascore-match-analytics-scraper) | Per-match shotmaps with pitch coordinates, lineups, ratings, momentum. Takes `eventId`. |
| [League Standings & Tables](https://apify.com/incognito_mode/sofascore-standings-scraper) | League tables, group stages, conferences and divisions. Takes `uniqueTournamentId`. |
| [Team & Player Data](https://apify.com/incognito_mode/sofascore-team-player-scraper) | Squads, bios, market values, transfers, match logs. Takes team and player IDs. |
| **Season Stats** | *This Actor.* |
| [Rankings & Search](https://apify.com/incognito_mode/sofascore-rankings-scraper) | FIFA, ATP/WTA, UEFA and UFC tables, plus name-to-ID search. |

**How they chain.** Live Scores emits the `eventId` that Match Stats takes.
Rankings' search turns a name into the team, player and tournament IDs the
other five accept. Standings and Season Stats share `uniqueTournamentId`, and
Team & Player emits that same ID as `primaryTournamentId`.

All six need no API key, no login and no headless browser; none requires a
residential-proxy add-on, and all run on the Apify free plan.

### Development

```bash
pip install -r requirements-dev.txt
pytest                       # 131 tests, no network
npx apify-cli@1 validate-schema
apify run                    # needs storage/key_value_stores/default/INPUT.json
```

`docs/architecture.md` covers how the client, season resolution and billing fit
together; `docs/ci-cd.md` the GitHub Actions pipeline; `docs/publishing.md` the
Apify Store listing and pricing.

# Actor input Schema

## `tournaments` (type: `array`):

Competitions to scrape. Paste Sofascore tournament URLs (copy them from your browser) or unique tournament IDs. Examples: 17 = Premier League, 8 = LaLiga, 132 = NBA, 234 = NHL, 7 = Champions League. Baseball is not supported — Sofascore publishes no leaderboards for it.

## `seasonId` (type: `integer`):

Optional. Which season to read. Leave empty for the latest. A season pinned in a tournament URL (the #id: part) wins over this.

## `seasonFallback` (type: `boolean`):

Sofascore lists a season as soon as it is scheduled, but statistics only exist once matches have been played. In August 2026 the NBA, the NHL and the Champions League all returned nothing for their newest season. With this on, those competitions return the previous season instead, and every row records which season was actually used in 'seasonYear' and 'seasonRequested'. Turn it off to get an empty result instead.

## `includePlayerLeaderboards` (type: `boolean`):

Top players per statistic — scorers, assists, ratings, tackles, and 30 more depending on the sport. One row per player per leaderboard.

## `includeTeamStats` (type: `boolean`):

One row per team with every team statistic the competition publishes — goals, expected goals, possession, passing, cards. Unlike the player tables this covers the whole competition, not a top-50 cut, so a Premier League run is 20 complete rows.

## `includeCupBracket` (type: `boolean`):

The knockout bracket, one row per tie: round, both teams, aggregate result and who went through. Cups, tournament knockout stages and play-off leagues have one — the NBA returns its 15-tie play-off bracket. Round-robin leagues decided on points, like the Premier League, return nothing. Off by default.

## `playerCategories` (type: `array`):

Category names to keep, or \['all'] for every leaderboard the sport publishes. This is what controls the size of your bill: an unfiltered Premier League season is 1,560 player rows across 34 categories, while the default three is 150. Football names include goals, assists, rating, expectedGoals, tackles, keyPasses, saves; basketball has points, rebounds, steals, blocks, tripleDoubles; ice hockey has points, savePercentage, timeOnIce. A name that doesn't exist for the sport is reported in the log with the full list of the ones that do.

## `topN` (type: `integer`):

Keep only the top N of each player leaderboard. Sofascore returns 50 and offers no way to ask for more, so 50 is both the default and the maximum.

## `statisticsTypes` (type: `array`):

Leave empty and the Actor works this out for itself — football spells it 'overall' while the NBA and NHL want 'regularSeason', and there is no endpoint that says which. Set it only to ask for a table the automatic choice skips: the NBA publishes 'regularSeason' and 'playoffs' as two separate tables, and picking both returns both.

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

Stop after this many rows across all record types.

## `proxyConfiguration` (type: `object`):

Optional. This Actor calls Sofascore directly and only reaches for a proxy if a request is actually blocked, trying datacenter IPs (included in every Apify plan) before residential ones. Set this only to pin a specific proxy or country from the start.

## Actor input object example

```json
{
  "tournaments": [
    "https://www.sofascore.com/tournament/football/england/premier-league/17"
  ],
  "seasonFallback": true,
  "includePlayerLeaderboards": true,
  "includeTeamStats": true,
  "includeCupBracket": false,
  "playerCategories": [
    "rating",
    "goals",
    "assists"
  ],
  "topN": 50,
  "maxItems": 10000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset containing every leaderboard, team and bracket row.

# 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 = {
    "tournaments": [
        "https://www.sofascore.com/tournament/football/england/premier-league/17"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/sofascore-season-stats-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 = { "tournaments": ["https://www.sofascore.com/tournament/football/england/premier-league/17"] }

# Run the Actor and wait for it to finish
run = client.actor("incognito_mode/sofascore-season-stats-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 '{
  "tournaments": [
    "https://www.sofascore.com/tournament/football/england/premier-league/17"
  ]
}' |
apify call incognito_mode/sofascore-season-stats-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,incognito_mode/sofascore-season-stats-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/CWEJOxkSb0BCkTQUD/builds/Lq7crVy98yCvJW6KB/openapi.json
