# Transfermarkt Scraper (`webdatalabs/transfermarkt-scraper`) Actor

Extract structured football data from Transfermarkt: player profiles, current and peak market values, the full value history, complete transfer history with clubs, dates and fees as integers, plus per-competition career stats (appearances, goals, assists). One flat JSON record per player.

- **URL**: https://apify.com/webdatalabs/transfermarkt-scraper.md
- **Developed by:** [WebDataLabs](https://apify.com/webdatalabs) (community)
- **Categories:** Sports, Developer tools, Business
- **Features:** Limited permissions
- **Stats:** 188 total users, 3 monthly users, 100.0% runs succeeded, 4 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $7.00 / 1,000 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.
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 - Football Player Data

Paste Transfermarkt player URLs, get one clean JSON record per player: profile, current and peak market value, the complete transfer history with fees, and whole-career appearances, goals, assists and minutes per competition.

### What you get

One flat record per player, ready for spreadsheets, n8n, Zapier or a database:

| Field group | Fields |
|---|---|
| Identity | `playerId`, `playerName`, `fullName` (name in home country), `profileUrl` |
| Personal | `dateOfBirth`, `age`, `placeOfBirth`, `nationality`, `secondNationality`, `height`, `foot` |
| Position | `position`, `detailedPosition` |
| Club & contract | `currentClub`, `currentClubId`, `joinedDate`, `contractExpiry`, `contractOption` |
| Market value | `marketValue` (integer), `marketValueCurrency`, `marketValueFormatted`, `highestMarketValue`, `highestMarketValueDate` |
| History | `transferHistory[]` (season, date, both clubs + ids, market value at the time, fee text, `feeNumeric`), `marketValueHistory[]` (full valuation time series) |
| Career stats | `careerStats[]` — one row per competition **and** club: `competition`, `competitionId`, `club`, `clubId`, `appearances`, `goals`, `assists`, `minutesPlayed` |
| Extras | `agent`, `agentUrl`, `outfitter`, `instagramUrl`, `achievements[]` |
| Metadata | `scrapedAt`, `scrapedBy` |

**Nulls mean "Transfermarkt does not publish this"** — the Actor never writes `N/A`, `0`, `Unknown` or any other placeholder into a field it could not fill.

### Quick start

```json
{
  "urls": [
    "https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229",
    "https://www.transfermarkt.us/erling-haaland/profil/spieler/418560"
  ],
  "maxItems": 10
}
```

That is the whole configuration. Everything else has a working default.

### Input parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `urls` | array | Mbappé + Haaland | **Required.** Transfermarkt player URLs, one per player. |
| `maxItems` | integer | `100` | Hard cap on players scraped and billed. Extra URLs are reported as skipped. |
| `useProxies` | boolean | `true` | Route through Apify Proxy. Leave it on: Transfermarkt blocks the platform IP after roughly 10 requests. Proxy cost is included in the per-player price. |
| `proxyGroups` | array | `["RESIDENTIAL"]` | `RESIDENTIAL` (recommended) or `DATACENTER`, which Transfermarkt blocks after ~10 requests. |
| `delayBetweenRequests` | integer | `1500` | Milliseconds between player requests. |

#### Supported URLs

Any Transfermarkt **player** URL works — the Actor rewrites it to the profile page automatically:

```
https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229          profile
https://www.transfermarkt.us/filip-stankovic/geruechte/spieler/552223     rumours    -> profile
https://www.transfermarkt.de/x/leistungsdaten/spieler/342229              stats page -> profile
https://www.transfermarkt.co.uk/x/transfers/spieler/342229                transfers  -> profile
https://www.transfermarkt.com/profil/spieler/342229                       no name    -> profile
```

All regional hosts are accepted (`.us`, `.com`, `.de`, `.co.uk`, `.es`, `.it`, `.fr`). Output is always in English.

**Not supported:** club/squad URLs (`/verein/`) and competition URLs (`/wettbewerb/`). Passing one is rejected before any billable work with a message telling you what to pass instead.

### Sample output

```json
{
  "playerId": "421873",
  "playerName": "Elia Caprile",
  "profileUrl": "https://www.transfermarkt.us/elia-caprile/profil/spieler/421873",
  "fullName": null,
  "dateOfBirth": "2001-08-25",
  "age": 24,
  "placeOfBirth": "Verona",
  "nationality": "Italy",
  "secondNationality": null,
  "height": "1.91m",
  "foot": "right",
  "position": "Goalkeeper",
  "detailedPosition": "Goalkeeper",
  "currentClub": "Cagliari Calcio",
  "currentClubId": "1390",
  "joinedDate": "2025-07-01",
  "contractExpiry": "2029-06-30",
  "contractOption": null,
  "marketValue": 15000000,
  "marketValueCurrency": "EUR",
  "marketValueFormatted": "€15.00m",
  "highestMarketValue": 15000000,
  "highestMarketValueDate": "2025-12-23",
  "agent": "GB1",
  "agentUrl": "https://www.transfermarkt.us/gb1-football-agency/beraterfirma/berater/11148",
  "outfitter": null,
  "instagramUrl": "https://www.instagram.com/eliacaprile/",
  "transferHistory": [
    {
      "season": "25/26",
      "date": "2025-07-01",
      "clubFrom": "Napoli",
      "clubFromId": "6195",
      "clubTo": "Cagliari",
      "clubToId": "1390",
      "marketValue": 9000000,
      "fee": "€8.20m",
      "feeNumeric": 8200000
    },
    {
      "season": "24/25",
      "date": "2025-01-07",
      "clubFrom": "Napoli",
      "clubFromId": "6195",
      "clubTo": "Cagliari",
      "clubToId": "1390",
      "marketValue": 6000000,
      "fee": "loan transfer",
      "feeNumeric": null
    }
  ],
  "marketValueHistory": [
    { "date": "2019-09-01", "age": 18, "club": "Chievo Primavera", "clubId": "14845", "value": 50000 }
  ],
  "careerStats": [
    { "competition": "Serie A", "competitionId": "IT1", "club": "Cagliari Calcio", "clubId": "1390", "appearances": 56, "goals": 0, "assists": 0, "minutesPlayed": 5040 },
    { "competition": "Serie B", "competitionId": "IT2", "club": "SSC Bari", "clubId": "332", "appearances": 37, "goals": 0, "assists": 0, "minutesPlayed": 3330 },
    { "competition": "Serie A", "competitionId": "IT1", "club": "SSC Napoli", "clubId": "6195", "appearances": 4, "goals": 0, "assists": 0, "minutesPlayed": 324 }
  ],
  "achievements": [
    { "title": "Serie A winner", "count": 1, "season": null, "club": null, "clubId": null, "competition": null, "competitionId": null }
  ],
  "scrapedAt": "2026-07-27T10:28:17.818+00:00",
  "scrapedBy": "transfermarkt-scraper"
}
```

All money fields are integers in base currency units (`8200000` = €8.20m), so you can sort and aggregate them directly.

`careerStats` is truncated above — the real record has 20 rows, one per competition and club Caprile actually played in. He is a goalkeeper, hence the zeroes in `goals` and `assists`: those are Transfermarkt's real counts, not placeholders.

#### Dataset views

1. **Overview** — name, age, nationality, position, club, market value
2. **Market Value** — current and peak valuations
3. **Full Details** — every field

### How you know a run worked

Every run writes a machine-readable `OUTPUT` record to the key-value store:

```json
{
  "outcome": "ok",
  "message": "Delivered 2 player record(s) for all 2 requested URL(s).",
  "urlsSubmitted": 2,
  "urlsAttempted": 2,
  "itemsDelivered": 2,
  "results": [
    { "inputUrl": "...", "scrapedUrl": "...", "playerId": "342229", "status": "succeeded", "reason": null }
  ]
}
```

- `outcome: "ok"` — every requested player was delivered.
- `outcome: "partial"` — some URLs were skipped or failed; each one is listed with its reason, and the run log says so too.
- `outcome: "empty"` — nothing was delivered, and **the run status is FAILED** with the reason in the status message. A run that scrapes nothing will never report success.
- `outcome: "invalid-input"` — nothing was attempted because no URL was a player profile (for example only club or competition URLs). The run status is FAILED, no player record was charged (only the small per-run start fee), and `message` names each rejected URL and what to pass instead.

`itemsDelivered` is read back from the dataset itself, so it is never larger than what actually landed.

### Use cases

- **Sports analytics** — track valuation trends and transfer fee patterns over time
- **Scouting** — build a watchlist and monitor market value movements
- **Journalism** — transfer market research with sourced fees and dates
- **Fantasy football** — squad metadata and player background
- **Automation** — feed n8n / Zapier / Make / Google Sheets

### Technical details

- **HTTP only (Cheerio + got-scraping)** — Transfermarkt profile pages are server-rendered, so no browser is started. Runs are typically well under 30 seconds per player and cost a fraction of a browser-based scraper.
- **Data sources** — the profile page HTML plus Transfermarkt's own `ceapi/transferHistory`, `ceapi/marketValueDevelopment` and `ceapi/performance-game` JSON endpoints. Career statistics are aggregated from the same per-game feed the site's own stats widget reads, so the numbers match the ones on the page.
- **Residential proxy by default** — Transfermarkt returns HTTP 403 to a single IP after roughly 10 requests and blocks datacenter IPs too. Measured on one 20-URL run: no proxy 10/20, datacenter 10/20, residential 20/20. The proxy cost is ours, included in the per-player price.
- **Retries** — a request is retried only when the response is actually unusable (block page, redirect, 5xx). Deterministic failures are reported, not retried.

### Limitations (read before buying)

- **`careerStats` counts appearances, not squad call-ups.** Only games Transfermarkt marks as played are counted; being an unused substitute, injured or absent produces no appearance and no row. Transfermarkt does not record assists for every competition or era, so `assists: 0` can mean "none recorded" as well as "none made".
- **Players only.** Club, squad and competition scraping are not implemented. Passing such a URL fails fast with a clear message.
- **No search.** You supply the URLs; there is no keyword lookup.
- **Retired players** have no market value on Transfermarkt, so `marketValue`, `marketValueFormatted` and `marketValueHistory` are `null` for them. That is the site's data, not a scraping failure.
- **Public data only.** No login, no restricted content, no live/real-time feed.

### Pricing

Pay-per-event: you are charged **per player record actually written to the dataset**. A URL that produces nothing is not charged. Current per-event prices are shown in the pricing box at the top of this page; platform compute is included, nothing is billed on top.

### API integration

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });

