Tennis Stats Scraper: ATP & WTA Match History, Elo avatar

Tennis Stats Scraper: ATP & WTA Match History, Elo

Pricing

from $6.40 / 1,000 matches

Go to Apify Store
Tennis Stats Scraper: ATP & WTA Match History, Elo

Tennis Stats Scraper: ATP & WTA Match History, Elo

Tennis Abstract match history with serve and return stats for many ATP and WTA players per run, plus profiles, season and surface splits and Elo tables. Filters by date, surface, level, opponent rank. $6.40 per 1,000 rows, no monthly fee.

Pricing

from $6.40 / 1,000 matches

Rating

0.0

(0)

Developer

Misha Ershov

Misha Ershov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

19 hours ago

Last modified

Share

Tennis Abstract Scraper: Match Stats, Elo, ATP & WTA

Pulls structured tennis data from Tennis Abstract: full match logs with serve and return statistics for any ATP or WTA player, player profiles, season and surface splits, and the current ATP/WTA Elo rankings (overall, hard, clay, grass).

Give it player names, slugs or Tennis Abstract URLs. Get back clean rows you can drop into a spreadsheet, a model or a betting/analytics pipeline. No login and no API key are needed.

What you get

  • Matches (type: "match"): date, tournament, surface, level (raw code plus a readable levelName), round, result, score (plus sets and outcome), matchId, opponent with rank/seed/hand/age data, minutes, and the serve statistics for both players (aces, double faults, first/second serve points won, break points). Derived percentages (ace %, first serve in %, service points won %, return points won %, dominance ratio) are computed for you. Newest matches come first.
  • Players (type: "player"): current rank, peak rank, age, hand, backhand, height, country, titles, career match count and the current Elo values.
  • Seasons (type: "season"): year-by-year and surface/last-52-week split tables.
  • Elo (type: "elo"): the full Tennis Abstract Elo ranking tables for ATP and WTA.

Input

FieldTypeDefaultDescription
modeplayers / eloplayersplayers scrapes the players you list. elo scrapes the Elo ranking tables only.
playerslist of stringsemptyNames (Jannik Sinner), Tennis Abstract slugs (JannikSinner) or URLs containing p=. Required in players mode.
tourauto / atp / wtaautoauto tries ATP first, then WTA.
dateFrom, dateToYYYY-MM-DDnoneKeep only matches in this date range.
surfaceslist of Hard, Clay, Grass, CarpetallKeep only these surfaces.
levelslist of level namesallKeep only these tournament levels: Grand Slam, Masters 1000, Tour 250/500, Tour Finals, Challenger, ITF, Team & Olympics, Other.
roundslist of roundsallKeep only these rounds: R128, R64, R32, R16, QF, SF, F, RR, BR, Q1, Q2, Q3.
opponentRankMin, opponentRankMaxinteger0 (off)Keep only matches where the opponent's ranking at the time is within this range. Matches with unknown opponent rank are dropped when either is set. If min is above max they are swapped.
resultsall / wins / lossesallFilter by the player's result.
includeUpcomingbooleanfalseAlso output scheduled matches (result U).
opponentstringnoneKeep only matches whose opponent name contains this text (case-insensitive).
maxMatchesPerPlayerinteger 0-5000500Cap on match rows per player, newest first. 0 means no match rows (profile and seasons only).
includeProfilebooleantrueOutput one player row per player.
includeSeasonStatsbooleanfalseOutput season rows (yearly and split tables).
eloTourslist of atp, wtabothWhich Elo tables to read in elo mode.
maxResultsinteger1000Max Elo rows per tour in elo mode.
onlyNewbooleanfalseSkip matches already returned by earlier runs with the same state.
proxyConfigurationobjectApify proxyDatacenter proxy is enough.

Match row fields added for filtering

  • levelName: readable tournament level, derived from the raw level code (table below).
  • matchId: Tennis Abstract match id, null for older matches.
  • sets: list of {winnerGames, loserGames, tiebreak} from the winner's point of view (the same as score); tiebreak is the loser's tie-break points or null.
  • outcome: completed, retired, walkover, default, or null when there is no score.
Raw level codelevelName
GGrand Slam
M, PMMasters 1000
A, P, ITour 250/500
FTour Finals
CChallenger
S, any number (ITF prize money in $ thousands)ITF
D, OTeam & Olympics
anything else (for example WTA W)Other

Example input:

{
"mode": "players",
"players": ["Jannik Sinner", "Iga Swiatek"],
"dateFrom": "2026-01-01",
"surfaces": ["Grass"],
"maxMatchesPerPlayer": 100
}

