Tennis Abstract Scraper | Match History API
Pricing
from $2.85 / 1,000 results
Tennis Abstract Scraper | Match History API
Export Tennis Abstract ATP and WTA player match logs with scores, opponents, surfaces and serve/return counts. Filter by season, tournament or head-to-head opponent.
Pricing
from $2.85 / 1,000 results
Rating
0.0
(0)
Developer
Eli J
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
8 days ago
Last modified
Categories
Share
What you can export
Use this Tennis Abstract scraper and match-history API for ATP and WTA player research. Supply player slugs, player-page URLs or supported discovery tokens to export dated matches, tournaments, surfaces, scores, opponents and available serve/return counts.
Filter the fetched player logs by season, surface, tournament or head-to-head opponent. Players mode exports singles match logs. Set mode to tournaments for the main draw, qualifying and side tables of a tournament page. Doubles are not exported.
How to export Tennis Abstract match data
- Put player slugs, pasted player-page URLs or discovery tokens in
players. - Optionally set the filters (
span,surface,opponent, and the others in the Input table). - Set
maxMatchesPerPlayer,maxPlayersandincludePlayerRecord. - Start the run and read the rows in the default dataset, exported as JSON, CSV or Excel.
- Check the
ERRORSrecord and the item count.
Two public tasks start from a saved input:
- Export Novak Djokovic recent matches and serve/return stats runs the first saved input shown under Input.
- Export Djokovic vs Nadal head-to-head match history runs the second saved input shown under Input.
Export a tournament
Use the site's tournament links for t= values. The Actor never builds a
t= value from a tournament name; a value without its /Name part is
refused, never guessed. This input exports the measured 2024 Wimbledon
main draw:
{"mode":"tournaments","tournaments":["2024-540/Wimbledon"],"maxRowsPerTarget":127}
Players-mode filters and caps are ignored in tournaments mode.
Pricing
Two charges, both automatic:
- Result — one match row, player record, tournament match or tournament table row in the dataset, charged at the rate in the current Pricing tab, where unit prices depend on your subscription plan.
- Actor Start — a per-run start fee, charged once per run. The Actor's memory ceiling is 1024 MB, and Apify charges this event once up to and including 1 GB of RAM and once more per extra GB, so the ceiling keeps it to exactly one start event per run.
The run's compute and its result writes are covered by those event prices, so platform usage is not billed on top. Pages that fail are never written to the dataset and so are never billed as rows; a run that matches nothing is charged no result rows at all. Storage retention and later reads of the dataset are billed according to your plan.
Player caps are in the Input table. After discovery, @elo-top:551 at
defaults fetches 50 players, not 551. With the match cap off, up to 200
players means 200 source-dependent full careers.
At the free-tier rate of $0.005 per result, the measured 127-row main draw costs $0.635 and the 467-row whole tournament $2.335, plus the start event. Repeated tournament entries are fetched and billed once.
Your Apify spending limit stops further fetching. The charge-limit entry
in the error table below describes how unfetched players or tournaments are
reported.
Input
Nothing is required: with no players in players mode, or no tournaments
in tournaments mode, the run makes no source request, writes no result rows
and incurs no result-row charge. The automatic start event is charged once
per run, as Pricing says.
Saved input of the public task Export Novak Djokovic recent matches and serve/return stats:
{"players":["NovakDjokovic"],"tour":"auto","maxMatchesPerPlayer":100,"maxPlayers":50,"includePlayerRecord":false}
Saved input of the public task Export Djokovic vs Nadal head-to-head match history:
{"players":["NovakDjokovic"],"tour":"atp","span":"Career","opponent":"RafaelNadal","maxMatchesPerPlayer":100,"maxPlayers":1,"includePlayerRecord":false}
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | players | players for career logs; tournaments for each page's result tables. Any other value is a run-level unsupported-target input error. |
tournaments | array | None | Tournaments mode only. The site's own t= values (2024-540/Wimbledon) or pasted tourney.cgi?t= URLs. Values retain + exactly; missing /Name, WTA tournament URLs and malformed URLs are per-entry unsupported-target errors, never guessed or fetched. Later valid entries still run. |
maxRowsPerTarget | integer | 500 | Tournaments mode only. Rows per page in page order; 0 means all. Negative or non-whole values are input errors. |
players | array | None; no player is read until you set it | Player slugs (NovakDjokovic), pasted player-page URLs in either p= form, or discovery tokens (@elo-top:10, @leaders:1-50, @wta-leaders:1-50). |
tour | string | auto | auto, atp or wta: which surface family to read a player from. auto tries ATP first, then WTA, and accepts the first page whose own published name matches the slug. |
span | string | All matches | Career, Last 52, or a four-digit season. Last 52 counts back from the run clock for an active player and from the player's newest match otherwise, which is the page's own rule. It matches on the row's date, the tournament's start date, so a season is a tournament-start season. |
surface | string | All surfaces | Hard, Clay, Grass or Carpet. |
level | string | All levels | The site's own raw level code, matched exactly. Suggestions are open, not enforced. |
round | string | All rounds | The site's own raw round code, matched exactly. Suggestions are open, not enforced. |
tourney | string | All tournaments | Case-insensitive substring of the tournament name, so US Open keeps both US Open and ATP US Open. |
result | string | Wins and losses | W or L, the row's raw result value. |
opponent | string | All opponents | Head-to-head filter over rows already fetched, at no extra request, written either as a slug or as a display name. |
maxMatchesPerPlayer | integer | 100 | Matches kept per player, newest first, after the filters. 0 means every match. |
maxPlayers | integer | 50 | Players fetched per run, applied after the entries are resolved. Maximum 200; a higher value is clamped to 200 with a warning in the log rather than refused. |
includePlayerRecord | boolean | false | Add one player record row per successfully read player. |
proxyConfiguration | object | No proxy | Optional Apify Proxy settings. No group is set by default; the datacenter group (BUYPROXIES94952) is the first thing to try if a run is ever blocked. |
The platform prefills players with ["NovakDjokovic"] and tournaments
with ["2024-540/Wimbledon"]. The default mode is players, so the
tournaments prefill is ignored until you select that mode. A prefill does not
supply targets to API calls that omit them.
The source page offers a wider filter vocabulary than the filters above: minimum-matches and minimum-years thresholds, sets, scores, entry and ranking bands, hand, age, height, country. They are aggregates or menu state rather than per-row predicates, so they are not shipped; ask for one on the Issues tab and it can be added as a row filter.
Output
Row types and fields follow; actual values depend on the source.
Match rows
One flat row per match, newest first (recordType: "match"). Every count is the raw number
the source stores, never a rate this Actor computed; every absent value a row
carries is null, never 0. An ATP row is 51 columns wide and a WTA row 48:
opponentPlayerId, roundNumber and matchNumber are absent from a WTA row
rather than null, so read them with a lookup that tolerates a missing key. A
WTA-only run still sees those three as empty columns in the default dataset
view, because the view names them; that is the source, not a fault.
| Column | Type | Unit / form |
|---|---|---|
recordType | string | Literal match on a match row |
tour | string | ATP or WTA |
player | string | null | Display name as the source page publishes it |
playerSlug | string | The slug this run fetched |
date | string | Tournament start date, YYYY-MM-DD — the source dates every row of an event alike, so the per-match date is in chartSlug where the match is charted |
tournament | string | Tournament name as printed; current-season rows carry an ATP prefix |
surface | string | Source surface label, such as Hard, Clay, Grass or Carpet |
level | string | The site's own raw level code, not glossed |
round | string | The site's own raw round code, not glossed |
result | string | W or L, from the player's side |
score | string | Score as printed, e.g. 7-6(5) 5-7 4-6 6-2 6-1 |
bestOfSets | integer | null | Sets (3 or 5) |
minutes | integer | null | Match duration in minutes |
playerRank / playerSeed / playerEntry | integer | integer | string, nullable | The player's ranking, seed and entry code at the match, as the site writes them |
opponent | string | Opponent display name |
opponentPlayerId | string | null | The site's own numeric player id for the opponent (ATP rows only) |
opponentRank / opponentSeed / opponentEntry | integer | integer | string, nullable | The same three for the opponent |
opponentHand | string | null | R or L |
opponentBackhand | string | null | The site's own backhand code, not glossed by the page |
opponentBirthDate | string | null | Date, YYYY-MM-DD |
opponentHeightCm | integer | null | Centimetres |
opponentCountry | string | null | Three-letter country code (SRB, ITA, ARG) |
aces, doubleFaults, servePoints, firstServesIn, firstServePointsWon, secondServePointsWon, serviceGames, breakPointsSaved, breakPointsFaced | integer | null | The player's own serve counts; the whole block is null together when the source recorded none |
opponentAces, opponentDoubleFaults, opponentServePoints, opponentFirstServesIn, opponentFirstServePointsWon, opponentSecondServePointsWon, opponentServiceGames, opponentBreakPointsSaved, opponentBreakPointsFaced | integer | null | The same nine counts from the opponent's serve — i.e. the player's return side |
matchId | string | The site's own id, <year>-<eventId>-<matchNum> |
roundNumber / matchNumber | integer | null | The site's round and match indexes (ATP rows only) |
chartSlug | string | null | Point-by-point charting slug, where the match is charted |
chartUrl | string | null | The charting page URL built from that slug |
sourceUrl | string | ATP career-page URL or WTA wplayer-classic.cgi link; WTA values come from the two history files in Data source |
scrapedAt | string | ISO-8601 UTC, to the second |
Player rows
Turn on includePlayerRecord and each successfully read player also gets one
bio row (recordType: "player"). The source publishes the nine bio fields on
the ATP career page and on the WTA shortlist file only, so a WTA target whose
history came from the career file alone still gets its row, with those nine
fields null and an identity-unchecked note on the target — nothing is
filled in from another page:
| Column | Type | Unit / form |
|---|---|---|
recordType | string | Literal player on a bio row |
tour | string | ATP or WTA |
player | string | null | Display name |
playerSlug | string | The slug this run fetched |
currentRank | integer | null | Current ranking the source publishes |
peakRank | integer | null | Career-high ranking the source publishes |
birthDate | string | null | Date, YYYY-MM-DD |
hand | string | null | R or L |
backhand | string | null | The site's own backhand code, not glossed |
country | string | null | Three-letter country code |
active | integer | null | 1 active, 0 retired, as the source writes it |
twitter | string | null | Handle, without the @ |
matchCount | integer | null | The rows the source served for that player, before your filters and cap: the published career total when every history surface answered (the ATP career page, or both WTA history files); when only one WTA file answers, the total of the one that did. Not a count of the rows this run delivered |
sourceUrl | string | ATP classic page or supplying WTA shortlist file (jsmatches/{slug}.js); career-only null-bio rows keep the classic-page link |
scrapedAt | string | ISO-8601 UTC, to the second |
Tournament rows
One tournament page yields every data row of its eight result tables, in the order the page prints them. Measured on the 2024 Wimbledon page, 467 rows:
Table (table) | Record type | Rows | What it is |
|---|---|---|---|
singles-results | tournamentMatch | 127 | Main draw, final first |
seed-outcomes | tournamentTable | 16 | Each seed's result, two seeds per row |
event-finals | tournamentTable | 5 | This event's finals, 2022–2026 editions |
surface-speed | tournamentTable | 5 | The site's surface-speed figures for those editions |
stat-summaries | tournamentTable | 129 | Per-player stat summaries; the last row is the page's own "All Main Draw Players" row |
winners-errors | tournamentTable | 31 | Winners and unforced errors |
qual-results | tournamentMatch | 112 | Qualifying draw |
week-finals | tournamentTable | 42 | Finals of other events the same week |
Absent side tables are skipped. Actual HTML id selects each table;
comments contribute no table, row or cell text. Match-table rates remain
printed strings, never converted, rounded or computed; only break-point
counts are supplied behind them. stat-summaries retains its published
Pts, A, DF, BPFaced and BPEarned counts as text in values.
Main-draw and qualifying rows (recordType: "tournamentMatch"); a missing value
is null:
| Column | Type | Unit / form |
|---|---|---|
recordType | string | Literal tournamentMatch |
tournamentSlug | string | The t= value this run fetched, e.g. 2024-540/Wimbledon |
sourceUrl | string | The exact URL the row was read from |
scrapedAt | string | ISO-8601 UTC, to the second |
table | string | singles-results (main draw) or qual-results (qualifying) |
rowNumber | integer | 1-based position among that table's rows, in page order |
round | string | As printed: F, SF, QF, R16, R32, R64, R128 in the main draw, Q1, Q2, Q3 in qualifying |
winner / loser | string | Display name as the page links it |
winnerPlayerId / loserPlayerId | string | The site's own numeric player id, from the player link |
winnerSeed / loserSeed | integer | null | Seed |
winnerEntry / loserEntry | string | null | Entry code as printed (Q, WC, LL, A on the measured page) |
winnerCountry / loserCountry | string | Three-letter country code as printed |
winnerRank / loserRank | integer | null | Ranking the page prints for the player |
score | string | As printed, RET and W/O included |
dominanceRatio | string | null | The page's printed dominance ratio, glossed by the page as "% of return points won divided by % of serve points lost", e.g. "1.41" |
winnerAcePct / loserAcePct | string | null | Printed ace rate, e.g. "5.3%" |
winnerFirstInPct / loserFirstInPct | string | null | Printed first-serve percentage |
winnerFirstWonPct / loserFirstWonPct | string | null | Printed first-serve points won |
winnerSecondWonPct / loserSecondWonPct | string | null | Printed second-serve points won |
winnerBreakPointsSaved / loserBreakPointsSaved | integer | null | Break points saved (count), from the printed saved/faced |
winnerBreakPointsFaced / loserBreakPointsFaced | integer | null | Break points faced (count), from the same cell |
time | string | null | Match duration as printed, h:mm |
The page's walkovers print every statistic empty, so those columns are null
on a walkover row.
The six side tables use recordType: "tournamentTable". Header/value arrays
preserve repeated and unnamed headers:
| Column | Type | Unit / form |
|---|---|---|
recordType | string | Literal tournamentTable |
tournamentSlug, sourceUrl, scrapedAt, rowNumber | As above | Same run context and 1-based position as tournament match rows |
table | string | The side table's id from the list above |
columns | array of strings | The table's own header texts, in order; an unnamed header is "" |
values | array of string | null | The row's cell texts, same order and length as columns; null for an empty cell |
When something goes wrong
Nothing that failed is ever written to your dataset. Instead the run's ERRORS
key-value record collects one entry per affected target, each with four keys:
target, slug, errorClass and message.
Ten errorClass values:
errorClass | What it means |
|---|---|
no-history | The surface exists for that player and publishes no matches, or a tournament page's result tables hold no row. |
source-error | Source script failure. A current-season tournament CGI has served an HTTP 200 Python traceback while older editions read: one first-call error, zero rows, no retry. |
unreadable-page | The site answered with another surface, a tournament year that contradicts the requested year, an unreadable title year or tables outside the measured shape. No row is taken. |
blocked | The site refused the request, or a tournament response has no actual Tennis Abstract title. A commented-only title counts as absent. |
transport | The request never completed after the built-in retry ladder. |
unresolved-name | A display name from a leaders list did not resolve to a player page. |
unsupported-target | An entry, or an input value, the Actor cannot act on. Run-level entries carry slug null. |
wrong-player | The page answered with a different player than the slug asked for, and no row was taken from it. |
charge-limit | Run-level: spending limit stopped fetching. Names the number and list of unfetched targets in ERRORS and the log warning; target and slug are null. Summary counts attempted targets only. |
identity-unchecked | Run-level note, not a failure: a note beside delivered rows. The surface they came from publishes no player name, so they could not be checked against the slug you asked for. |
Unknown mode/tour, fractional caps and unparseable spans end as
SUCCEEDED with an empty dataset, one unsupported-target ERRORS entry
and no run status message. Check ERRORS and item count to detect bad input.
Data source and robots.txt
Every row comes from www.tennisabstract.com, a public statistics site run by
one person. Requests are ordinary HTTPS GETs made the way a browser renders
the page. Successive surface reads are paced by 1.5 seconds; transport warm-ups
and retries follow their own policy. maxPlayers is capped at 200 for the
same reason. No login, no paywall, no API key, and
nothing but professional match statistics — no personal data about private
individuals.
Tennis Abstract publishes no terms page reachable from its home page. This
Actor reads only surfaces its robots.txt allows where one exists for the
data, and demotes and discloses the ones it disallows. The site's robots file
disallows /jsfrags/, /jsmatches/ and /jsplayers/ under User-agent: *.
A WTA career is assembled from the shortlist file plus the older-history file,
the way the site's own page assembles it. Per family:
| Surface | Path | robots | Used for |
|---|---|---|---|
| Tournament page | cgi-bin/tourney.cgi | allowed | Every tournaments-mode row |
| ATP career page | cgi-bin/player-classic.cgi | allowed | Every ATP match row and the ATP bio |
| WTA header page | cgi-bin/wplayer-classic.cgi | allowed | The WTA column names |
| Elo report | /reports/ | allowed | The @elo-top:N discovery token |
| WTA recent history | jsmatches/{slug}.js | disallowed — fetched, and disclosed here | WTA rows and the WTA bio; the site serves WTA history from nowhere else |
| WTA older history | jsmatches/{slug}Career.js | disallowed — fetched, and disclosed here | The rest of a WTA career |
| Leader lists | jsmatches/leadersource*.js | disallowed — fetched, and disclosed here | The @leaders: and @wta-leaders: tokens (names only) |
Nothing under /jsplayers/ is fetched, and /jsfrags/ is never fetched at
all: it is the page's recent-results fragment, and everything in it is
already on the allowed career page.
Limits and things worth knowing
- Raw player counts. The page's displayed percentages are computed at
render time, and this Actor does not invent them; derive them from the
columns: first-serve-in percentage is
firstServesIn / servePoints, first-serve win percentagefirstServePointsWon / firstServesIn, second-serve win percentagesecondServePointsWon / (servePoints - firstServesIn), break points savedbreakPointsSaved / breakPointsFaced, ace rateaces / servePoints, and the return-side equivalents from the matchingopponent*columns. - Missing statistics: the nullable count blocks and
minutesin the match table reflect missing source data, not zero counts. - The opponent id column is unnamed by the source. The site's own header array does not name the last ATP column. It was identified as the opponent's site player id by this parser's deployed mapping.
- Unglossed codes:
A,F,Olevels andBR,Q1,Q2,Q3rounds occur without a source label, so they keep their raw codes. - Tournament identity checks the published year. The requested year must match the actual title and visible event heading when present. This does not verify the event name or id. Match-table header drift, row-width drift, or unreadable player, rank or break-point cells refuse the whole page before any row is pushed.
- One tournament page was measured: the counts above describe the measured 2024 Wimbledon capture.
- Tournament exports omit the event header and the "Extreme Performances"
prose block. WTA tournament pages (
wtourney.cgi), player charting tables, rankings and Elo report exports are not shipped.
Related Actors
| Actor | Research fit | What it exports |
|---|---|---|
| Kalshi Weather Markets | Price History API | Weather-market price history | Hourly or daily candles for named Kalshi weather markets: prices, bids, asks, volume and open interest as exact decimal strings |
| UFCStats Scraper | Fight & Round Statistics | UFC fight statistics | UFCStats fight and round statistics as flat rows with fighter IDs, event dates and locations |
| Google Trends Scraper | Keyword Data API | Search-interest research | Google Trends interest over time, interest by region and related or rising queries, plus the Trending Now board by country |
| Clutch Agency Scraper | B2B Directory Export | Agency research and lead generation | B2B agency listings from Clutch.co directories: company names, websites, locations, headcounts and service mix |
| Google Ads Transparency Scraper & Archive | Ad-creative research | Advertiser ad records from Google's Ads Transparency Center: creative format, available images, first and last shown dates and source links |
Support
Found a bug, need a column, or want one of the source's other filters exposed? Open an issue on the Actor's Issues tab and I'll take a look.