Tennis Abstract Match History Scraper - Stats, H2H, New Matches
Pricing
from $12.00 / 1,000 match or head-to-head returneds
Tennis Abstract Match History Scraper - Stats, H2H, New Matches
For tennis bettors, modelers and analysts: every ATP singles match on a player's Tennis Abstract page - score, round, surface, opponent, ranks, aces, serve points, break points. Djokovic: 1,516 matches. Filter by date, surface, level or opponent; get head-to-heads and only new matches.
Pricing
from $12.00 / 1,000 match or head-to-head returneds
Rating
0.0
(0)
Developer
NeverEmpty
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
For tennis bettors, modelers and analysts: every ATP singles match on a player's Tennis Abstract page, one row per match - tournament, level, surface, round, result, score, opponent with rank, seed, hand, height and country, plus aces, double faults, serve points, first serves in, first- and second-serve points won, service games and break points for both players. On 21 Sep 2026 Tennis Abstract listed 1,516 matches for Novak Djokovic (2003-2026) and 565 for Jannik Sinner (506 of them with serve stats). Pull several players in one run by name, filter by date, surface, level, opponent or result, get head-to-head records, or schedule it to return only the matches added since the last run - no copying tables by hand and no guessing player ids.
Export as JSON, CSV or Excel.
The data comes from Tennis Abstract's public player pages (/cgi-bin/player-classic.cgi?p=<id>) and its public player list (/mwplayerlist.js). This Actor is unofficial and not affiliated with Tennis Abstract or the ATP. For Elo ratings and win probabilities from the same site, see Tennis Elo Ratings Scraper.
Why this Actor
- Many players in one run, typed the way you write them.
Jannik Sinner,JANNIK SINNER,novak djokovič, the Tennis Abstract idJannikSinneror the player page URL all work. The name is looked up in Tennis Abstract's own list of 79,580 players, so a name that fits several men is never guessed:Sinnerreturns a free row listing Jannik Sinner and Martin Sinner. - The right player, checked. Tennis Abstract answers an unknown id with another player's page (on 21 Sep 2026
?p=IgaSwiatekreturned Benoit Paire's page). Every page's own name is compared with the player you asked for before any row is returned. - Filters: tournament start date range, surfaces, tournament levels (Grand Slam, Masters 1000, Tour Finals, Olympics, Davis Cup, other tour-level, Challenger, ITF Futures, qualifying, juniors), opponents and won/lost.
- Head-to-head.
Jannik Sinner vs Carlos Alcarazreturns one summary row (meetings, wins for each player, wins by surface, first and last meeting) and every meeting as a match row. Like Tennis Abstract's own list, meetings and wins include walkovers; thewalkoverscolumn says how many there were (the official ATP head-to-head leaves them out). - Only new matches. Turn on
monitoringModeand schedule the Actor: each run returns only the matches Tennis Abstract added since the player was last checked with the same filters. - Honest failures. Unknown names, ambiguous names, unreadable pages, "no new match" and a run that hits your spending limit each come back as a free row that says what happened. An unreadable page is never reported as "0 matches".
Input
| Field | Type | Default | What it does |
|---|---|---|---|
players | list | (prefilled: Jannik Sinner, Carlos Alcaraz) | ATP players: full name, Tennis Abstract id or player page URL. |
headToHead | list | (prefilled: Jannik Sinner vs Carlos Alcaraz) | Player A vs Player B, one per line. |
dateFrom, dateTo | text | (none) | YYYY-MM-DD. Compared with the tournament start date. |
surfaces | select | all | hard, clay, grass, carpet. |
levels | select | all | grand-slam, masters, tour-finals, olympics, davis-cup, other-tour-level, challenger, futures, qualifying, juniors. |
opponents | list | (none) | Only matches against these opponents (all words of a line must be in the opponent's name). |
results | select | both | won and/or lost. |
maxMatchesPerPlayer | integer | 100 (prefilled: 20) | Newest matching matches per player. 0 = whole career. |
monitoringMode | boolean | false | Return only matches added since the last check (see below). |
resetMonitoringState | boolean | false | Forget what was remembered for this set of filters. |
With no players and no head-to-head, the run returns a 10-match sample of Jannik Sinner.
Examples:
{ "players": ["Jannik Sinner", "Carlos Alcaraz", "Alexander Zverev"], "dateFrom": "2025-01-01", "surfaces": ["clay"], "maxMatchesPerPlayer": 0 }
{ "players": ["Novak Djokovic"], "levels": ["grand-slam"], "opponents": ["Rafael Nadal", "Roger Federer"], "maxMatchesPerPlayer": 0 }
{ "headToHead": ["Jannik Sinner vs Carlos Alcaraz", "Novak Djokovic vs Rafael Nadal"], "surfaces": ["hard"] }
{ "players": ["Jannik Sinner", "Carlos Alcaraz"], "monitoringMode": true, "maxMatchesPerPlayer": 5 }
Output
Match row (Jannik Sinner, Wimbledon 2026 final):
{"source": "tennisabstract.com","status": "ok","rowType": "match","player": "Jannik Sinner","playerId": "JannikSinner","requestedAs": "Jannik Sinner","tournamentStartDate": "2026-06-29","tournament": "Wimbledon","surface": "Grass","level": "G","levelName": "Grand Slam","round": "F","result": "W","score": "6-7(7) 7-6(2) 6-3 6-4","outcome": "completed","bestOf": 5,"playerRank": 1,"playerSeed": 1,"playerEntry": null,"opponent": "Alexander Zverev","opponentId": "AlexanderZverev","opponentRank": 3,"opponentSeed": 2,"opponentEntry": null,"opponentHand": "right","opponentBackhand": "two-handed","opponentBirthDate": "1997-04-20","opponentHeightCm": 198,"opponentCountry": "GER","minutes": 226,"statsAvailable": true,"aces": 15,"doubleFaults": 2,"servePoints": 136,"firstServesIn": 87,"firstServePointsWon": 70,"secondServePointsWon": 32,"serviceGames": 22,"breakPointsSaved": 1,"breakPointsFaced": 1,"opponentAces": 17,"opponentDoubleFaults": 2,"opponentServePoints": 139,"opponentFirstServesIn": 105,"opponentFirstServePointsWon": 76,"opponentSecondServePointsWon": 20,"opponentServiceGames": 21,"opponentBreakPointsSaved": 3,"opponentBreakPointsFaced": 5,"matchId": "2026-540-603","matchChartingUrl": "https://www.tennisabstract.com/charting/20260712-M-Wimbledon-F-Jannik_Sinner-Alexander_Zverev.html","playerUrl": "https://www.tennisabstract.com/cgi-bin/player-classic.cgi?p=JannikSinner"}
Head-to-head row (21 Sep 2026):
{"source": "tennisabstract.com","status": "ok","rowType": "head-to-head","headToHead": "Jannik Sinner vs Carlos Alcaraz","player1": "Jannik Sinner","player1Id": "JannikSinner","player2": "Carlos Alcaraz","player2Id": "CarlosAlcaraz","meetings": 18,"player1Wins": 7,"player2Wins": 11,"walkovers": 0,"bySurface": { "hard": { "player1Wins": 3, "player2Wins": 7 }, "clay": { "player1Wins": 2, "player2Wins": 4 }, "grass": { "player1Wins": 2, "player2Wins": 0 } },"lastMeetingTournamentStartDate": "2026-04-05","lastMeetingTournament": "Monte Carlo Masters","lastMeetingRound": "F","lastMeetingWinner": "Jannik Sinner","lastMeetingScore": "7-6(5) 6-3","firstMeetingTournamentStartDate": "2019-04-01","playerUrl": "https://www.tennisabstract.com/cgi-bin/player-classic.cgi?p=JannikSinner"}
Notes on the columns:
tournamentStartDateis the start date of the tournament (Tennis Abstract dates every round of Wimbledon 2026 as 2026-06-29), not the day of the match.scoreis written from the winner's side, as Tennis Abstract shows it by default, also whenresultisL.outcomeiscompleted,retired,walkover,defaultedorabandoned, read from the score.- The serve columns mean exactly what Tennis Abstract's own table computes from them: ace % =
aces / servePoints, 1st in % =firstServesIn / servePoints, 1st won % =firstServePointsWon / firstServesIn, 2nd won % =secondServePointsWon / (servePoints - firstServesIn), break points saved =breakPointsSaved / breakPointsFaced. On 21 Sep 2026 Novak Djokovic's page was compared in a browser with Tennis Abstract's own tables: the first 8 matches on the serve table and the first 5 on the return and raw-count tables matched on every value.serviceGamesis not shown in those tables; in 3,527 of 3,588 matches checked (four players), both players' service games add up to the games in the score minus tiebreaks. - Matches without serve statistics (mostly older and lower-level matches, and walkovers) have
statsAvailable: falseandnullin the serve columns. Nothing is filled with 0. levelis Tennis Abstract's code;levelNamespells it out.A("Other tour-level") is Tennis Abstract's catch-all for ATP 250/500 events, United Cup and Laver Cup, and in some years it also holds the Tour Finals or the Olympics.playerEntry/opponentEntry:Qqualifier,WCwild card,LLlucky loser,PRprotected ranking and other codes as Tennis Abstract writes them.matchIdis Tennis Abstract's match id (tournament id and match number) and is how monitoring recognises a match it has already returned.- Matches that are listed without a score (not yet played) are not returned.
Not included: women's players (WTA). Tennis Abstract keeps women's match histories in files under /jsmatches/, which its robots.txt asks crawlers not to read, and this Actor respects that (together with /jsfrags/ and /jsplayers/). The men's pages checked on 21 Sep 2026 (18 players, including Djokovic, Federer, Nadal, Connors, Agassi and Lendl) carried the whole career; if a page ever flags that part of a career is kept in such a file, a free partial-career row says how many matches were read. Doubles are not included.
Monitoring (only new matches)
With monitoringMode on, the Actor remembers, per player and per set of filters, the matches it has seen:
- First check of a player: returns the newest
maxMatchesPerPlayermatching matches and records all the others as known (a free row says how many). SetmaxMatchesPerPlayerto0to get the whole career on the first check. - Later checks: returns every matching match Tennis Abstract has added since, without the
maxMatchesPerPlayerlimit. If there is none, a freeno-new-matchesrow says so. - Matches that were not returned because the run reached your maximum charge are not remembered, so they come back as new on the next check.
- Head-to-head lines are not watched; put the first player in
playersand the second inopponentsinstead. - Do not put the same players and filters in two schedules that run at the same time. The memory is saved by reading it again just before writing and merging, but Apify's key-value store has no atomic update, so overlapping runs can still overwrite each other.
Free note rows
These rows have rowType: "note", a status and a plain-English note, and are never charged:
status | Meaning |
|---|---|
ambiguous-player | The name fits several men; the row lists them. Use the full name. |
player-not-found | The name is not on Tennis Abstract's player list, or Tennis Abstract has no page for it. |
wta-not-available | The name is on Tennis Abstract's women's list (see "Not included"). |
name-mismatch | Tennis Abstract answered with a different player's page; nothing from it is returned. |
duplicate | The same player, or the same two players (in either order), was asked for twice; returned once. |
no-matching-matches | The player's page was read, and no match fits your filters (or the two players never met). |
no-new-matches | Monitoring: nothing new since the last check. |
not-returned | Older matching matches left out by maxMatchesPerPlayer (the row gives the number). |
partial-career | Part of the player's career is in a file robots.txt excludes; only the page was read. |
unreadable | A page could not be read after waiting and retrying. Nothing from it is guessed. |
budget-reached | The run reached the maximum total charge you set; players after it were not read. |
invalid-input / invalid-head-to-head | The input or a head-to-head line could not be used. |
Speed and politeness
Tennis Abstract is an independent site, so this Actor reads one page at a time, waits at least 3 seconds between pages, follows the site's Retry-After when it answers HTTP 429 (too many requests) and then waits one second longer between pages (up to 8 seconds). It identifies itself with its own User-Agent and connects through Apify Proxy (datacenter), because plain cloud addresses get a browser check page; the check page itself is never solved or bypassed, and if it keeps coming back the player gets a free unreadable row. Each run reads the player list once and one page per player. Measured on 21 Sep 2026: one whole career (Novak Djokovic, 1,516 rows) took 11 seconds, two players with 20 matches each plus a head-to-head took 15 seconds, and 12 whole careers (11,290 rows) took 4 minutes. The default run timeout is 1 hour; if a run hits it, the rows returned so far are kept and only they are charged.
Pricing
Pay per result: each match row and each head-to-head row is one result. Note rows are free. In monitoring mode each player checked also costs a small check fee, whether or not he has new matches. If a run reaches the maximum total charge you set, it stops before reading the next player and says so in a free row.
FAQ
Which players are covered? Men (ATP), from the pages Tennis Abstract keeps for them, including Challenger, ITF Futures, qualifying and Davis Cup matches where Tennis Abstract has them.
How fresh is the data? As fresh as Tennis Abstract's pages, which are updated as tournaments progress. Use monitoring on a schedule to get new matches as they appear.
Why does a name return a list instead of matches? Because several men share it on Tennis Abstract. Use the full name or the Tennis Abstract id.
Is this official? No. It reads Tennis Abstract's public pages; it is not affiliated with or endorsed by Tennis Abstract or the ATP.