Output examples

Real rows from a live run.

Match row (shortened):

{
"type": "match",
"player": "Jannik Sinner",
"playerSlug": "JannikSinner",
"tour": "atp",
"date": "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",
"sets": [{"winnerGames": 6, "loserGames": 7, "tiebreak": 7}, {"winnerGames": 7, "loserGames": 6, "tiebreak": 2}, {"winnerGames": 6, "loserGames": 3, "tiebreak": null}, {"winnerGames": 6, "loserGames": 4, "tiebreak": null}],
"outcome": "completed",
"bestOf": 5,
"rank": 1,
"seed": 1,
"opponent": "Alexander Zverev",
"opponentRank": 3,
"opponentHand": "R",
"opponentCountry": "GER",
"minutes": 226,
"aces": 15,
"doubleFaults": 2,
"servePoints": 136,
"firstServesIn": 87,
"firstServeWon": 70,
"secondServeWon": 32,
"acePct": 11,
"doubleFaultPct": 1.5,
"firstServeInPct": 64,
"firstServeWonPct": 80.5,
"secondServeWonPct": 65.3,
"servicePointsWonPct": 75,
"returnPointsWonPct": 30.9,
"dominanceRatio": 1.24,
"playerUrl": "https://www.tennisabstract.com/cgi-bin/player-classic.cgi?p=JannikSinner",
"scrapedAt": "2026-10-13T05:46:44Z"
}

Player row:

{
"type": "player",
"name": "Carlos Alcaraz",
"slug": "CarlosAlcaraz",
"tour": "atp",
"url": "https://www.tennisabstract.com/cgi-bin/player.cgi?p=CarlosAlcaraz",
"currentRank": 3,
"peakRank": 1,
"peakRankDate": "2022-09-12",
"birthDate": "2003-05-05",
"age": 23.4,
"heightCm": 183,
"hand": "R",
"backhand": "two-handed",
"country": "ESP",
"careerMatches": 477,
"careerWins": 387,
"careerLosses": 90,
"titles": 35,
"eloRank": 2,
"elo": 2171.2,
"hardElo": 2111.3,
"clayElo": 2098.8,
"grassElo": 2026.4,
"scrapedAt": "2026-10-13T05:47:22Z"
}

Season row:

{
"type": "season",
"table": "tour-years",
"label": "2026",
"matches": 47,
"wins": 44,
"losses": 3,
"winPct": 93.6,
"holdPct": 94.4,
"breakPct": 29.1,
"acePct": 14.2,
"doubleFaultPct": 2.1,
"firstServeInPct": 64.8,
"firstServeWonPct": 81.4,
"secondServeWonPct": 59.1,
"servicePointsWonPct": 73.6,
"returnPointsWonPct": 40.7,
"totalPointsWonPct": 56.2,
"dominanceRatio": 1.54,
"player": "Jannik Sinner",
"slug": "JannikSinner",
"tour": "atp"
}

Elo row:

{
"type": "elo",
"tour": "atp",
"eloRank": 1,
"name": "Jannik Sinner",
"slug": "JannikSinner",
"age": 24.8,
"elo": 2296.9,
"hardEloRank": 1,
"hardElo": 2234.3,
"clayEloRank": 1,
"clayElo": 2186.8,
"grassEloRank": 1,
"grassElo": 2100.5,
"peakElo": 2339.8,
"peakMonth": "2026-05",
"tourRank": 1,
"updatedAt": "2026-10-05"
}

Pricing events

Rows are charged per type: match, player, season and elo-row. Runs stop cleanly when your spending limit is reached.

FAQ

Where does the data come from? Directly from Tennis Abstract's public pages and data files. Tennis Abstract refreshes its data roughly weekly, so expect "weekly-ish" updates; very recent matches may show up with a delay.

How are player names matched? A URL with p= always wins. A single token without spaces is treated as the slug. Otherwise the name is matched (accents, case and punctuation ignored) against the ATP and WTA Elo tables, which are downloaded once per run. If there is no match, the capitalized words are joined into a slug (jannik sinner becomes JannikSinner). Players that are not found are logged and counted as notFound; the run still succeeds.

What do Elo, hElo, cElo and gElo mean? Elo is the overall rating. hElo, cElo and gElo are the surface-specific ratings for hard, clay and grass courts (output fields hardElo, clayElo, grassElo).

Why are some statistics empty? Detailed serve data exists only for matches Tennis Abstract has charted or received stats for; other rows keep null in those fields.

Missing something? Open an issue - we add fields on request.