# Transfermarkt Football Transfers & Fees Scraper (`incognito_mode/transfermarkt-transfers-scraper`) Actor

Scrape football transfers from Transfermarkt: every arrival and departure for a club, a whole league's transfer window, or the global latest-transfers feed. Fee, market value, position, age and both clubs on every row. Any season. No API key, no login, no proxy needed.

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

## Pricing

from $1.50 / 1,000 transfer 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 Football Transfers & Fees Scraper

Scrape football transfers from [Transfermarkt](https://www.transfermarkt.com)
three ways — **a club's window**, **a whole league's window**, or the **global
latest-transfers feed** — and get one row per transfer with the fee, the
player's market value at the time, and **both clubs named on every row**.

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

| Field | Value |
| --- | --- |
| `transferId` | `6107087` |
| `source` / `direction` | `club` / `in` |
| `season` / `seasonStartYear` | `25/26` / `2025` |
| `playerId` / `playerName` | `583255` / `Antoine Semenyo` |
| `position` / `age` | `Right Winger` / `26` |
| `nationality` | `["Ghana", "England"]` |
| `marketValue` | `65000000` |
| `fee` | `72000000` |
| `feeType` / `feeText` | `fee` / `€72.00m` |
| `fromClubId` / `fromClubName` | `989` / `AFC Bournemouth` |
| `toClubId` / `toClubName` | `281` / `Manchester City` |
| `clubIncome` / `clubExpenditure` | `107280000` / `301800000` |
| `clubBalance` | `-194520000` |
| `playerUrl` | `https://www.transfermarkt.com/x/profil/spieler/583255` |

**Both ends of the move are on every row.** Transfermarkt's own pages name only
one — a club's page tells you who arrived and leaves "arrived where" implicit in
which table the row was in. Here `fromClub*` and `toClub*` are always filled, so
a club run and a league run stack into one table without a single join.

#### A loan is not a zero-euro transfer

`fee` is **null**, never `0`, for a free transfer, a loan, an end of loan or an
undisclosed fee — because none of those is a fee of zero, and averaging a column
where they became zero quietly halves every number in it. `feeType` says which
of the five cases it was, and `feeText` keeps Transfermarkt's own wording so an
unusual one is never silently rounded into a number.

| `feeType` | `fee` | Transfermarkt's wording |
| --- | --- | --- |
| `fee` | the amount | `€72.00m` |
| `loan` | the loan fee, or null | `loan transfer`, `Loan fee: €2.00m` |
| `free` | null | `free transfer` |
| `end_of_loan` | null, plus `loanEndDate` | `End of loan 30/06/2025` |
| `undisclosed` | null | `?` |

#### `transferId` makes repeat runs safe

Every row carries Transfermarkt's own transfer id, so a daily run against the
latest feed de-duplicates on one integer instead of on a name-and-fee guess.

***

### Three ways to ask

| Input | What it scrapes | Rows | Requests |
| --- | --- | ---: | ---: |
| `clubIds` | one club's arrivals and departures | 20–45 per club | 1 per club |
| `competitionCodes` | **every club** in a league, in and out | **~740** | 1 per code |
| `includeLatestTransfers` | the worldwide latest-confirmed feed | 25 per page, 250 max | 1 per page |

They combine freely in one run, and all three produce **exactly the same
columns**, so the dataset is one table however you asked for it.

#### ⚠️ A league window is around 740 rows

`{"competitionCodes": ["GB1"]}` is twenty clubs' complete transfer business —
around 740 charged rows from one three-character code. That is the single thing
to know before setting `maxItems`, and it is why `maxItems` defaults to `1000`
rather than the smaller default the rest of this suite uses.

**You are never charged for the same transfer twice.** A move between two clubs
of the same league is on Transfermarkt's page twice — the buyer's arrival and
the seller's departure — and the Premier League's 25/26 window is 819 table
rows for 740 actual transfers. Those 79 are collapsed before billing, keeping
the arrival, so `direction` is `in` and `clubId` is the buying club for every
move inside the league. `fromClubId` still names the seller. The same applies
across sources: ask for a club *and* its league and the overlap is charged
once.

***

### Input

```json
{
  "clubIds": ["281", "https://www.transfermarkt.com/real-madrid/startseite/verein/418"],
  "competitionCodes": ["GB1"],
  "includeLatestTransfers": false,
  "season": 2025,
  "maxItems": 1000
}
```

| Input | What it does |
| --- | --- |
| `clubIds` | Numeric IDs **or any Transfermarkt club URL**. The squad, profile and transfers tabs all work — the name in the URL is ignored. |
| `competitionCodes` | League codes (`GB1`, `ES1`, `L1`, `IT1`, `FR1`) or any Transfermarkt competition URL. **~820 rows each.** |
| `includeLatestTransfers` | The global feed, newest first. It is ten pages — 250 transfers — deep, and stops on its own when a page repeats. |
| `season` | The year a season **starts** — `2025` is the 2025/26 window. Leave empty for the current one. Does not apply to the latest feed. |
| `maxItems` | Hard cap on charged rows across every source. Default `1000`. |
| `proxyConfiguration` | Leave it off — see below. |

***

### Cups have no transfer window

Transfermarkt publishes transfer windows **per league**, not per cup. Ask for
`CL` or `EL` and you get an unbilled `COMPETITION_HAS_NO_TRANSFERS` row saying
so, rather than an empty dataset you have to diagnose yourself.

To get Champions League clubs' transfers, take their `clubId`s from the
*Transfermarkt League Table & Club Values Scraper* and pass those here.

***

### What this Actor does NOT return

- **Transfer dates.** Transfermarkt's transfer *tables* do not carry them —
  not the club page, not the league page, not the latest feed. Transfers are
  grouped by season, and the only date anywhere on them is `loanEndDate` on an
  end-of-loan row. Per-player dated transfer history exists on a different
  endpoint, and it is what the *Transfermarkt Player & Market Value Scraper*
  returns.
- **Rumours.** Transfermarkt's rumour pages are not reachable at any documented
  URL; treat the Rumour Mill as unavailable rather than missing.
- **All-time transfer records**, which are a different table with a different
  shape.
- **Contract details or wages.** Not published.
- **Anything requiring a Transfermarkt login.**

***

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

A league window is **one request for ~820 rows**, which is the best
rows-per-request ratio anywhere in this suite.

The Actor start fee is **$0.00005** and the default memory is **512 MB** —
under the 1 GB line, so Apify's per-gigabyte start fee is charged once. Several
Transfermarkt scrapers on the Store run at 4 GB with a $0.10 start fee, which
bills **$0.40 before returning a single row**.

**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

- **Both clubs on every row**, from three sources that each name only one.
- **A loan is null, not zero.** The difference is invisible until someone
  averages the column.
- **Market value alongside the fee**, so "did they overpay" is a subtraction
  rather than a second scrape. The club view only carries it in Transfermarkt's
  detailed layout, which this Actor always requests.
- **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.
- **A cup, a missing club and a missing code are three different answers**,
  each an unbilled row that says which, rather than silence.
- **You are never billed for a failure**, or for the same transfer twice, and
  bad input returns an `INVALID_INPUT` row instead of failing the run.
- **A weekly canary** runs the deployed build against live Transfermarkt across
  all three sources and opens a GitHub issue if field coverage drops.

***

### Example runs

**One club's window:**

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

**A whole league, past season:**

```json
{ "competitionCodes": ["GB1"], "season": 2024, "maxItems": 1000 }
```

**Who did Europe's big five buy from:**

```json
{ "competitionCodes": ["GB1", "ES1", "L1", "IT1", "FR1"], "maxItems": 4000 }
```

**A daily feed of the world's confirmed transfers:**

```json
{ "includeLatestTransfers": true, "maxItems": 200 }
```

***

### Related Actors

Part of a Transfermarkt suite:

- **Transfermarkt Player & Market Value Scraper** — one player in depth, with
  the full market-value and **dated** 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** — league tables merged
  with what every club's squad is worth.
- **Transfermarkt Football Transfers & Fees Scraper** — this one.

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

# Actor input Schema

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

Transfermarkt club IDs (281 for Manchester City) or any Transfermarkt club URL. One charged row per transfer, and one club's window is 20-45 transfers.

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

⚠️ Expensive. A whole league's transfer window is around 740 charged rows — every club, in and out, with moves between two clubs of the league counted once. Codes look like GB1, ES1, L1, IT1. Cups have no transfer window at all and return an unbilled note instead.

## `includeLatestTransfers` (type: `boolean`):

Add Transfermarkt's worldwide latest-confirmed feed, newest first, 25 charged rows per page. It is ten pages (250 transfers) deep and stops on its own; maxItems caps it below that.

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

The year a season starts — 2025 means the 2025/26 window. Leave empty for the current one. Transfermarkt silently serves its newest season for a future year, so check seasonStartYear in the output rather than assuming. Does not apply to the latest-transfers feed.

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

Hard cap on charged rows for this run, across every source. This is the main cost control: one league window alone is around 820 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"
  ],
  "competitionCodes": [
    "GB1"
  ],
  "includeLatestTransfers": false,
  "season": 2025,
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset containing every scraped transfer.

# 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",
        "418"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/transfermarkt-transfers-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",
        "418",
    ] }

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

```

## MCP server setup

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