const run = await client.actor('webdatalabs/transfermarkt-scraper').call({
  urls: ['https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229'],
  maxItems: 1,
});

// Always check the run outcome, not just the status
const { value: outcome } = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTPUT');
console.log(outcome.outcome, outcome.message);

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items[0].marketValue, items[0].transferHistory);
```

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")

run = client.actor("webdatalabs/transfermarkt-scraper").call(run_input={
    "urls": ["https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229"],
    "maxItems": 1,
})

outcome = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")["value"]
print(outcome["outcome"], outcome["message"])

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["playerName"], item["marketValue"])
```

### FAQ

**Do I need a Transfermarkt account?** No. Only publicly accessible pages are read.

**Can I scrape many players at once?** Yes — put every player URL in `urls` and raise `maxItems`.

**What if I get blocked?** Make sure `useProxies` is `true` and `proxyGroups` is `["RESIDENTIAL"]` (both are the defaults). Blocked URLs are listed in the `OUTPUT` record with the reason, so you can re-run just those.

**Do the career stats match the numbers on the Transfermarkt page?** Yes. They are aggregated from the same per-game feed the site's own stats widget reads, using Transfermarkt's own definition of an appearance. Rows are split by club as well as competition, so a player's Serie A record appears once per club rather than merged into a single line.

