Tennis Abstract Match History Scraper - Stats, H2H, New Matches avatar

Tennis Abstract Match History Scraper - Stats, H2H, New Matches

Pricing

from $12.00 / 1,000 match or head-to-head returneds

Go to Apify Store
Tennis Abstract Match History Scraper - Stats, H2H, New Matches

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

NeverEmpty

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

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 id JannikSinner or 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: Sinner returns 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=IgaSwiatek returned 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 Alcaraz returns 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; the walkovers column says how many there were (the official ATP head-to-head leaves them out).
  • Only new matches. Turn on monitoringMode and 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

FieldTypeDefaultWhat it does
playerslist(prefilled: Jannik Sinner, Carlos Alcaraz)ATP players: full name, Tennis Abstract id or player page URL.
headToHeadlist(prefilled: Jannik Sinner vs Carlos Alcaraz)Player A vs Player B, one per line.
dateFrom, dateTotext(none)YYYY-MM-DD. Compared with the tournament start date.
surfacesselectallhard, clay, grass, carpet.
levelsselectallgrand-slam, masters, tour-finals, olympics, davis-cup, other-tour-level, challenger, futures, qualifying, juniors.
opponentslist(none)Only matches against these opponents (all words of a line must be in the opponent's name).
resultsselectbothwon and/or lost.
maxMatchesPerPlayerinteger100 (prefilled: 20)Newest matching matches per player. 0 = whole career.
monitoringModebooleanfalseReturn only matches added since the last check (see below).
resetMonitoringStatebooleanfalseForget 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:

  • tournamentStartDate is the start date of the tournament (Tennis Abstract dates every round of Wimbledon 2026 as 2026-06-29), not the day of the match.
  • score is written from the winner's side, as Tennis Abstract shows it by default, also when result is L. outcome is completed, retired, walkover, defaulted or abandoned, 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. serviceGames is 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: false and null in the serve columns. Nothing is filled with 0.
  • level is Tennis Abstract's code; levelName spells 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: Q qualifier, WC wild card, LL lucky loser, PR protected ranking and other codes as Tennis Abstract writes them.
  • matchId is 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 maxMatchesPerPlayer matching matches and records all the others as known (a free row says how many). Set maxMatchesPerPlayer to 0 to get the whole career on the first check.
  • Later checks: returns every matching match Tennis Abstract has added since, without the maxMatchesPerPlayer limit. If there is none, a free no-new-matches row 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 players and the second in opponents instead.
  • 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:

statusMeaning
ambiguous-playerThe name fits several men; the row lists them. Use the full name.
player-not-foundThe name is not on Tennis Abstract's player list, or Tennis Abstract has no page for it.
wta-not-availableThe name is on Tennis Abstract's women's list (see "Not included").
name-mismatchTennis Abstract answered with a different player's page; nothing from it is returned.
duplicateThe same player, or the same two players (in either order), was asked for twice; returned once.
no-matching-matchesThe player's page was read, and no match fits your filters (or the two players never met).
no-new-matchesMonitoring: nothing new since the last check.
not-returnedOlder matching matches left out by maxMatchesPerPlayer (the row gives the number).
partial-careerPart of the player's career is in a file robots.txt excludes; only the page was read.
unreadableA page could not be read after waiting and retrying. Nothing from it is guessed.
budget-reachedThe run reached the maximum total charge you set; players after it were not read.
invalid-input / invalid-head-to-headThe 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.