# Transfermarkt League Table & Club Values Scraper (`incognito_mode/transfermarkt-competition-scraper`) Actor

Scrape any Transfermarkt competition by code: full league table plus every club's squad size, average age, foreigners and total market value — merged into one row per club. Works for leagues and cups, any season. No API key, no login, no proxy needed. JSON, CSV or Excel.

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

## Pricing

from $3.00 / 1,000 competition club scrapeds

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

## Transfermarkt League Table & Club Values Scraper

Scrape any competition on [Transfermarkt](https://www.transfermarkt.com) by its
code — `GB1`, `ES1`, `L1`, `IT1`, `CL` — and get **one row per club** that
merges the league table with what the squad is worth.

Rank, points, wins, draws, losses and goals on the same row as squad size,
average age, foreigners and total market value. No join, no second run.

Works for **leagues and cups**, any season. No API key, no login, no residential
proxy, no browser. It runs on the Apify free plan. Export to **JSON, CSV,
Excel, XML** or pull it from the API.

***

### What you get

One row per club. Real, unedited output from `{"competitionCodes": ["GB1"], "season": 2025}`:

| Field | Value |
| --- | --- |
| `competitionCode` / `competitionName` | `GB1` / `Premier League` |
| `competitionType` | `league` |
| `competitionCountry` / `competitionTier` | `England` / `First Tier` |
| `reigningChampion` | `Arsenal FC` |
| `numberOfTeams` / `numberOfPlayers` | `20` / `571` |
| `averageMarketValuePerPlayer` | `23100000` |
| `averageAge` | `26.0` |
| `season` / `seasonStartYear` | `25/26` / `2025` |
| `clubId` / `clubName` | `11` / `Arsenal FC` |
| `squadSize` | `40` |
| `clubAverageAge` | `23.9` |
| `foreignersCount` | `20` |
| `averageMarketValue` | `33150000` |
| `totalMarketValue` | `1330000000` |
| `rank` | `1` |
| `matchesPlayed` | `38` |
| `wins` / `draws` / `losses` | `26` / `7` / `5` |
| `goalsFor` / `goalsAgainst` / `goalDifference` | `71` / `27` / `44` |
| `points` | `85` |
| `clubUrl` | `https://www.transfermarkt.com/x/startseite/verein/11` |

**The table and the club values arrive merged.** On Transfermarkt these are two
different pages; here they are one row, so "which of the top six is overachieving
against its squad value" is a spreadsheet sort rather than a data project.

#### Money is parsed, not passed through

`€1.33bn` becomes `1330000000` and `€33.15m` becomes `33150000`. The data sorts,
sums and filters without a cleaning pass.

***

### Leagues and cups both work — and they carry different data

A cup has no league table. Transfermarkt does not publish one, so this Actor
does not invent one:

| | League (`GB1`, `ES1`, `L1`…) | Cup (`CL`, `EL`, `DFB`…) |
| --- | :-: | :-: |
| `rank`, `points`, `wins`, goals | ✅ | `null` |
| `squadSize`, `totalMarketValue`, `clubAverageAge` | ✅ | ✅ |
| `foreignersCount` | ✅ | `null` |
| `numberOfTeams`, `competitionTier`, `reigningChampion` | ✅ | `null` |
| Typical rows | 18–24 | 36 |

The **columns stay the same either way** — a cup row carries the standings
fields as nulls rather than dropping them, so a mixed export is still a clean
table in Excel. The run's status message says how many of your competitions were
cups, so an all-null standings column is never a mystery.

You do not have to tell the Actor which kind you asked for. It finds out from
Transfermarkt's own redirect and takes the right path.

***

### Input

```json
{
  "competitionCodes": ["GB1", "ES1", "CL"],
  "season": 2025,
  "includeStandings": true,
  "maxItems": 500
}
```

| Input | What it does |
| --- | --- |
| `competitionCodes` | Transfermarkt competition codes, **or any Transfermarkt competition URL**. The name in the URL is ignored. |
| `season` | The year a season **starts** — `2025` is 2025/26. Leave empty for the current season. |
| `includeStandings` | Merge the league table in. One extra request per league, **no extra charged rows**. Off means the standings fields come back null. |
| `maxItems` | Hard cap on charged rows. Default `500`. |
| `proxyConfiguration` | Leave it off — see below. |

#### Codes worth knowing

| Code | Competition | Code | Competition |
| --- | --- | --- | --- |
| `GB1` | Premier League | `CL` | UEFA Champions League |
| `ES1` | LaLiga | `EL` | UEFA Europa League |
| `L1` | Bundesliga | `UCOL` | UEFA Conference League |
| `IT1` | Serie A | `GB2` | Championship |
| `FR1` | Ligue 1 | `NL1` | Eredivisie |
| `PO1` | Liga Portugal | `TR1` | Süper Lig |
| `BRA1` | Brasileirão | `MLS1` | Major League Soccer |
| `AR1N` | Liga Profesional | `SA1` | Saudi Pro League |

Any code from a Transfermarkt URL works: the segment after `/wettbewerb/` or
`/pokalwettbewerb/`.

#### Cost lever

**One competition is 18–36 charged rows, not one.** A domestic league is 18–24;
the Champions League is 36. Five leagues is roughly 100 rows.

`includeStandings` costs an extra **request**, never an extra **row** — the
table is merged onto rows you are being charged for anyway.

***

### Why this is cheap to run

Transfermarkt is fronted by AWS WAF, and the usual answer — a residential proxy
at $8/GB — is what makes scrapers of this site expensive. It turns out **Apify's
own egress is not challenged**, so this Actor calls Transfermarkt directly and
pays nothing for proxy. A residential IP is fetched only if a request is
genuinely challenged, and most runs never touch one.

One competition is **one or two page loads for 20–36 rows**, which makes this
the cheapest way to get Transfermarkt data per row that exists in this suite.

That is also why the Actor start fee is **$0.00005** and the default memory is
**256 MB**. Apify charges the start fee once per gigabyte, so a 4 GB Actor with
a $0.10 start fee bills **$0.40 before returning a single row**. Several
Transfermarkt scrapers on the Store do exactly that.

**Do not turn on Apify's datacenter proxy.** Transfermarkt's WAF challenges
Apify's datacenter pool while leaving its direct egress alone — verified, same
URLs, same second — so switching it on takes a working run and breaks it.

***

### What makes this different

- **The table and the squad values on one row.** Everywhere else these are two
  scrapes and a join on club name, which breaks on `Arsenal FC` vs `Arsenal`.
  Here they are merged **by club ID**, so mid-season disagreements between the
  two tables cannot mis-pair a single row.
- **Cups are supported properly**, from their own entrant page, with the
  standings fields explicitly null rather than quietly absent.
- **`clubId` is the input to the squad scraper.** One run gives you every club
  in a league; feed those IDs to the *Transfermarkt Club Squad Scraper* and you
  have every player in the league.
- **The season returned is the season reported.** `season` is read back off the
  page, never echoed from your input, and `requestedSeason` sits beside it —
  Transfermarkt silently serves its newest season for a future year, and this is
  how you can see that it did.
- **A code Transfermarkt doesn't have returns an unbilled `COMPETITION_NOT_FOUND`
  row**, not silence and not a failed run. Transfermarkt answers an unknown code
  with HTTP 200 and its full competition index; this Actor reads the redirect
  rather than the status code.
- **You are never billed for a failure**, and bad input returns an
  `INVALID_INPUT` row instead of failing the run.
- **A weekly canary** runs the deployed build against live Transfermarkt — two
  leagues *and* a cup — and opens a GitHub issue if field coverage drops.

***

### What this Actor does NOT return

- **Players.** Squad size and total value, yes; the players themselves are one
  request per club — that is the *Transfermarkt Club Squad Scraper*, which takes
  the `clubId` this Actor returns.
- **Fixtures, results or match reports.** This is the table, not the schedule.
- **Cup brackets or round-by-round progress.** A cup returns its entrants.
- **A league table for a cup**, because Transfermarkt does not have one. Group
  stages are not published as one table.
- **Anything requiring a Transfermarkt login.**

***

### Example runs

**One league, current season:**

```json
{ "competitionCodes": ["GB1"] }
```

**Europe's big five, a past season:**

```json
{
  "competitionCodes": ["GB1", "ES1", "L1", "IT1", "FR1"],
  "season": 2024
}
```

**Every Champions League entrant and what their squad is worth:**

```json
{ "competitionCodes": ["CL"] }
```

**Club values only, at half the requests:**

```json
{ "competitionCodes": ["GB1", "ES1"], "includeStandings": false }
```

***

### Related Actors

Part of a Transfermarkt suite:

- **Transfermarkt Player & Market Value Scraper** — one player in depth, with
  the full market-value and transfer histories.
- **Transfermarkt Club Squad Scraper** — every player in a club's squad, with
  the fee each was signed for.
- **Transfermarkt League Table & Club Values Scraper** — this one.
- **Transfermarkt Transfers Scraper** — transfer feeds by club, competition or
  the global latest-transfers stream.

They chain: this Actor's `clubId` is the squad scraper's input, and the squad
scraper's `playerId` is the player scraper's input.

# Actor input Schema

## `competitionCodes` (type: `array`):

Transfermarkt competition codes — GB1 (Premier League), ES1 (LaLiga), L1 (Bundesliga), IT1 (Serie A), FR1 (Ligue 1), CL (Champions League), EL (Europa League) — or any Transfermarkt competition URL. One charged row per club, so a 20-team league is 20 rows.

## `season` (type: `integer`):

The year a season starts — 2025 means the 2025/26 season. Leave empty for the current season. Note that Transfermarkt silently serves its newest season for a future year, so check seasonStartYear in the output rather than assuming.

## `includeStandings` (type: `boolean`):

Merge rank, points, wins, draws, losses and goals into each club's row. One extra request per league; does not add charged rows. Cups have no league table at all, so this has no effect on them.

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

Hard cap on charged rows for this run, across all competitions. A domestic league is 18–24 rows; the Champions League is 36.

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

Leave this off. Transfermarkt is reachable directly from Apify at no proxy cost; a residential IP is used automatically only if a request is actually challenged. Apify's datacenter proxy is blocked by Transfermarkt and will make a working run fail.

## Actor input object example

```json
{
  "competitionCodes": [
    "GB1",
    "CL"
  ],
  "season": 2025,
  "includeStandings": true,
  "maxItems": 500,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset containing every scraped club.

# 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 = {
    "competitionCodes": [
        "GB1",
        "ES1"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/transfermarkt-competition-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 = { "competitionCodes": [
        "GB1",
        "ES1",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("incognito_mode/transfermarkt-competition-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 '{
  "competitionCodes": [
    "GB1",
    "ES1"
  ]
}' |
apify call incognito_mode/transfermarkt-competition-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,incognito_mode/transfermarkt-competition-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/7mYmXyeLgtceqt2VJ/builds/pqd0OgqOz6dOr5ofH/openapi.json
