# Sofascore League Standings & Tables Scraper (`incognito_mode/sofascore-standings-scraper`) Actor

Scrape league tables and standings from Sofascore for football, basketball, ice hockey, baseball and more. One row per team, with position, played, W/D/L, points, goal difference and promotion or relegation status. Handles group stages. No API key and no proxy needed.

- **URL**: https://apify.com/incognito\_mode/sofascore-standings-scraper.md
- **Developed by:** [Elena Vance](https://apify.com/incognito_mode) (community)
- **Categories:** News, Developer tools, 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 standings 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 League Standings & Tables Scraper

Scrape league tables from [Sofascore](https://www.sofascore.com) for football,
basketball, ice hockey, baseball and more — **one row per team**, ready to
open in Excel or feed into a spreadsheet.

No API key. No login. No residential proxy. Runs on the Apify free plan.

***

### What you get

One dataset row per team per table, with a stable column set across every
sport:

| Tournament | Season | Group | Pos | Team | P | W | D | L | Pts | GD | Promotion |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Premier League | 26/27 | | 1 | Arsenal | 1 | 1 | 0 | 0 | 3 | +4 | Champions League |
| Premier League | 26/27 | | 20 | Wolverhampton | 1 | 0 | 0 | 1 | 0 | −4 | Relegation |
| FIFA World Cup | 2026 | Group A | 1 | Mexico | 0 | 0 | 0 | 0 | 0 | 0 | |
| NBA | 26/27 | | 1 | Oklahoma City | 0 | 0 | | 0 | | 0 | |

Group-stage competitions (World Cup groups, MLB divisions, NHL conferences)
fill the `group` column. A single-table league leaves it empty.

### What makes this different

- **Built for league tables, not bolted on.** Where other Sofascore Actors
  offer standings at all, it is a checkbox inside a broad multi-mode scraper.
  Here it is the product: the input, the schema and the pricing are all shaped
  around one row per team per table.
- **Nested tables come back whole.** The NHL returns its league table, both
  conferences and all four divisions — 96 rows from 32 teams — and group
  stages fill a `group` column. Set `groupScope` to `overall` to keep only the
  top table and bill 32 rows instead.
- **One column set across every sport**, so a spreadsheet built against a
  football table works unchanged for basketball, ice hockey and baseball.
- **Off-season is reported, not faked.** A competition with no table yet
  succeeds with a status message saying so and bills nothing beyond the start
  event, instead of returning an empty run you have to diagnose.
- **A $0.00005 start fee**, against up to $0.08 on comparable Actors. For a
  run that fetches one table, that is the whole price difference.

### Input

| Field | Description |
|---|---|
| `tournaments` | **Required.** Sofascore tournament URLs or numeric IDs. Paste the URL straight from your browser. |
| `seasonId` | Optional. Defaults to the latest season. A season pinned in a URL's `#id:` fragment wins over this. |
| `standingsTypes` | `total` (default), `home`, `away`. Each adds one request per tournament. |
| `groupScope` | `all` (default) or `overall` — see [Overlapping tables](#overlapping-tables). |
| `seasonFallback` | Default on — see [Off-season behaviour](#off-season-behaviour). |
| `maxItems` | Stop after this many rows. |
| `proxyConfiguration` | Optional. Only needed to pin a country or a specific proxy. |

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

Some IDs to start from: **17** Premier League · **8** LaLiga · **23** Serie A ·
**35** Bundesliga · **34** Ligue 1 · **7** Champions League · **242** MLS ·
**132** NBA · **234** NHL · **11205** MLB · **16** FIFA World Cup.

### Sport coverage

Verified against live data across 14 competitions:

| Sport | Table | Sport-specific columns |
|---|---|---|
| Football | ✅ | `points`, `scoresFor`/`scoresAgainst`, `goalDifference`, `promotion` |
| Basketball | ✅ | `percentage`, `gamesBehind` — **no `points`** |
| Ice hockey | ✅ | `points`, `overtimeLosses`, `normaltimeLosses` |
| Baseball | ✅ | `percentage`, `gamesBehind` — no `draws` |
| Tennis | ❌ | A draw is not a table; there is nothing to return |
| Pure knockout cups | ❌ | No table until a league/group phase exists |

Columns a sport doesn't use are `null`, never missing — so the CSV keeps the
same header everywhere.

### Overlapping tables

Some leagues return several nested tables at once. The NHL sends the league
table **plus** 2 conferences **plus** 4 divisions, so 32 teams produce **96
rows** — the same team three times, at a different position in each. MLB does
the same (9 tables, 90 rows for 30 teams).

Those are real, distinct standings, not duplicates. But if you only wanted the
league table, set `groupScope` to `overall`:

| League | `all` | `overall` |
|---|---|---|
| NHL | 96 rows | **32 rows** |
| MLB | 90 rows | **30 rows** |
| Premier League | 20 rows | 20 rows (already one table) |
| FIFA World Cup | 60 rows | 60 rows (groups don't overlap) |

`overall` keeps the one table that contains every team. Where no such table
exists — a group stage, where the groups split the teams rather than nesting —
nothing is dropped and the run says so.

Either way the run logs which choice you got and what the other would have
returned.

### Off-season behaviour

Sofascore lists a season as soon as it is *scheduled*, but a table only exists
once it has *started*. Measured on 2026-08-24, the Champions League and the
NHL both had **no table for 2026/27** while 2025/26 was complete.

With `seasonFallback` on (the default), the Actor returns the previous
season's table instead of nothing and records what happened:

- `seasonId` / `seasonYear` — the season the rows actually came from
- `seasonRequested` — set **only** when a fallback happened

So you can always tell "here is 26/27" from "26/27 hasn't started, here is
25/26". Turn the toggle off to get an empty result instead.

### Notes

- A competition with no table is reported in the run's status message, not as
  a failure — it is a real answer.
- A run where *every* request failed fails loudly rather than finishing green
  with an empty dataset.
- Requests go directly to Sofascore. A proxy IP is only fetched if a request
  is actually blocked, and then a datacenter IP (included in every Apify plan)
  is tried before any residential one.

### 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** | *This Actor.* |
| [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](https://apify.com/incognito_mode/sofascore-season-stats-scraper) | Season leaderboards and complete team statistics. Takes `uniqueTournamentId`. |
| [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
python -m venv .venv && .venv/bin/pip install -r requirements.txt -r requirements-dev.txt
.venv/bin/python -m pytest          # unit tests, no network
.venv/bin/python -m src             # local run, reads storage/key_value_stores/default/INPUT.json
```

See [`docs/architecture.md`](docs/architecture.md) for how the transport and
season resolution work, and [`docs/ci-cd.md`](docs/ci-cd.md) for the pipeline.

# 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, 16 = FIFA World Cup.

## `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.

## `standingsTypes` (type: `array`):

Which tables to fetch. 'total' is the normal league table; 'home' and 'away' are the split tables. Each type adds one request per tournament.

## `groupScope` (type: `string`):

Some leagues return several overlapping tables. The NHL sends league, conference and division standings, so 32 teams produce 96 rows — and you are billed for all 96. 'Overall' keeps only the league-wide table (32 rows). It has no effect on group stages like the World Cup, where the groups do not overlap, or on single-table leagues.

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

Sofascore lists a season as soon as it is scheduled, but a table only exists once it has started. With this on, a competition whose new season hasn't kicked off yet returns the previous season's table instead of nothing, and the rows record which season was used. Turn it off to get an empty result instead.

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

Stop after this many standings rows. One row is one team in one table.

## `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"
  ],
  "standingsTypes": [
    "total"
  ],
  "groupScope": "all",
  "seasonFallback": true,
  "maxItems": 10000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset containing every standings 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-standings-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-standings-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-standings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,incognito_mode/sofascore-standings-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/5wHh4Jlux0XaOdPHE/builds/2xlbyfAJabKe0VoOo/openapi.json
