Tennis Results & Odds Scraper - ATP, WTA, Challenger, ITF
Pricing
from $0.92 / 1,000 matches
Tennis Results & Odds Scraper - ATP, WTA, Challenger, ITF
Tennis results and odds scraper: TennisExplorer's matches for the days you pick, with tour, level, tournament, surface, round, both players, set scores with tiebreaks, winner and average odds. Upcoming matches come with odds too. Filter by tour, level, player or surface. $0.95 per 1,000 matches.
Pricing
from $0.92 / 1,000 matches
Rating
0.0
(0)
Developer
Dami's Studio
Maintained by CommunityActor stats
0
Bookmarked
10
Total users
4
Monthly active users
2 days ago
Last modified
Categories
Share
Tennis results and odds scraper for the days you pick, one row per match: tour, level, tournament, surface, round, both players, the set scores with tiebreak points, the winner and the average odds. Matches that have not been played yet come back too, with their odds and head to head. Or paste a player's page link and get their season.
Retirements are worked out from the score, because TennisExplorer never writes the word in its results. That is right almost always, and wrong in one shape: a best-of-five match abandoned between two completed sets reads as a finished win.
| Input | Days, as a rolling window or a fixed date range, or players' page links and years, plus optional filters |
| Output | One row per match: tour, level, tournament category, surface, round, players and seeds, set scores with tiebreaks, winner, how it ended, average odds |
| Ceiling | 25,000 matches per run, or 2,000 with match details on; up to 366 days or 500 player pages per run |
| Speed | Two days of schedule, 582 matches, took about a minute; a week of results, 2,938 matches, about two |
| Account needed | None |
| Price | $0.95 per 1,000 matches on the Free plan, a little less on paid plans. The free plan's $5 a month covers about 5,200 |
| Using an AI agent? | |
|---|---|
| Actor | dami_studio/tennis-matches-scraper |
| Set one field | lastDays, labelled "Last days" in the Console |
| Cap the bill | maxTotalChargeUsd on the run, for example 0.10: it never charges past that |
| MCP, one link | https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/tennis-matches-scraper |
| How often | Daily, after the day's matches end |
🔍 What Tennis Results and Odds Scraper does
It turns each match TennisExplorer lists for a day into a row. Yesterday only, a fixed range back to 1990, or today and the next day or two of schedule.
Player history works from a player's page instead: every match they played in the years you pick, singles, doubles and mixed, with surface, round, score and odds. Men's ITF matches get their surface and round here too.
Filters cut the list before anything is charged: match type, level, tournament category, status, surface, a tournament name or link, or a player's surname or link. A player filter keeps the match when either side matches, doubles partners included.
Optional passes add depth and cost time: Round (on), Rank on every row, and Match details, a page per match for full names, rankings, bookmakers' opening and closing prices, surface records and the head-to-head list.
📋 What data you get from each tennis match
| What you get | Field |
|---|---|
| The match id, the Prague date and time, and the same moment in UTC | matchId, date, time, startTimeUtc |
| Played or scheduled, and how it ended | status, outcome |
| Tour, singles or doubles, and level | tour, matchType, level |
| Tournament, host country, surface and round | tournament, tournamentCountryCode, surface, round |
| Both players with their seeds, and the winner | player1, player1Seed, player2, player2Seed, winner |
| The score, sets won, and every set with its tiebreak points | score, player1Sets, player2Sets, sets |
| Average odds, and the head to head before a match | player1Odds, player2Odds, player1H2H, player2H2H |
| Links to both players, the tournament and the match | player1Url, player2Url, tournamentUrl, url |
| With match details: full names, ranks, birth dates, heights, playing hand, each bookmaker's prices | player1FullName, player1Rank, player1BirthDate, player1HeightCm, player1Plays, the same for player 2, bookmakerOdds, detailsRead |
| With match details: win-loss by surface for the season and career, and every earlier meeting | surfaceRecords, headToHead |
| With countries on | player1Country, player2Country |
| Tournament category: Grand Slam, 1000, 500, 250 or 125 | category |
| With rank on every row: today's singles ranks and the list's date | player1Rank, player2Rank, rankingDate |
| In player history: whose page the row came from, a link to their photo, and whether their page lists this match as their retirement | historyPlayerUrl, historyPlayerPhotoUrl, historyPlayerRetired |
▶️ How to scrape tennis results and odds
- Open Tennis Results and Odds Scraper and click Try for free.
- Set Last days to 1 for yesterday, or Upcoming days to 1 for today's schedule. From date and To date give a fixed range instead. For one player's season, paste their page link into Player history.
- Narrow it with Match types, Levels, Tournament categories, Status, Surfaces, Tournaments or Players.
- Set Maximum matches. Start around 20 to see the output shape.
- Click Start, then download the dataset as JSON, CSV or Excel, or read it from the Apify API.
💰 How much does it cost to scrape tennis results?
$0.95 per 1,000 matches on the Free plan. Paid plans pay a little less: $0.94 on Starter, $0.93 on Scale and $0.92 on Business. On the free plan, the $5 Apify gives you each month covers about 5,200 matches.
You pay per match row delivered. Match details, rounds, ranks, countries and odds add nothing to the price. The sample row is not charged, and neither is anything that never reaches your dataset: a match dropped by a filter, one still being played, a repeat, a day with no matches, and a row that could not be read. A match whose detail page would not load is delivered without its details and not charged.
📥 What you give it
{"lastDays": 1,"upcomingDays": 1,"matchTypes": ["atp-single", "wta-single"],"levels": ["tour"],"maxMatches": 500}
| Field | Default | What it is |
|---|---|---|
lastDays | none, the box starts at 1 | The last N full days of results, ending yesterday. 1 is yesterday only, which suits a daily schedule. |
upcomingDays | none, the box starts at 1 | Today plus the schedule after it, up to 7. 1 is today, 2 adds tomorrow. Set beside lastDays with a maxMatches too small for both, the days from today on get up to half of it and the past days the rest, so both show. |
dateFrom | none | YYYY-MM-DD, for a fixed range instead. Back to 1990-01-01. Wins over the two fields above. |
dateTo | none | The last day. Empty stops at today. One run covers up to 366 days. |
playerHistory | none | Links to players' pages, like https://www.tennisexplorer.com/player/rybakina/. Their matches replace the days: the four fields above are not used. Up to 50 players. |
historyYears | this year | Years for playerHistory, like 2025, back to 1990. Each player and year is one page, 500 at most. |
matchTypes | all four | atp-single, atp-double, wta-single, wta-double. Men's matches sit under ATP and women's under WTA, Challenger and ITF included. Mixed doubles, which only player history returns, fit none of them. |
levels | every level | tour, challenger, itf, other-lower, exhibition. |
categories | every tournament | grand-slam, 1000, 500, 250, 125, from the points the winner gets. Other events have none, Challenger and ITF included, so the filter leaves their matches out. |
statuses | both | finished, scheduled. |
surfaces | any | hard, clay, grass, indoors. |
tournaments | none | Part of a tournament's name as TennisExplorer writes it, like US Open or Genoa, or a link to its page. |
players | none | A surname as TennisExplorer writes it, like Sinner or Rybakina, or a link to the player's page. |
includeOdds | on | The average of the bookmakers' prices for each player. On a finished match, the last prices before it started. |
addRounds | on | Reads each tournament's page once for the round and the category. Turn it off on long ranges to finish sooner. |
includeMatchDetails | off | One more page per match. Caps the run at 2,000 matches. |
addPlayerCountries | off | One more page per player, so a busy day takes several minutes longer. |
addRanks | off | Today's singles rank for both players, from the first 2,000 of each list. A small run reads less of the list and takes a missing player's rank from the match's own page, which shows the same rank. Nothing to add when match details are on. |
maxMatches | 1000, the box starts at 20 | Up to 25,000. Each match returned is one charge. |
Give no days at all and you get one labelled sample row, not an error and not an empty dataset.
📤 What you get back

