Tennis Abstract Scraper | Match History API avatar

Tennis Abstract Scraper | Match History API

Pricing

from $2.85 / 1,000 results

Go to Apify Store
Tennis Abstract Scraper | Match History API

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

Eli J

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

8 days ago

Last modified

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

  1. Put player slugs, pasted player-page URLs or discovery tokens in players.
  2. Optionally set the filters (span, surface, opponent, and the others in the Input table).
  3. Set maxMatchesPerPlayer, maxPlayers and includePlayerRecord.
  4. Start the run and read the rows in the default dataset, exported as JSON, CSV or Excel.
  5. Check the ERRORS record and the item count.

Two public tasks start from a saved 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}
FieldTypeDefaultDescription
modestringplayersplayers for career logs; tournaments for each page's result tables. Any other value is a run-level unsupported-target input error.
tournamentsarrayNoneTournaments 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.
maxRowsPerTargetinteger500Tournaments mode only. Rows per page in page order; 0 means all. Negative or non-whole values are input errors.
playersarrayNone; no player is read until you set itPlayer slugs (NovakDjokovic), pasted player-page URLs in either p= form, or discovery tokens (@elo-top:10, @leaders:1-50, @wta-leaders:1-50).
tourstringautoauto, 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.
spanstringAll matchesCareer, 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.
surfacestringAll surfacesHard, Clay, Grass or Carpet.
levelstringAll levelsThe site's own raw level code, matched exactly. Suggestions are open, not enforced.
roundstringAll roundsThe site's own raw round code, matched exactly. Suggestions are open, not enforced.
tourneystringAll tournamentsCase-insensitive substring of the tournament name, so US Open keeps both US Open and ATP US Open.
resultstringWins and lossesW or L, the row's raw result value.
opponentstringAll opponentsHead-to-head filter over rows already fetched, at no extra request, written either as a slug or as a display name.
maxMatchesPerPlayerinteger100Matches kept per player, newest first, after the filters. 0 means every match.
maxPlayersinteger50Players 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.
includePlayerRecordbooleanfalseAdd one player record row per successfully read player.
proxyConfigurationobjectNo proxyOptional 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.

ColumnTypeUnit / form
recordTypestringLiteral match on a match row
tourstringATP or WTA
playerstring | nullDisplay name as the source page publishes it
playerSlugstringThe slug this run fetched
datestringTournament 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
tournamentstringTournament name as printed; current-season rows carry an ATP prefix
surfacestringSource surface label, such as Hard, Clay, Grass or Carpet
levelstringThe site's own raw level code, not glossed
roundstringThe site's own raw round code, not glossed
resultstringW or L, from the player's side
scorestringScore as printed, e.g. 7-6(5) 5-7 4-6 6-2 6-1
bestOfSetsinteger | nullSets (3 or 5)
minutesinteger | nullMatch duration in minutes
playerRank / playerSeed / playerEntryinteger | integer | string, nullableThe player's ranking, seed and entry code at the match, as the site writes them
opponentstringOpponent display name
opponentPlayerIdstring | nullThe site's own numeric player id for the opponent (ATP rows only)
opponentRank / opponentSeed / opponentEntryinteger | integer | string, nullableThe same three for the opponent
opponentHandstring | nullR or L
opponentBackhandstring | nullThe site's own backhand code, not glossed by the page
opponentBirthDatestring | nullDate, YYYY-MM-DD
opponentHeightCminteger | nullCentimetres
opponentCountrystring | nullThree-letter country code (SRB, ITA, ARG)
aces, doubleFaults, servePoints, firstServesIn, firstServePointsWon, secondServePointsWon, serviceGames, breakPointsSaved, breakPointsFacedinteger | nullThe player's own serve counts; the whole block is null together when the source recorded none
opponentAces, opponentDoubleFaults, opponentServePoints, opponentFirstServesIn, opponentFirstServePointsWon, opponentSecondServePointsWon, opponentServiceGames, opponentBreakPointsSaved, opponentBreakPointsFacedinteger | nullThe same nine counts from the opponent's serve — i.e. the player's return side
matchIdstringThe site's own id, <year>-<eventId>-<matchNum>
roundNumber / matchNumberinteger | nullThe site's round and match indexes (ATP rows only)
chartSlugstring | nullPoint-by-point charting slug, where the match is charted
chartUrlstring | nullThe charting page URL built from that slug
sourceUrlstringATP career-page URL or WTA wplayer-classic.cgi link; WTA values come from the two history files in Data source
scrapedAtstringISO-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:

ColumnTypeUnit / form
recordTypestringLiteral player on a bio row
tourstringATP or WTA
playerstring | nullDisplay name
playerSlugstringThe slug this run fetched
currentRankinteger | nullCurrent ranking the source publishes
peakRankinteger | nullCareer-high ranking the source publishes
birthDatestring | nullDate, YYYY-MM-DD
handstring | nullR or L
backhandstring | nullThe site's own backhand code, not glossed
countrystring | nullThree-letter country code
activeinteger | null1 active, 0 retired, as the source writes it
twitterstring | nullHandle, without the @
matchCountinteger | nullThe 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
sourceUrlstringATP classic page or supplying WTA shortlist file (jsmatches/{slug}.js); career-only null-bio rows keep the classic-page link
scrapedAtstringISO-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 typeRowsWhat it is
singles-resultstournamentMatch127Main draw, final first
seed-outcomestournamentTable16Each seed's result, two seeds per row
event-finalstournamentTable5This event's finals, 2022–2026 editions
surface-speedtournamentTable5The site's surface-speed figures for those editions
stat-summariestournamentTable129Per-player stat summaries; the last row is the page's own "All Main Draw Players" row
winners-errorstournamentTable31Winners and unforced errors
qual-resultstournamentMatch112Qualifying draw
week-finalstournamentTable42Finals 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:

