Transfermarkt Player & Market Value Scraper
Pricing
from $1.20 / 1,000 player scrapeds
Transfermarkt Player & Market Value Scraper
Scrape Transfermarkt football players by id, URL or name. Returns market value, full market-value history, every transfer with fees, contract expiry, agent, position, height, foot, citizenship and caps/goals. No API key, no login, no residential proxy — runs on the free plan. JSON, CSV or Excel.
Pricing
from $1.20 / 1,000 player scrapeds
Rating
0.0
(0)
Developer
Elena Vance
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Share
Scrape football players from Transfermarkt — the reference database for transfer market values — by player ID, by URL, or just by name. Every run returns a flat, typed record per player plus the player's complete market-value history and complete transfer history, with fees as numbers rather than strings.
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 player. This is a real, unedited run (playerIds: ["418560"]),
abbreviated only to fit the page:
| Field | Value |
|---|---|
playerId | 418560 |
name | Erling Haaland |
nameInHomeCountry | Erling Braut Håland |
dateOfBirth | 2000-07-21 |
age | 26 |
placeOfBirth | Leeds |
heightCm | 195 |
citizenship | ["Norway"] |
positionGroup / position | Attack / Centre-Forward |
foot | left |
shirtNumber | 9 |
playerAgent | Rafaela Pimenta |
outfitter | Nike |
clubId / clubName | 281 / Manchester City |
competitionCode / competitionName | GB1 / Premier League |
joinedClub | 2022-07-01 |
contractExpires | 2034-06-30 |
contractExtensionDate | 2025-01-17 |
marketValue | 220000000 |
marketValueCurrency | € |
marketValueLastUpdate | 2026-07-22 |
highestMarketValue | 220000000 |
totalTransferFees | 88100000 |
nationalTeam | {"team": "Norway", "caps": 55, "goals": 62} |
portraitUrl | https://img.a.transfermarkt.technology/portrait/header/418560-…png |
profileUrl | https://www.transfermarkt.com/x/profil/spieler/418560 |
Plus two nested arrays:
marketValueHistory — every valuation Transfermarkt has ever recorded
(28 points for Haaland, back to €200k at Bryne FK in 2016):
[{ "date": "2016-12-18", "marketValue": 200000, "marketValueFormatted": "€200k", "clubName": "Bryne FK", "age": 16 },{ "date": "2019-11-07", "marketValue": 30000000, "marketValueFormatted": "€30.00m", "clubName": "Red Bull Salzburg", "age": 19 },{ "date": "2026-07-22", "marketValue": 220000000, "marketValueFormatted": "€220.00m", "clubName": "Manchester City","age": 26 }]
transferHistory — every career move, with the fee as a number:
[{"season": "22/23", "date": "2022-07-01","fromClubId": 16, "fromClubName": "Dortmund","toClubId": 281, "toClubName": "Man City","marketValueAtTransfer": 150000000,"fee": 60000000, "feeFormatted": "€60.00m", "feeType": "fee","isUpcoming": false}]
Money and dates are parsed, not passed through
Transfermarkt renders €220.00m, €200k, €1.39bn and dates as dd/mm/yyyy.
This Actor emits 220000000, 200000, 1390000000 and 2000-07-21, so the
data sorts, sums and filters without a cleaning pass. The original strings are
kept beside the numbers (marketValueFormatted, feeFormatted) where they add
anything.
A fee that is not a number is not turned into one. free transfer,
End of loan and ? (undisclosed) become fee: null with feeType set to
free, end_of_loan or undisclosed. Coercing those to 0 would be
indistinguishable from a real zero once it is in your dataset.
Input
Give it IDs, URLs, names, or any mix of the three.
{"playerIds": ["418560","https://www.transfermarkt.com/jude-bellingham/profil/spieler/581678"],"searchQueries": ["Lamine Yamal"],"maxPlayersPerQuery": 1,"includeMarketValueHistory": true,"includeTransferHistory": true,"maxItems": 100}
| Input | What it does |
|---|---|
playerIds | Numeric IDs or any Transfermarkt player URL. The profile, transfers, injuries and performance URLs all work — the name in the URL is ignored, so a copy-paste from the browser is enough. |
searchQueries | Player names, when you don't have IDs. Each name is one extra request and yields up to maxPlayersPerQuery players. |
maxPlayersPerQuery | How many players to take per name. Transfermarkt returns its own relevance order, so 1 is usually the player you meant. Default 5. |
includeMarketValueHistory | Nested valuation history. One extra request per player, no extra charged rows. Default on. |
includeTransferHistory | Nested transfer history. One extra request per player, no extra charged rows. Default on. |
maxItems | Hard cap on charged rows. Default 100. |
proxyConfiguration | Leave it off — see below. |
Cost lever
The one input that multiplies your bill is searchQueries × maxPlayersPerQuery:
three names at the default maxPlayersPerQuery: 5 is up to 15 charged rows,
not three. Set it to 1 when you want one player per name.
Neither history toggle affects the bill. They add a request each, not a row.
Why this is cheap to run
Transfermarkt is fronted by AWS WAF, and the usual answer to that — a residential proxy at $8/GB, on pages that are 124 KB each — is what makes most 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, a 20-player run with both histories: 7.1 s, 75 MB peak, 0.00049 compute units, $0.000053 of platform usage — about $0.0027 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. On a 10-player evaluation run this one costs about $0.012, against $0.10–$0.40 of start fee alone elsewhere.
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. The proxy input is there for people who need a specific configuration; the default of "no proxy" is the right answer.
What makes this different
- The whole valuation history, not just today's number. Both histories come from Transfermarkt's own JSON endpoints, so they are exact rather than scraped out of a chart, and adding them costs you nothing per row.
- A missing player is reported, not invented. Transfermarkt answers an
unknown ID with HTTP 200 and quietly redirects to its "most valuable
players" page — a complete, valid page about something else. A scraper that
trusts the status code returns a plausible-looking empty row. This one checks
where the request actually landed and emits an unbilled
PLAYER_NOT_FOUND. - You are never billed for a failure. Rows carrying
errorare pushed for visibility and never charged. Bad input does not fail the run either — it returns anINVALID_INPUTrow explaining what to fix. - Partial failures are named. If some IDs could not be fetched, the run's status message lists them so you can retry those specific ones, instead of leaving you to diff a short dataset against your input.
- A weekly canary runs the deployed build against live Transfermarkt and opens a GitHub issue if field coverage drops — because the unit tests run on captured fixtures and would stay green through a redesign.
What this Actor does NOT return
- Transfer rumours. Every rumour URL Transfermarkt's structure suggests
(
/geruechtekueche/…,/geruechte/…) returns 404. There is no public rumour surface to scrape, so nothing is emitted rather than something guessed. - Match-by-match performance logs and injury history. Both exist on Transfermarkt and neither is in this Actor's scope yet.
- Anything requiring a Transfermarkt login, including Transfermarkt PRO data.
- A "current" market value that is fresher than Transfermarkt's own. Values
are revalued periodically;
marketValueLastUpdatetells you exactly when the number you got was set.
Example runs
One player, everything:
{ "playerIds": ["418560"] }
A squad's worth of IDs, no histories (fastest and smallest):
{"playerIds": ["418560", "581678", "937958"],"includeMarketValueHistory": false,"includeTransferHistory": false}
By name, one player each:
{ "searchQueries": ["Haaland", "Bellingham", "Yamal"], "maxPlayersPerQuery": 1 }
Output formats
JSON, JSONL, CSV, Excel, XML and RSS, from the Console or the API:
https://api.apify.com/v2/datasets/<datasetId>/items?format=csv
The nested marketValueHistory and transferHistory arrays flatten into
indexed columns in CSV and Excel. Turn them off if you want a strictly flat
table.
Related Actors
Part of a Transfermarkt suite:
- Transfermarkt Player & Market Value Scraper — this one.
- Transfermarkt Club Squad Scraper — a full squad per club, with values, contracts and joined dates.
- 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 others, and vice versa.