A real row, the 2026 US Open men's final:
{"matchId": "3321413","date": "2026-09-13","time": "20:20","startTimeUtc": "2026-09-13T18:20:00Z","status": "finished","outcome": "completed","tour": "ATP","matchType": "singles","level": "tour","tournament": "US Open","tournamentCountryCode": "US","surface": "hard","round": "final","player1": "Zverev A.","player1Seed": "1","player2": "Shelton B.","player2Seed": "8","winner": "Zverev A.","score": "6-3, 7-6(2), 5-7, 6-2","player1Sets": 3,"player2Sets": 1,"sets": [{ "player1": 6, "player2": 3, "tiebreak": null },{ "player1": 7, "player2": 6, "tiebreak": 2 },{ "player1": 5, "player2": 7, "tiebreak": null },{ "player1": 6, "player2": 2, "tiebreak": null }],"player1Odds": 1.66,"player2Odds": 2.24,"player1H2H": null,"player2H2H": null,"player1Url": "https://www.tennisexplorer.com/player/zverev-6f768/","player2Url": "https://www.tennisexplorer.com/player/shelton-38989/","tournamentUrl": "https://www.tennisexplorer.com/us-open/2026/atp-men/","url": "https://www.tennisexplorer.com/match-detail/?id=3321413","category": "grand-slam"}
| Field | How to read it |
|---|---|
matchId | Stable. Use it as your dedupe key across runs. |
status, outcome | finished or scheduled, then completed, retired, walkover, or null when the match has not been played. |
score, sets | score is written from player 1's side in 7-6(3) style. sets breaks it out per set, with the tiebreak loser's points. |
time | The Prague clock, and null when TennisExplorer shows --:--. startTimeUtc is the same moment in UTC. |
player1Odds, player2Odds | The average across bookmakers, when includeOdds is on. |
player1H2H, player2H2H | The head to head, on matches that have not been played. |
round | From the tournament page, so it needs addRounds. Men's ITF and UTR matches have none to read, except in player history. |
category | With addRounds on and details off, or with the categories filter. null at any other event, Challenger and ITF included. |
player1Rank, rankingDate | With addRanks: the rank on today's list, not on the day of the match, and that list's date. null on doubles, and usually outside the first 2,000. |
surfaceRecords | With details, singles only: each player's win-loss by surface for the season named in it, and for their career, as they stand on the day of the run, not of the match. |
headToHead | With details, singles only: the count as the page prints it on the day of the run, and every meeting it lists, exhibitions included, from player 1's side. |
historyPlayerRetired | true only when the history player's own page lists this match as their retirement. An opponent's retirement still shows in outcome. |
time in player history | null: a player's page shows the day, not the time. |
| Doubles with details on | The full names join both partners; the one-player facts stay null, as the page shows teams. |
🧾 Reading the output
Two kinds of row can land in your dataset.
| Row | How to spot it | Charged |
|---|---|---|
| A match | It has a matchId and no flag | yes |
| The sample row | _sample: true | no |
There are no diagnostic rows here. Anything that went wrong is in the run's status message, and in
full in RUN_REPORT in the key-value store: which days or player pages were read, how many matches
each listed against how many came back, what could not be read, matches left out, and rounds that
were missing. stoppedBecause in that report says why the run ended.
| Value | What it means |
|---|---|
limit | It reached your maxMatches. |
deadline | It ran out of time. Everything delivered is complete. |
budget | The run's charge limit was reached. |
checks | The category filter kept finding tournaments outside your categories, so it stopped instead of reading on. Name the tournaments as well. |
unreachable | TennisExplorer stopped answering. |
The run reads nothing when the input cannot work, and still ends as succeeded, with an amber status message.
| Note row (not charged) | What it means |
|---|---|
BAD_INPUT | A value in the input is one the run refuses, so nothing was read. The run still ends as succeeded. |
COST_LIMIT | The maximum cost you set for the run does not cover a single result. Raise it and run again. The run still ends as succeeded. |
NOT_SERVED | The site did not answer any search in this run. Run it again later. The run still ends as succeeded. |
UNEXPECTED_ERROR | The run stopped early on an unexpected error. The results before this row are kept. |
💡 What people use it for
- Running it daily with Last days on 1, so each run picks up only yesterday's results.
- Pulling a player's seasons from their page link and checking the odds against the results.
- Watching tomorrow's schedule with odds and head to head before the day starts.
- Pulling every Grand Slam match of a season with the category filter, set scores and tiebreak points included.
🚧 What it does not do
- No live scores. A match being played is left out rather than delivered with a partial score.
- Retirements are read from the score. A best-of-five match abandoned between two completed sets reads as a completed win. In player history, a retirement the player's own page lists is marked.
- Ranks are today's, from the current lists, not the rank a player held on the day.
- Men's ITF matches come thin by day. TennisExplorer lists them under one
Futuresheading with no link, so they have no tournament name, surface or round unless match details are on or you use player history, and a surface filter drops them. - UTR events have no round, whatever you set.
- The schedule runs a day or two ahead, no further. That is how far TennisExplorer publishes.
- Finished days get edited afterwards. A match can leave a past day's list hours later, so two runs over one day can differ.
- Match details cap a run at 2,000 matches and add a page per match. Player countries add a page per player.
- Level is TennisExplorer's own split. Some WTA 125 events sit in its lower level rather than under tour.
- A long range takes real time. Turn rounds off to speed it up.
- It is not an official tool of any platform or company named on this page. Dami's Studio is independent and is not affiliated with or endorsed by any of them.
🧭 Which sports data scraper do you need?
| If you want | Use |
|---|---|
| Tennis results, schedules and average odds, day by day or by player | This one |
| Sportsbook lines and player props, tennis included | Sports Odds Scraper |
| Fixtures, scores and league tables across 21 sports | Sofascore Scraper |
| Prediction-market prices and volume | Kalshi Markets Scraper |
❓ Questions people ask
Do I need a TennisExplorer account?
No. The day, tournament, match, player and ranking pages it reads are all public.
How far back can I get tennis results?
To 1990-01-01. One run covers up to 366 days, so split a longer history into several.
Can I get just one player's matches?
Paste their page link into Player history and pick the years: every match they played comes back, newest first. The Players filter does the same within your days, by surname or link.
Is the rank the one on the day of the match?
No. Rank on every row reads today's lists. Match details give the rank the match page shows.
Why is my match date off by a day?
The date is TennisExplorer's, on Prague time. Use startTimeUtc when the exact moment matters.
Can I call it from code or connect it to an AI assistant?
The API tab has code for Python, JavaScript and the
CLI. MCP clients like Claude and ChatGPT use
https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/tennis-matches-scraper; coding assistants can load our
skill
file.
All three bill your Apify account at the same price.
# pip install apify-clientfrom apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_TOKEN")run = client.actor("dami_studio/tennis-matches-scraper").call(run_input={"lastDays": 1,"maxMatches": 20,},max_total_charge_usd=0.10, # the most this run may spend)for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row.get("ok") is False: # free note rows, not resultscontinueprint(row.get("date"), row.get("tournament"), row.get("score"))
Is scraping tennis results legal?
These are public sports results. TennisExplorer's own site terms still apply to its pages, so read them and decide what suits your use. Apify's write-up on the legality of web scraping is a good starting point, and we are not lawyers.
🆘 If something breaks
Open the Issues tab on the actor page. Send the dates and filters you used and the run ID. The
RUN_REPORT in the run's key-value store usually explains it on its own.