# Transfermarkt Club Squad & Market Value Scraper (`incognito_mode/transfermarkt-club-squad-scraper`) Actor

Scrape full football squads from Transfermarkt by club id, URL or name. One row per player: market value, shirt number, position, age, height, foot, nationality, joined date, signed-from club and the fee paid. Any season. No API key, no login, no proxy needed. JSON, CSV or Excel.

- **URL**: https://apify.com/incognito\_mode/transfermarkt-club-squad-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 $1.20 / 1,000 squad player 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 Club Squad & Market Value Scraper

Scrape a whole football squad from [Transfermarkt](https://www.transfermarkt.com)
in one call — by club ID, by URL, or by club name. **One row per player**, with
market value, position, age, height, foot, nationality, joined date, the club
they were signed from and **the fee that was paid for them**, plus the club's own
totals repeated on every row.

Any season, back to whenever Transfermarkt's archive starts for that club.

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 squad player. Real, unedited output from `{"clubIds": ["281"]}`:

| Field | Value |
| --- | --- |
| `clubId` / `clubName` | `281` / `Manchester City` |
| `competitionCode` / `competitionName` | `GB1` / `Premier League` |
| `clubTotalMarketValue` | `1310000000` |
| `clubSquadSize` | `26` |
| `clubAverageAge` | `24.9` |
| `clubLeagueLevel` | `First Tier` |
| `clubTablePosition` | `1` |
| `season` / `seasonStartYear` | `26/27` / `2026` |
| `playerId` | `315858` |
| `name` | `Gianluigi Donnarumma` |
| `shirtNumber` | `1` |
| `positionGroup` / `position` | `Goalkeeper` / `Goalkeeper` |
| `dateOfBirth` / `age` | `1999-02-25` / `27` |
| `nationality` | `["Italy"]` |
| `heightCm` | `196` |
| `foot` | `right` |
| `joinedClub` | `2025-09-01` |
| `contractExpires` | `2030-06-30` |
| `signedFrom` / `signedFromClubId` | `Paris Saint-Germain` / `583` |
| `signedForFee` | `30000000` |
| `marketValue` | `45000000` |
| `playerUrl` | `https://www.transfermarkt.com/x/profil/spieler/315858` |

Every row repeats the club context, so a multi-club export is readable in a
spreadsheet without a single join.

#### The signing fee, which is not on the page

`signedForFee` is pulled out of the crest tooltip on the "Signed from" column —
Transfermarkt renders it as `Paris Saint-Germain: Ablöse €30.00m`, in German
even on the English site. It is `null` for a free transfer, a loan, an
undisclosed fee, or a youth-team graduate, because none of those is zero.

#### Money and dates are parsed, not passed through

`€45.00m` becomes `45000000`; `25/02/1999` becomes `1999-02-25`; `1,96m` becomes
`196`. The data sorts, sums and filters without a cleaning pass.

***

### ⚠️ One thing worth knowing about seasons

**Transfermarkt shows different columns for the current season and a past one.**
Both tables have ten columns, so this is easy to miss:

| | Current season | Past season |
| --- | :-: | :-: |
| `contractExpires` | ✅ | — |
| `currentClub` / `isOnLoanElsewhere` | — | ✅ |
| Rows returned | the registered squad (26) | everyone who appeared (44) |

So: **ask for the current season if you want contract dates**, and **ask for a
past season if you want to know where a club's loanees actually went**. This
Actor locates columns by their header text rather than by position, which is
what keeps a contract date from being read as a club crest.

***

### Input

```json
{
  "clubIds": ["281", "https://www.transfermarkt.com/real-madrid/startseite/verein/418"],
  "searchQueries": ["Bayern Munich"],
  "maxClubsPerQuery": 1,
  "season": 2025,
  "includePlayersOnLoanElsewhere": true,
  "maxItems": 500
}
```

| Input | What it does |
| --- | --- |
| `clubIds` | Numeric IDs **or any Transfermarkt club URL**. The squad, profile and transfers URLs all work — the name in the URL is ignored. |
| `searchQueries` | Club names, when you don't have IDs. |
| `maxClubsPerQuery` | Clubs to take per name. Default `3`. |
| `season` | The year a season **starts** — `2025` is 2025/26. Leave empty for the current season. |
| `includePlayersOnLoanElsewhere` | Past seasons list players who are now elsewhere. Keep them (flagged with `isOnLoanElsewhere`) or drop them. Default on. |
| `maxItems` | Hard cap on charged rows. Default `500`. |
| `proxyConfiguration` | Leave it off — see below. |

#### Cost lever

**One club is 25–45 charged rows, not one.** That is the single thing to know
before setting `maxItems`. Two multipliers follow from it:

- `searchQueries` × `maxClubsPerQuery` — three names at the default `3` is up to
  **nine full squads**, around 250 rows.
- A past season returns roughly **70% more rows** than the current one, because
  it lists everyone who appeared rather than the registered squad. Turning
  `includePlayersOnLoanElsewhere` off brings that back down.

***

### Why this is cheap to run

Transfermarkt is fronted by AWS WAF, and the usual answer — a residential proxy
at $8/GB on 205 KB pages — 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.

Measured on the platform: **four full squads (109 players) in 8.7 s, 66 MB peak,
$0.0007 of platform usage** — about **$0.007 per 1,000 players**.

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 fee each player was signed for**, extracted from a German-language
  tooltip that is not part of the visible table.
- **Club totals on every row** — squad value, average age, league position — so
  one row answers "how does this player compare to his own squad" without a join.
- **A missing club and an unpublished season are different answers.** A club id
  Transfermarkt doesn't have returns an unbilled `CLUB_NOT_FOUND`; a season it
  hasn't published yet returns an unbilled `SEASON_NOT_AVAILABLE` naming the
  club and season. Neither is silently dropped, and neither is charged.
- **The season returned is the season reported.** `season` is read back off the
  page, never echoed from your input, and `requestedSeason` sits beside it so a
  fallback is visible rather than inferred.
- **Columns are found by name, not position.** Which is what makes the
  current/past-season column swap a non-event instead of a silent data corruption.
- **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 and
  opens a GitHub issue if field coverage drops.

***

### What this Actor does NOT return

- **Per-player market value history or transfer history.** Those are one
  request per player rather than one per club — they belong to the
  *Transfermarkt Player & Market Value Scraper*, which takes the `playerId`
  this Actor returns.
- **Coaching and technical staff.** Transfermarkt has them at
  `/mitarbeiter/verein/{id}`; they are not in this Actor's scope.
- **Contract expiry for a past season**, or **loan destinations for the current
  one** — Transfermarkt publishes each in only one of the two views. See the
  seasons note above.
- **Anything requiring a Transfermarkt login.**

***

### Example runs

**One club, current season:**

```json
{ "clubIds": ["281"] }
```

**A whole league's worth of clubs, past season, no loanees:**

```json
{
  "clubIds": ["281", "11", "631", "985", "31"],
  "season": 2024,
  "includePlayersOnLoanElsewhere": false
}
```

**By name:**

```json
{ "searchQueries": ["Ajax", "Benfica"], "maxClubsPerQuery": 1 }
```

***

### 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** — this one.
- **Transfermarkt Competition Scraper** — league tables, participating clubs and
  top market values, for leagues and cups.
- **Transfermarkt Transfers Scraper** — transfer feeds by club, competition or
  the global latest-transfers stream.

The `playerId` this Actor returns is the input to the player scraper, and the
competition scraper's `clubId` is the input to this one.

# Actor input Schema

## `clubIds` (type: `array`):

Transfermarkt club IDs (e.g. 281 for Manchester City) or any Transfermarkt club URL — the squad, profile or transfers page all work, and the name part of the URL is ignored. Each club yields 20–45 charged rows, one per squad player.

## `searchQueries` (type: `array`):

Club names to look up when you don't have IDs. Each name costs one extra request and yields up to `maxClubsPerQuery` clubs — and every club found is a full squad of charged rows.

## `maxClubsPerQuery` (type: `integer`):

How many clubs to take from each name search. Transfermarkt returns its own relevance order, so 1 is usually the club you meant.

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

The year a season starts — 2025 means the 2025/26 season. Leave empty for the current season. A season Transfermarkt has not published yet returns an unbilled SEASON\_NOT\_AVAILABLE row rather than nothing.

## `includePlayersOnLoanElsewhere` (type: `boolean`):

Transfermarkt's squad list includes players currently on loan at another club. Keep them (each is a charged row, flagged with isOnLoanElsewhere and the club they are actually at) or drop them to bill only the players physically in the squad.

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

Hard cap on charged rows for this run, across all clubs. A Premier League club is about 25–45 rows.

## `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
{
  "clubIds": [
    "281",
    "418"
  ],
  "searchQueries": [
    "Bayern Munich"
  ],
  "maxClubsPerQuery": 3,
  "season": 2025,
  "includePlayersOnLoanElsewhere": true,
  "maxItems": 500,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset containing every scraped squad player.

# 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 = {
    "clubIds": [
        "281",
        "https://www.transfermarkt.com/real-madrid/startseite/verein/418"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/transfermarkt-club-squad-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 = { "clubIds": [
        "281",
        "https://www.transfermarkt.com/real-madrid/startseite/verein/418",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("incognito_mode/transfermarkt-club-squad-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 '{
  "clubIds": [
    "281",
    "https://www.transfermarkt.com/real-madrid/startseite/verein/418"
  ]
}' |
apify call incognito_mode/transfermarkt-club-squad-scraper --silent --output-dataset

```

## MCP server setup

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