# Transfermarkt Data: Player Values, Contracts & Transfers (`solalab_digital/transfermarkt-scraper`) Actor

Scrapes Transfermarkt player profiles, club squads, league tables and club transfers into normalised rows: market value as a number, ISO dates, height in cm, split positions, plus optional market value and transfer history and run-over-run change tracking.

- **URL**: https://apify.com/solalab\_digital/transfermarkt-scraper.md
- **Developed by:** [Sankov Vadim](https://apify.com/solalab_digital) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 results

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Transfermarkt Data: player values, contracts and transfers

Point it at a club, a league or a player name and get back rows with numbers as numbers: market value in euros, ISO dates, height in centimetres, split positions. Add the site's own market-value and transfer-history feeds, a momentum score, a contract-risk flag and a run-over-run diff, all from public pages - no login, no browser.

You get three entry modes (player / club / league), search by name, normalised fields next to the site's own raw labels, optional market-value and transfer history, a 0-100 momentum score and a contract-risk indicator, squad monitoring between runs, and a self-contained HTML report. It runs on 256 MB.

### 🚀 Quick start

1. Open the Actor and leave the defaults: **What to scrape** set to `club`, with Manchester City's squad prefilled in **Start URLs**.
2. Press **Start**. The default run is one request for about 25 players - a near-free trial.
3. Open the **Overview** view of the dataset, or open the `REPORT` file in the run's key-value store for a sortable dashboard. Then switch to `player` or `league`, raise `maxPlayers` / `maxClubs`, and turn on the history and monitoring options for a full run.

### 🧭 Who it is for

- **Fantasy football and sports analytics.** Pull market value, age, position and contract data as clean numbers instead of scraping HTML yourself.
- **Scouts and agents.** Track a shortlist's market-value momentum and flag players heading into the last year of their contract.
- **Sports media and content creators.** Get transfer fees, dates and market-value-at-transfer for a club's arrivals and departures, ready for a table or a chart.
- **Data engineers and researchers.** A flat, documented schema across players, clubs and leagues, with a dataset schema and views already defined - no scraper to build or selectors to maintain.

### ✨ What it does

#### Three ways in: player, club, league

`entityType` decides what a row is. `club` turns every club you give into its squad, one row per player. `player` scrapes individual players by URL, ID or name search. `league` walks a competition: clubs first (`maxClubs`), then their squads. Any Transfermarkt player, club, league or club-transfers URL works in **Start URLs** - only the numeric ID (or league code, e.g. `GB1`) inside it matters, not the slug.

#### Search by name

Set `entityType` to `player` and type a name into `searchQuery` instead of a URL. The Actor runs it through the site's own quick search and scrapes the players it finds.

#### Two crawl depths

`crawlDepth: 1` reads only the listing page - name, position, age, nationality, market value - one request per club or league page. `crawlDepth: 2` also opens every player's profile: date and place of birth, height, foot, agent, contract dates, outfitter. Player start URLs always open the profile regardless of depth.

#### Numbers instead of strings

Where the source page prints `€45.00m`, `1,95 m` or `21/07/2000 (26)`, the dataset carries `marketValueEur: 45000000`, `heightCm: 195`, `dateOfBirth: "2000-07-21"` and `age: 26`. Positions are split into `positionMain` (Goalkeeper / Defender / Midfield / Attack) and `positionSub` (e.g. Centre-Forward). The site's own label/value pairs are kept too, in `raw{}`, for anyone who wants the original text.

#### Market value and transfer history from the site's own feeds

Turn on `includeMarketValueHistory` and `includeTransferHistory` to add, per player, the full valuation series (`marketValueHistory[]`: date, value, club, age) and the career transfer list (`transferHistory[]`: date, season, both clubs, fee, fee kind, market value at the time). Each adds one request per player to the site's own JSON endpoints - the same data the market-value graph and transfer table on the page are built from.

#### Momentum score and contract risk

Two indices computed from the data the site already gives you, not a valuation:

- `valueMomentum6m` / `valueMomentum12m`: the market value now divided by the last history point at least 6 or 12 months old, minus 1. `0.25` means +25%. Needs `includeMarketValueHistory`.
- `momentumScore`: `round(50 + 50 * clamp(mean(available momentums), -1, 1))` - 50 is flat, 100 is +100% or more, 0 is -100% or worse.
- `contractMonthsLeft`: days between today and `contractExpires`, divided by 30.4375.
- `contractRisk`: `round(100 * clamp((36 - monthsLeft) / 30, 0, 1))` - 100 at six months left or less (or an expired contract), 0 at three years or more.

#### Watch a squad over time

Turn on `compareWithPrevious` and every player row gets a `monitor` block comparing it with the snapshot saved under `monitorKey` by the previous run: `new`, `changed` (market value moved 5% or 100,000 EUR or more, club changed, or the contract date changed) or `unchanged`. A `MONITOR_DIFF` file in the key-value store lists new, changed and gone players (only meaningful on a complete run). The snapshot is a named key-value store and is only overwritten after a clean, complete run.

#### Built-in HTML report

Every run saves a self-contained `REPORT` page in the key-value store: a sortable, filterable player table (text, position, club, age, value range), top-N lists by market value and by momentum (10/25/50), an SVG market-value chart for a chosen player when history is included, the monitoring block, and a CSV export of whatever is filtered. No external scripts.

#### Polite by default

The default rate is 2 requests per second - deliberately conservative, not a technical ceiling anyone confirmed. It is halved automatically after a 429 and eased back up after a run of clean successes. Proxy is off by default because a direct connection worked in testing; `proxyConfiguration` is there to switch on if your log shows 403s. The whole Actor runs on 256 MB with plain HTTP requests, no browser.

### 🆚 Why use this Actor

| You need | What you get here |
|---|---|
| Market value as a number, not a string | `marketValueEur` as an integer, everywhere it appears |
| Dates you can sort and filter | ISO `yyyy-mm-dd` for birth date, joining date and contract expiry |
| To see where a value is heading | `valueMomentum6m` / `12m` and a 0-100 `momentumScore`, formula documented |
| To flag expiring contracts | `contractMonthsLeft` and a 0-100 `contractRisk` |
| One request in, not a scraper to build | `player`, `club` and `league` modes, plus name search |
| Every valuation and transfer a player has | `marketValueHistory[]` and `transferHistory[]` straight from the site's own JSON |
| To track a squad between runs | Named-store snapshots, a `monitor` block per row, a diff file |
| A quick visual read | Built-in HTML report with a value chart and CSV export |
| Low cost, no browser | Plain HTTP, 256 MB, polite defaults |

### ⚙️ Input

```json
{
  "entityType": "club",
  "startUrls": [{ "url": "https://www.transfermarkt.com/manchester-city/startseite/verein/281" }],
  "crawlDepth": 1,
  "maxPlayers": 30,
  "includeMarketValueHistory": false,
  "includeTransferHistory": false,
  "compareWithPrevious": false
}
```

| Field | Type | Default | Meaning |
|---|---|---|---|
| `entityType` | select | `club` | `club`, `player` or `league` - decides what a row is. |
| `startUrls` | array | Manchester City squad | Any player, club, league or club-transfers URL. Only the numeric ID / league code matters. |
| `ids` | string\[] | `[]` | Numeric player/club IDs or league codes (e.g. `GB1`, `ES1`), read according to `entityType`. |
| `searchQuery` | string | `""` | Player name to look up through quick search. Only used when `entityType` is `player`. |
| `crawlDepth` | integer | `1` | `1` = listing page only; `2` = also opens every player profile. |
| `maxClubs` | integer | `20` | Clubs opened per league. League mode only. |
| `maxPlayers` | integer | `30` | Ceiling on player rows for the whole run. |
| `maxRequests` | integer | `200` | Hard ceiling on HTTP requests, retries included. |
| `includeMarketValueHistory` | boolean | `false` | Full valuation history, peak value and momentum. +1 request per player. |
| `includeTransferHistory` | boolean | `false` | Career transfers: date, clubs, fee, fee kind, value at the time. +1 request per player. |
| `includeClubRow` | boolean | `false` | Also write one row per club: squad size, average age, foreigners, total value. |
| `positions` | select\[] | `[]` | Keep only these position groups. Empty means all. |
| `minMarketValueEur` / `maxMarketValueEur` | integer | - | Drop players outside this value range. |
| `minAge` / `maxAge` | integer | - | Drop players outside this age range. |
| `compareWithPrevious` | boolean | `false` | Diff against the previous run's snapshot for the same `monitorKey`. |
| `monitorKey` | string | derived | Snapshot slot name. Empty derives it from entity type and inputs. |
| `maxRequestsPerSecond` | number | `2` | Request rate cap, 0.5-10. Halved automatically after a 429. |
| `maxConcurrency` | integer | `4` | Parallel in-flight requests, 1-10. |
| `proxyConfiguration` | proxy | off | Optional. Switch on if the log shows 403 or repeated 429. |

Filters run before a row is written, so anything they drop is never stored and never charged for. The prefill (one club, `maxPlayers` 30, depth 1, history off) costs one request for roughly 25 rows.

### 📦 Output

One row per player, club or transfer, `resultType` telling you which: `player`, `club`, `transfer` or `searchHit`. Here is a real player row, produced from this Actor's own profile parser and its own market-value/transfer-history fetchers against a saved Erling Haaland profile page (`crawlDepth: 2`, both histories on). The history and transfer arrays are shown trimmed - the full row carries every point and every transfer the endpoint returns:

```json
{
  "resultType": "player",
  "playerId": "418560",
  "url": "https://www.transfermarkt.com/erling-haaland/profil/spieler/418560",
  "name": "Erling Haaland",
  "nameHome": "Erling Braut Håland",
  "shirtNumber": "9",
  "position": "Attack - Centre-Forward",
  "positionMain": "Attack",
  "positionSub": "Centre-Forward",
  "dateOfBirth": "2000-07-21",
  "age": 26,
  "placeOfBirth": "Leeds",
  "citizenship": ["Norway"],
  "heightCm": 195,
  "foot": "left",
  "agent": "Rafaela Pimenta",
  "club": "Man City",
  "clubId": "281",
  "clubUrl": "https://www.transfermarkt.com/manchester-city/startseite/verein/281",
  "joined": "2022-07-01",
  "contractExpires": "2034-06-30",
  "lastContractExtension": "2025-01-17",
  "outfitter": "Nike",
  "marketValueText": "€220.00m",
  "marketValueEur": 220000000,
  "marketValueUpdated": "2026-07-22",
  "marketValueHistory": [
    { "date": "2016-12-18", "valueEur": 200000, "club": "Bryne FK", "age": 16 },
    { "date": "2019-11-07", "valueEur": 30000000, "club": "Red Bull Salzburg", "age": 19 },
    { "date": "2019-12-16", "valueEur": 45000000, "club": "Red Bull Salzburg", "age": 19 }
  ],
  "peakMarketValueEur": 45000000,
  "valueMomentum6m": 3.8889,
  "valueMomentum12m": 3.8889,
  "momentumScore": 100,
  "transferHistory": [
    { "date": "2019-01-01", "season": "18/19", "fromClub": "Molde FK", "toClub": "Salzburg", "feeEur": 8000000, "feeKind": "fee", "mvEur": 5000000 },
    { "date": "2020-01-01", "season": "19/20", "fromClub": "Salzburg", "toClub": "Dortmund", "feeEur": 20000000, "feeKind": "fee", "mvEur": 45000000 },
    { "date": "2022-07-01", "season": "22/23", "fromClub": "Dortmund", "toClub": "Man City", "feeEur": 60000000, "feeKind": "fee", "mvEur": 150000000 }
  ],
  "contractMonthsLeft": 93.1,
  "contractRisk": 0,
  "scrapedAt": "2026-09-27T10:39:36+00:00"
}
```

Note on this specific example: the saved capture used to build this dataset row only returned market-value points through late 2019, so `peakMarketValueEur` and the momentum figures here are computed from that shorter series, not from a value history running up to today. A live run pulls whatever points the endpoint currently returns.

Club rows (`resultType: "club"`) carry `clubId`, `name`, `leagueName`, `squadSize`, `avgAge`, `foreigners`, `avgMarketValueEur`, `totalMarketValueEur`, `url`. Transfer rows (`resultType: "transfer"`, from a club's transfers page) carry `direction` (`arrival`/`departure`), `player`, `position`, `age`, `fromClub`, `toClub`, `feeEur`, `feeKind`, `season`. Search hits (`resultType: "searchHit"`) carry `hitType` (`player`/`club`) plus the matching identity fields.

Dataset views: **Overview** (identity, position, age, value, contract, momentum, risk), **Values** (market value, peak, updated date, momentum), **Contracts** (joined, expires, months left, risk, agent), **Transfers**. Export as JSON, CSV, Excel, XML, RSS or HTML from the dataset tab, or read it through the Apify API.

### ⚠️ Limits, stated plainly

- **Unofficial endpoints.** The market-value history and transfer-history data come from the site's own `/ceapi/` JSON endpoints that its own pages call in the browser - not a documented, versioned API. They can change or disappear without notice; a broken endpoint returns `null` for the affected fields instead of failing the run.
- **No season statistics.** Goals, appearances and minutes played are not collected: they are not present in the page's server-rendered HTML, only loaded separately in the browser, and are out of scope for this Actor.
- **Selectors can change.** The HTML parsers key off CSS classes on the profile, squad, league and transfer pages. A layout change can turn a field to `null` rather than crash the run, but it can also mean a field goes quiet until the parser is updated.
- **Rate ceiling not confirmed, and it is a shared site.** Testing saw no blocks at a polite pace, which is why the default is 2 requests per second. Running faster (above roughly 2-3 requests per second) or re-running the same targets very frequently from one IP can trigger 403 responses. `proxyConfiguration` is available if that happens.
- **Small, seasonal niche.** Squad and market-value activity concentrates around the transfer windows; expect quieter data (and quieter demand) outside them.
- **English only.** The Actor reads `www.transfermarkt.com` with `Accept-Language: en`; labels on other-language Transfermarkt domains are not parsed.

### 💳 Pricing

Pay per event. See the **Pricing** tab of this Actor for the current event prices. A row is charged when it is added to the dataset - filtered-out rows are not. Momentum, contract risk, history and monitoring cost nothing extra.

### ❓ FAQ

**Does it give live scores or match statistics?**
No. This Actor covers player, squad, market-value and transfer data. Goals, appearances and minutes are not in the source pages it reads.

**Do I need a proxy?**
No, not by default - a direct connection worked in testing. Turn on `proxyConfiguration` only if your run starts seeing 403s.

**Can I track a squad over time?**
Yes. Turn on `compareWithPrevious`, keep the same `monitorKey`, and schedule the Actor. Each run marks players as new, changed or unchanged against the last clean snapshot.

**Why is a field `null`?**
The source page did not print it for that player (common for `agent`, `outfitter`, `lastContractExtension`), a history endpoint returned nothing, or a selector missed a layout change. A missing field never fails the run.

**Is a Transfermarkt account needed?**
No. Only public pages and the site's own public JSON endpoints are used.

**What does `momentumScore` measure?**
A documented arithmetic mapping of the site's own market-value history onto a 0-100 scale, not a market valuation or a prediction.

### 🔌 Integrations

Send results on through the Apify API, webhooks, Zapier, Make, n8n or Google Sheets. The flat per-`resultType` schema loads into a spreadsheet as is.

### 📝 Changelog

- **0.1** First release: player/club/league entry modes, name search, normalised fields, market-value and transfer history, momentum score, contract risk, run-over-run monitoring, HTML report.

### 🛟 Support

Something looks off, or you need a field added? Open an issue from the Actor's **Issues** tab and include the run link and the input you used.

### 🔗 Related Actors

Check the author's profile on Apify Store for other sports and football data Actors.

# Actor input Schema

## `entityType` (type: `string`):

Decides what a row is. 'club' turns every club you give into its squad (one row per player). 'player' scrapes individual players. 'league' walks a competition: clubs first, then their squads.

## `startUrls` (type: `array`):

Any transfermarkt.com player, club, league, club-transfers or quick-search URL. The slug does not matter, only the numeric ID (or the league code) inside the URL.

## `ids` (type: `array`):

Numeric player or club IDs, or league codes such as GB1, ES1, L1. They are read according to 'What to scrape'.

## `searchQuery` (type: `string`):

Player name to look up through the site's quick search. Used only when 'What to scrape' is set to player.

## `crawlDepth` (type: `integer`):

1 reads only the listing page: name, position, age, nationality and market value, one request per club. 2 opens every player profile as well, adding date and place of birth, height, foot, agent, contract dates and outfitter - one extra request per player. Player start URLs always open the profile.

## `maxClubs` (type: `integer`):

How many clubs of a competition are opened. Only used in league mode.

## `maxPlayers` (type: `integer`):

Ceiling on player rows for the whole run, across every club and league.

## `maxRequests` (type: `integer`):

Hard ceiling on HTTP requests, retries included. The main guard against a runaway run: when it is hit, everything already collected is still saved.

## `includeMarketValueHistory` (type: `boolean`):

Full valuation history per player (date, value, club, age), plus the peak value and the momentum figures. One extra request per player.

## `includeTransferHistory` (type: `boolean`):

Career transfers per player: date, season, both clubs, fee and market value at the time. One extra request per player.

## `includeClubRow` (type: `boolean`):

Also write one row per club with squad size, average age, foreigners and total market value.

## `positions` (type: `array`):

Keep only these position groups. Empty means all.

## `minMarketValueEur` (type: `integer`):

Drop players valued below this, e.g. 10000000 for ten million.

## `maxMarketValueEur` (type: `integer`):

Drop players valued above this.

## `minAge` (type: `integer`):

Drop players younger than this.

## `maxAge` (type: `integer`):

Drop players older than this.

## `compareWithPrevious` (type: `boolean`):

Load the snapshot saved by the previous run with the same monitor key and mark every player as new, changed or unchanged, with the market value delta. The diff also lands in MONITOR\_DIFF.

## `monitorKey` (type: `string`):

Name of the snapshot slot. Leave empty to derive it from the entity type and the start URLs or IDs.

## `maxRequestsPerSecond` (type: `number`):

Request rate cap. The default of 2 is deliberately polite; the rate is halved automatically whenever the site answers 429.

## `maxConcurrency` (type: `integer`):

Parallel in-flight requests. The rate cap above still applies.

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

Optional. The actor connects directly by default and that works from Apify's own IPs. Switch on Apify Proxy if the log starts showing 403 or repeated 429.

## Actor input object example

```json
{
  "entityType": "club",
  "startUrls": [
    {
      "url": "https://www.transfermarkt.com/manchester-city/startseite/verein/281"
    }
  ],
  "ids": [],
  "searchQuery": "",
  "crawlDepth": 1,
  "maxClubs": 20,
  "maxPlayers": 30,
  "maxRequests": 200,
  "includeMarketValueHistory": false,
  "includeTransferHistory": false,
  "includeClubRow": false,
  "positions": [],
  "compareWithPrevious": false,
  "maxRequestsPerSecond": 2,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `report` (type: `string`):

Sortable and filterable player table with a market value chart, top lists, the monitoring block and a CSV export.

## `players` (type: `string`):

One row per player: identity, position, age, market value, contract and the derived indices.

## `values` (type: `string`):

Market value, peak value and momentum per player.

## `contracts` (type: `string`):

Contract dates, months left and contract risk per player.

## `transfers` (type: `string`):

Arrival and departure rows from club transfer pages.

## `monitorDiff` (type: `string`):

New, changed and gone players compared with the previous snapshot. Written only when comparison is enabled.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.transfermarkt.com/manchester-city/startseite/verein/281"
        }
    ],
    "crawlDepth": 1,
    "maxPlayers": 30,
    "maxRequests": 200,
    "includeMarketValueHistory": false,
    "includeTransferHistory": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("solalab_digital/transfermarkt-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 = {
    "startUrls": [{ "url": "https://www.transfermarkt.com/manchester-city/startseite/verein/281" }],
    "crawlDepth": 1,
    "maxPlayers": 30,
    "maxRequests": 200,
    "includeMarketValueHistory": False,
    "includeTransferHistory": False,
}

# Run the Actor and wait for it to finish
run = client.actor("solalab_digital/transfermarkt-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 '{
  "startUrls": [
    {
      "url": "https://www.transfermarkt.com/manchester-city/startseite/verein/281"
    }
  ],
  "crawlDepth": 1,
  "maxPlayers": 30,
  "maxRequests": 200,
  "includeMarketValueHistory": false,
  "includeTransferHistory": false
}' |
apify call solalab_digital/transfermarkt-scraper --silent --output-dataset

```

## MCP server setup

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