# Transfermarkt Scraper - Squads, Market Values, Transfers (`neverempty/transfermarkt-players-transfers-monitor`) Actor

Football data from public Transfermarkt pages: club squads (position, age, nationality, market value, contract end), league club tables with total market value, and transfers with fees. Monitoring mode returns only new transfers and changed values or contracts.

- **URL**: https://apify.com/neverempty/transfermarkt-players-transfers-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Sports, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 player, club or transfer row returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Scraper - Squads, Market Values, Transfers

**Unofficial. Reads public Transfermarkt pages only. Not affiliated with, endorsed by or connected to Transfermarkt.**

Get football squads, market values, contract end dates and transfers from Transfermarkt as clean rows, and in monitoring mode get **only what changed since your last run**: new transfers, players whose market value moved, players whose contract end date changed.

Made for agents, scouts, journalists and betting analysts who look at the same clubs every day and do not want to diff 500 rows by hand.

### What you can ask for

| Input | You get | Requests |
|---|---|---|
| A league (URL or code such as `GB1`) | One `club` row per club: squad size, average age, foreigners, average and total market value | 1 |
| A league with "Also read every club's squad" | The club rows plus one `player` row per player of every club | 1 + 1 per club |
| A club (URL or number such as `281`) | One `player` row per squad member: position, shirt number, date of birth, age, nationalities, height, foot, joined, signed from, signing fee, contract end, market value. Plus one `transfer` row per arrival and departure of the current season | 2 (1 without transfers) |
| A player (URL or number such as `418560`) | One `player` row: club, position, contract end, market value | 1 to 2 |
| Latest transfers worldwide | The newest transfers on the site (about 18): player, club left, club joined, fee | 1 |

Market values and fees come as a number in euros (`marketValue: 45000000`), the text as shown (`marketValueText: "€45.00m"`) and `currency: "EUR"`. The Actor always reads the .com edition, so the format is the same on every run.

### Monitoring mode

Turn on `monitoringMode` and schedule the Actor (for example daily).

- The **first run** of a league, club or player remembers its state. It returns one free note row per target and no data rows.
- **Later runs** return only rows that changed, with a `changes` column and the previous value:
  - `new-transfer` - a transfer that was not on the page before
  - `market-value` - with `previousMarketValue`
  - `contract-expiry` - with `previousContractExpires`
  - `new-in-squad` - a player who was not in the squad before
  - `total-market-value` / `new-in-league` - for club rows of a league
- A target with **no change** returns one free note row (`no-change-since-last-run`) and is charged as one `target-checked` event.

The state is kept per target in a named key-value store (`transfermarkt-monitoring`) in your account. Rows that did not fit in the run's maximum charge are not remembered, so they come back at the next run. Avoid running two monitoring runs for the same target at the same moment.

### Input

| Field | What it does |
|---|---|
| `leagues` | League URLs or competition codes (`GB1`, `ES1`, `L1`, `IT1`, `FR1`). |
| `clubs` | Club URLs or club numbers (`281`). |
| `players` | Player URLs or player numbers (`418560`). |
| `latestTransfers` | `true` reads the "Latest transfers" page. |
| `leagueSquads` | `true` also reads the squad of every club of each league. |
| `includeClubTransfers` | `true` (default) also returns the arrivals and departures of each club. |
| `maxRows` | Stop at this many rows (default 500, at most 5,000). Not used in monitoring mode. |
| `monitoringMode` | `true` returns only what changed since the last run. |
| `resetMonitoringState` | `true` forgets the remembered state before the run. |

There is no default league, club or player: a run with none of them returns one free note row and reads nothing.

### Example input

```json
{
    "clubs": ["https://www.transfermarkt.com/manchester-city/startseite/verein/281"],
    "includeClubTransfers": true,
    "monitoringMode": true
}
```

### Example rows

```json
{
    "type": "player", "status": "ok", "playerId": "315858", "playerName": "Gianluigi Donnarumma", "position": "Goalkeeper",
    "playerUrl": "https://www.transfermarkt.com/gianluigi-donnarumma/profil/spieler/315858",
    "shirtNumber": "1", "dateOfBirth": "1999-02-25", "age": 27, "nationalities": ["Italy"], "heightCm": 196, "foot": "right",
    "clubId": "281", "clubName": "Manchester City", "season": "26/27", "joinedDate": "2025-09-01",
    "signedFromClubId": "583", "signedFromClub": "Paris Saint-Germain", "signedFee": 30000000,
    "contractExpires": "2030-06-30", "marketValue": 45000000, "marketValueText": "€45.00m", "currency": "EUR"
}
```

```json
{
    "type": "transfer", "status": "ok", "transferId": "6558535", "playerId": "648195", "playerName": "Enzo Fernández", "position": "Central Midfield",
    "fromClubId": "631", "fromClub": "Chelsea FC", "fromLeague": "Premier League", "fromCountry": "England",
    "toClubId": "281", "toClub": "Manchester City", "feeText": "€145.00m", "feeType": "fee", "fee": 145000000, "currency": "EUR",
    "direction": "arrival", "season": "26/27", "transferDate": null
}
```