**Why is `marketValue` null for some players?** Transfermarkt publishes no value for them (typically retired players). Check the `OUTPUT` record — if the run says `ok`, the page really had no value.

**Will a run ever silently return nothing?** No. A run that delivers zero records finishes as FAILED with the reason in the status message and in `OUTPUT`.

***

### Explore more of our actors

| Actor | Description |
|-------|-------------|
| [Shopify Store Intelligence](https://apify.com/webdatalabs/shopify-store-intelligence) | Shopify store analysis for competitive intelligence |

### Need this data as a managed feed?

If you would rather receive this data on a schedule than run the Actor yourself, we can build and operate the feed: the fields you specify, the cadence you set, delivered to a dataset, S3, a webhook, or your database. Every scheduled run is checked against an agreed shape rather than assumed to be fine, missing values are reported as null instead of filled with placeholders, and repairing the collector when the source changes is covered by the monthly rate. Priced as a one-time setup fee plus a monthly rate, scoped in writing before anything is built.

**Contact:** support@webdatalabs.net

### Related actors

- [Reddit Scraper Pro](https://apify.com/webdatalabs/reddit-scraper-pro) — posts and full comment threads
- [YouTube Shorts Scraper & Data Extractor](https://apify.com/webdatalabs/youtube-shorts-scraper) — Shorts metadata and engagement
- [Google Trends Scraper - Reliable Trends Data API](https://apify.com/webdatalabs/google-trends-reliable) — reliable interest-over-time data
- [Event Scraper Pro](https://apify.com/webdatalabs/event-scraper-pro) — events across Eventbrite, Meetup and Luma

Explore the full portfolio from [WebDataLabs](https://apify.com/webdatalabs).

### Leave a review

Is this actor saving you time? A quick review on the Store helps other people find it — and tells us which fields to add next. **[Rate this actor →](https://apify.com/webdatalabs/transfermarkt-scraper)**

### Legal disclaimer

This actor is a general-purpose tool for analyzing publicly accessible web data. The user bears sole responsibility for ensuring their specific use complies with applicable laws (GDPR/DSGVO, copyright law), the target website's Terms of Service, and Apify's Terms of Service. The provider (webdatalabs) expressly disclaims liability for any unauthorized or unlawful use. By using this actor, the user agrees to indemnify the provider against any third-party claims arising from their use of the data.

***

*This tool is not affiliated with Transfermarkt. All trademarks belong to their respective owners.*

# Actor input Schema

## `urls` (type: `array`):

One Transfermarkt player URL per player. Any player sub-page works - rumours, transfers, performance data and market value pages are automatically converted to the profile page. Example: https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229 . Club (/verein/) and competition (/wettbewerb/) URLs are not supported.

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

Hard cap on how many players are scraped and billed. URLs beyond this cap are reported as skipped in the run log and in the OUTPUT record.

## `useProxies` (type: `boolean`):

Route requests through Apify Proxy. Recommended: Transfermarkt blocks the Apify platform IP after roughly 10 requests (HTTP 403), so without a proxy larger runs lose URLs. Proxy costs are covered by the per-player price.

## `proxyGroups` (type: `array`):

Which Apify Proxy group to use when the proxy is enabled. Residential is the only group Transfermarkt does not block: on a 20-URL test it delivered 20/20, datacenter 10/20.

## `delayBetweenRequests` (type: `integer`):

Milliseconds to wait between player requests. Increase if you see rate limiting.

## Actor input object example

```json
{
  "urls": [
    "https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229",
    "https://www.transfermarkt.us/erling-haaland/profil/spieler/418560"
  ],
  "maxItems": 100,
  "useProxies": true,
  "proxyGroups": [
    "RESIDENTIAL"
  ],
  "delayBetweenRequests": 1500
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `marketValue` (type: `string`):

No description

## `fullDetails` (type: `string`):

No description

# 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 = {
    "urls": [
        "https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229",
        "https://www.transfermarkt.us/erling-haaland/profil/spieler/418560"
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("webdatalabs/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 = {
    "urls": [
        "https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229",
        "https://www.transfermarkt.us/erling-haaland/profil/spieler/418560",
    ],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("webdatalabs/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 '{
  "urls": [
    "https://www.transfermarkt.us/kylian-mbappe/profil/spieler/342229",
    "https://www.transfermarkt.us/erling-haaland/profil/spieler/418560"
  ],
  "maxItems": 100
}' |
apify call webdatalabs/transfermarkt-scraper --silent --output-dataset

```

## MCP server setup

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