ColumnTypeUnit / form
recordTypestringLiteral tournamentMatch
tournamentSlugstringThe t= value this run fetched, e.g. 2024-540/Wimbledon
sourceUrlstringThe exact URL the row was read from
scrapedAtstringISO-8601 UTC, to the second
tablestringsingles-results (main draw) or qual-results (qualifying)
rowNumberinteger1-based position among that table's rows, in page order
roundstringAs printed: F, SF, QF, R16, R32, R64, R128 in the main draw, Q1, Q2, Q3 in qualifying
winner / loserstringDisplay name as the page links it
winnerPlayerId / loserPlayerIdstringThe site's own numeric player id, from the player link
winnerSeed / loserSeedinteger | nullSeed
winnerEntry / loserEntrystring | nullEntry code as printed (Q, WC, LL, A on the measured page)
winnerCountry / loserCountrystringThree-letter country code as printed
winnerRank / loserRankinteger | nullRanking the page prints for the player
scorestringAs printed, RET and W/O included
dominanceRatiostring | nullThe 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 / loserAcePctstring | nullPrinted ace rate, e.g. "5.3%"
winnerFirstInPct / loserFirstInPctstring | nullPrinted first-serve percentage
winnerFirstWonPct / loserFirstWonPctstring | nullPrinted first-serve points won
winnerSecondWonPct / loserSecondWonPctstring | nullPrinted second-serve points won
winnerBreakPointsSaved / loserBreakPointsSavedinteger | nullBreak points saved (count), from the printed saved/faced
winnerBreakPointsFaced / loserBreakPointsFacedinteger | nullBreak points faced (count), from the same cell
timestring | nullMatch 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:

ColumnTypeUnit / form
recordTypestringLiteral tournamentTable
tournamentSlug, sourceUrl, scrapedAt, rowNumberAs aboveSame run context and 1-based position as tournament match rows
tablestringThe side table's id from the list above
columnsarray of stringsThe table's own header texts, in order; an unnamed header is ""
valuesarray of string | nullThe 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:

errorClassWhat it means
no-historyThe surface exists for that player and publishes no matches, or a tournament page's result tables hold no row.
source-errorSource 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-pageThe 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.
blockedThe site refused the request, or a tournament response has no actual Tennis Abstract title. A commented-only title counts as absent.
transportThe request never completed after the built-in retry ladder.
unresolved-nameA display name from a leaders list did not resolve to a player page.
unsupported-targetAn entry, or an input value, the Actor cannot act on. Run-level entries carry slug null.
wrong-playerThe page answered with a different player than the slug asked for, and no row was taken from it.
charge-limitRun-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-uncheckedRun-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:

SurfacePathrobotsUsed for
Tournament pagecgi-bin/tourney.cgiallowedEvery tournaments-mode row
ATP career pagecgi-bin/player-classic.cgiallowedEvery ATP match row and the ATP bio
WTA header pagecgi-bin/wplayer-classic.cgiallowedThe WTA column names
Elo report/reports/allowedThe @elo-top:N discovery token
WTA recent historyjsmatches/{slug}.jsdisallowed — fetched, and disclosed hereWTA rows and the WTA bio; the site serves WTA history from nowhere else
WTA older historyjsmatches/{slug}Career.jsdisallowed — fetched, and disclosed hereThe rest of a WTA career
Leader listsjsmatches/leadersource*.jsdisallowed — fetched, and disclosed hereThe @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 percentage firstServePointsWon / firstServesIn, second-serve win percentage secondServePointsWon / (servePoints - firstServesIn), break points saved breakPointsSaved / breakPointsFaced, ace rate aces / servePoints, and the return-side equivalents from the matching opponent* columns.
  • Missing statistics: the nullable count blocks and minutes in 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, O levels and BR, Q1, Q2, Q3 rounds 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.
ActorResearch fitWhat it exports
Kalshi Weather Markets | Price History APIWeather-market price historyHourly or daily candles for named Kalshi weather markets: prices, bids, asks, volume and open interest as exact decimal strings
UFCStats Scraper | Fight & Round StatisticsUFC fight statisticsUFCStats fight and round statistics as flat rows with fighter IDs, event dates and locations
Google Trends Scraper | Keyword Data APISearch-interest researchGoogle Trends interest over time, interest by region and related or rising queries, plus the Trending Now board by country
Clutch Agency Scraper | B2B Directory ExportAgency research and lead generationB2B agency listings from Clutch.co directories: company names, websites, locations, headcounts and service mix
Google Ads Transparency Scraper & ArchiveAd-creative researchAdvertiser 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.