`feeType` is one of `fee`, `free`, `loan`, `loan-fee`, `end-of-loan`, `undisclosed` (the site shows `?`), `none` (the site shows `-`) or `other`. `fee` is a number only when the site shows an amount (and `0` for a free transfer).

### Things to know before you rely on it

- **Transfer dates are mostly not available.** The transfers page of a club and the latest transfers page do not show a date. `transferDate` is filled only for an arrival whose player is in the current squad list with the same previous club (the date is the squad list's "Joined" date). Otherwise it is `null`. In monitoring mode, `scrapedAt` of a `new-transfer` row tells you when it was first seen.
- **Player transfer history is not included.** A player input returns his current profile row only.
- **Club transfers cover the current season** as shown on the club's transfers page.
- **The latest transfers page is one page** of about 18 rows; the site returns the same rows for page 2.
- **Leagues need a club table.** Cup competitions without a league-style club table come back as a free `unreadable` note row.
- **No player photos.** Image addresses are not returned.
- Market values on Transfermarkt are community estimates, not prices.

### Limits and pace

- One request at a time, 2.5 seconds apart, at most 400 requests per run. A 20-club league with squads takes about one minute.
- `maxRows` is 500 by default and at most 5,000 per run.
- If Transfermarkt refuses a request (HTTP 403 or 429), the run stops sending requests and says so in a free note row. It does not retry through other connections.

### Rows that are not charged

Every row with `type: "note"` is free and says why something was not returned: `bad-input`, `not-found` (a club, league or player that does not exist), `unreadable`, `blocked`, `empty`, `first-check`, `no-change-since-last-run`, `limit-reached`, `budget-reached`, `not-read`.

### Pricing

Pay per event: one `record-returned` event per player, club or transfer row, and one `target-checked` event for each monitored target that had no change. No start fee.

### Fair use

Use the data in line with Transfermarkt's terms and the law that applies to you. This Actor reads only pages that are public without login, at a slow pace.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `leagues` (type: `array`):

League pages on Transfermarkt, as a URL or as the competition code at the end of the URL (GB1 = Premier League, ES1 = LaLiga, L1 = Bundesliga, IT1 = Serie A, FR1 = Ligue 1). Each league returns one row per club with squad size, average age, foreigners, average and total market value. Turn on "Also read every club's squad" to get the players too.

## `clubs` (type: `array`):

Club pages on Transfermarkt, as a URL or as the club number in the URL (281 = Manchester City, the number after /verein/). Each club returns one row per player of the current squad (position, date of birth, age, nationality, height, foot, joined, signed from, contract end, market value) and, unless turned off below, one row per arrival and departure of the current season.

## `players` (type: `array`):

Player pages on Transfermarkt, as a URL or as the player number in the URL (418560, the number after /spieler/). Each player returns one row with current club, contract end and market value.

## `latestTransfers` (type: `boolean`):

Read the "Latest transfers" page of Transfermarkt: the newest transfers entered on the site (about 18), with player, club left, club joined and fee. The page shows no transfer date.

## `leagueSquads` (type: `boolean`):

For every league given above, also read the squad of each of its clubs (one request per club, 2.5 seconds apart), so a 20-club league returns about 20 club rows and 500 player rows.

## `includeClubTransfers` (type: `boolean`):

For every club, also read its transfers page of the current season (one more request per club) and return one row per arrival and departure with the other club and the fee. Turn off to get player rows only.

## `maxRows` (type: `integer`):

The run stops reading and returning rows at this number (players, clubs and transfers together). Not used in monitoring mode, where every change is returned.

## `monitoringMode` (type: `boolean`):

The first run of a league, club or player remembers its state and returns nothing for it. Later runs return only: new transfers, players whose market value changed, players whose contract end changed, new squad members, and clubs whose total market value changed, each with the previous value. A target with no change returns one free note row and is charged as one target-checked event.

## `resetMonitoringState` (type: `boolean`):

Clears everything monitoring mode remembers (for all targets) before this run.

## Actor input object example

```json
{
  "leagues": [
    "https://www.transfermarkt.com/premier-league/startseite/wettbewerb/GB1"
  ],
  "latestTransfers": false,
  "leagueSquads": false,
  "includeClubTransfers": true,
  "maxRows": 500,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per player (type player), club (type club) or transfer (type transfer) read from public Transfermarkt pages. In monitoring mode only rows that are new or changed since the last run, with the previous value. Targets that could not be read, runs with no change and runs that hit a limit come back as their own rows (type note) and are not charged.

# 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 = {
    "leagues": [
        "https://www.transfermarkt.com/premier-league/startseite/wettbewerb/GB1"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/transfermarkt-players-transfers-monitor").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 = { "leagues": ["https://www.transfermarkt.com/premier-league/startseite/wettbewerb/GB1"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/transfermarkt-players-transfers-monitor").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 '{
  "leagues": [
    "https://www.transfermarkt.com/premier-league/startseite/wettbewerb/GB1"
  ]
}' |
apify call neverempty/transfermarkt-players-transfers-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/transfermarkt-players-transfers-monitor"
        }
    }
}
```

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/iOrlLmqu6yAgUxUle/builds/eV4Ri29Hp3JlOOaWl/openapi.json
