Tennis Scraper — ATP, WTA | Live Scores, Odds, Point by Point
Pricing
from $0.78 / 1,000 match with score and competitions
Tennis Scraper — ATP, WTA | Live Scores, Odds, Point by Point
ATP, WTA, ITF and Challenger tennis: per-set statistics, point-by-point sequences, head-to-head, live scores down to the point in play and odds from up to 20 bookmakers per country. Current rankings, tournament level and seeds. Season archives decades deep. No login, no browser.
Pricing
from $0.78 / 1,000 match with score and competitions
Rating
0.0
(0)
Developer
Huyen Trang
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
3 hours ago
Last modified
Categories
Share
Every ATP, WTA, ITF and Challenger match with the numbers behind the result: statistics per match and per set — aces, serve speed, break points, up to 23 metrics at Grand Slams (smaller events carry fewer) — winners, unforced errors — plus head-to-head history, venue and match duration.
No login. No cookies. No API key. No headless browser.
Quick start
- Open Input: the form is ready to run as it is — the 20 matches closest to today in the 15-day window, top competitions first, with statistics and summary. To narrow it, set Days to Yesterday and Tours to ATP.
- Press Start and wait for the run to finish.
- Open Output, press Export, set View to Matches and the format to Excel, then Download: one row per match with the match ID, date and time, players, the score as numbers and as text, status, winner and link. In CSV and Excel All fields keeps the score and players too, but scattered among about 2,000 columns in alphabetical order, and most nested data (statistics, events, box scores) is cut off at that limit; keep it for JSON (see Getting a spreadsheet).
With the form as it ships, Matches and Statistics have data; the other tabs of Output stay empty until you turn on the option named in their title.
Cost: the form as it ships comes to at most $0.03365 a run — $0.00005 for the start plus $0.00168 a match; a match without a stat sheet or summary costs less. 1 000 matches cost $1.68 at the default settings, and Apify's free plan, $5 of usage a month, covers about 2 900 of them. Every option is priced under What a run actually costs.
Recipes
Paste one into the JSON view of the Input tab or send it through the API. Prices are per match returned, at this Actor's event prices; the run start adds $0.00005.
ATP results of yesterday — $0.00168 a match with statistics and summary.
{"dateRange": "yesterday", "tennisTours": ["atp"]}
Only matches in play, with the score in play — $0.00098 a match: the result plus the live state.
{"matchStatuses": ["live"], "includeLive": true, "includeStats": false, "includeSummary": false}
One tournament from its page — $0.00078 a match, scores only.
{"leagueUrls": ["https://www.flashscore.com/tennis/atp-singles/us-open-2025/"], "matchType": ["singles"], "maxMatches": 300, "includeStats": false, "includeSummary": false}
A player's schedule and recent results — $0.00078 a match, scores only.
{"teams": ["Sinner"], "teamResultsPages": 1, "includeStats": false, "includeSummary": false}
A results feed on a schedule — $0.00078 for each new or changed match plus only the parts that changed; an unchanged run pays the start and the run minimum. Run it hourly from a schedule with isExclusive on.
{"dateRange": "yesterday-today", "tennisTours": ["atp", "wta"], "monitorMode": true, "monitorName": "tour-results"}
Pre-match odds board — $0.00118 a match with odds and lines; matches without prices are skipped and not billed.
{"dateRange": "today-tomorrow", "matchStatuses": ["scheduled"], "tennisTours": ["atp", "wta"], "includeStats": false, "includeSummary": false, "includeOdds": true, "includeOddsMarkets": true, "onlyWithOdds": true}
Getting a spreadsheet
In Console. Open the run's Output tab and press Export. Pick a View, then the format, then Download:
| View in Console | view= in the API | One row per | Needs |
|---|---|---|---|
| Matches | overview | match | — |
| Odds | odds | match | Match-winner odds |
| Live state | live | match | Live state |
| Statistics | statistics | statistic per part of the match | Match statistics |
| Bookmakers | bookmakers | bookmaker | Match-winner odds |
| Every price | prices | price of every bookmaker | Every bookmaker price |
| Totals | totals | over/under line | Totals and handicaps |
| Head-to-head | h2h | direct meeting | Head-to-head history |
| Changes (monitor runs only) | changes | match the monitor emitted | — |
Long views unwind a list of the record into rows, so one match gives as many rows as it has statistics, events, players or prices; a match without that part gives a single row with the match columns and empty columns of the part, and a match whose list is empty gives none. The dialog opens on All fields, which exports every nested field of the records. In CSV and Excel that stops at 2,000 columns: the top-level fields (score, teams, competition, date) stay, but in alphabetical order among the rest, while most nested data — statistics, events, box scores, head-to-head rows — is cut off. Choose Excel rather than CSV: Excel opens a CSV with its own rules, so a text score such as 6-4 can turn into a date, and with a semicolon list separator the file opens as one column.
Through the API. The same views come straight as files:
https://api.apify.com/v2/datasets/<datasetId>/items?view=overview&format=xlsx&clean=true
Use format=csv for CSV and any view= name from the table. The column headers of such a file
are field paths (homeTeam, odds.margin), not the labels of the table in Console. To name the
columns yourself, list the fields and give outputFields in the same order; outputFields works
with fields, not with view:
https://api.apify.com/v2/datasets/<datasetId>/items?fields=matchId,startDate,homeTeam,awayTeam,homeScore,awayScore&outputFields=Match,Date,Home,Away,Home%20score,Away%20score&format=xlsx
The dataset ID is in the run's details and in defaultDatasetId of the run object. A link
without a token works when your account lets anyone with the ID read your storages; with
Restricted access send your token in the Authorization: Bearer header or use
Export → Copy shareable link. Apify keeps your ten most recent runs with their datasets; older
runs are deleted with their storages after your plan's data retention period
(Apify docs), so keep the files you need
or append the records to a named dataset (outputDatasetName).
A named dataset has no views. The Actor opens it without its dataset schema, so view= does
not work on it and its Export offers only All fields. Name the columns with fields instead,
for example those of Matches and Changes:
https://api.apify.com/v2/datasets/<dataset ID, or owner~datasetName>/items?fields=matchId,startDate,startTime,tournament,homeTeam,awayTeam,homeScore,awayScore,scoreText,status,statusDetail,winnerName,matchUrl,scrapedAt,changeType,monitorChangedFieldsText,unavailablePartsText&format=xlsx&clean=true
Output example
One record from a local run, 14 October 2026 — De Minaur A. vs Khachanov K., ATP - SINGLES: Shanghai (China), hard — trimmed: lists keep their first item and nested objects only some of their fields. Every field is described under Output fields.
{"matchId": "hxyDNByK","sport": "tennis","startDate": "2026-10-13","startTime": "04:10","startTimeKnown": true,"timezone": "UTC","homeTeam": "De Minaur A.","awayTeam": "Khachanov K.","homeScore": 2,"awayScore": 0,"tournament": "ATP - SINGLES: Shanghai (China), hard","tournamentId": "lSHMFchs","startsAt": "2026-10-13T04:10:00.000Z","status": "finished","statusDetail": "finished","home": {"name": "De Minaur A.","id": "EZgZ9Xfh","url": "https://www.flashscore.com/player/de-minaur-alex/EZgZ9Xfh/","currentRank": 9,"seed": "9"},"away": {"name": "Khachanov K.","id": "4bNPtZT5","url": "https://www.flashscore.com/player/khachanov-karen/4bNPtZT5/","currentRank": 27,"seed": "26"},"score": {"home": 2, "away": 0},"scoreText": "6-3, 6-4","winnerName": "De Minaur A.","matchUrl": "https://www.flashscore.com/match/hxyDNByK/","stats": {"headline": {"Aces": {"home": "4", "away": "3"}, "Double Faults": {"home": "1", "away": "0"}}},"odds": {"market": "HOME_AWAY","priceState": "closing","home": 1.74,"away": 2.04,"bestHome": 1.76,"bestHomeBookmaker": "Unibetuk","bestAway": 2.1,"bestAwayBookmaker": "William Hill","bookmakersCount": 20,"margin": 6.49,"impliedHome": 0.5397,"impliedAway": 0.4603,"movement": {"homeDirection": "drifting"},"bookmakers": [{"bookmaker": "Unibetuk","country": "GB","homePrice": 1.76,"awayPrice": 2.07,"openingHome": 1.7,"openingAway": 2.09,"active": false}]},"scrapedAt": "2026-10-14T13:56:21.639Z"}
What you get
| Capability | What it means for you |
|---|---|
| Retirements and walkovers marked as such | statusDetail says retired or walkover, so an unfinished match never passes for an ordinary win in your model |
| Statistics per set, not just per match | model momentum inside a match, not only the outcome |
| Every point with server, winner and break, set and match points | tiebreaks with mini-breaks, match totals checked against Flashscore's own Total Points Won |
| Head-to-head with a form summary | history cut at the match date, records per surface, rest days and a last-10 form string |
| Current ranking, tournament level and seeds | ATP or WTA rank and points for both players, Grand Slam down to ITF, seeds and wild cards from the draw |
| Filters that combine | surface, singles or doubles, tour and tournament level joined with AND |
| Live-only runs | filter to matches in play before you are billed, not after |
Every field, per match
| Group | Metrics |
|---|---|
| Service | Aces, double faults, 1st serve %, 1st and 2nd serve points won, break points saved, average serve speed |
| Return | 1st and 2nd return points won, break points converted |
| Points | Winners, unforced errors, net points won, service and return points won |
| Games | Service games won, return games won, total games won, distance covered in metres |
Every metric comes per match and per period — Match, Set 1, Set 2 and so on — so you can model
momentum, not just outcomes.
Every stat row carries a stable snake_case key and numbers: homeValue/awayValue, and for shares like 63% (36/57) also homePercent, homeMade, homeAttempts (and the away side) — no string parsing before the data goes into a model.
Match context, not just numbers
Alongside the stat sheet each match carries a summary: venue, town, attendance,
referees, score broken down by period and match duration — whatever the source
publishes for that competition. Top-tier games carry the most. Match and set durations come both as text (totalDuration: "2:44") and as minutes (totalDurationMinutes: 164, periodDurationsMinutes: [41, 73, 50]).
Form history, not just the last meeting
Head-to-head returns three lists: recent matches of the first side, of the second, and their
direct meetings. Each row carries date, surface, tournament and country, the numeric score, the winner, how the match
ended (played out, retired or walkover, in status), participant IDs and a link to
the match. The default depth is 10 rows per side; set maxH2HRecords to 0 for everything the
source publishes — up to about 155 per side, as the clay, grass and hard tabs are merged in after the overall list of 50. Direct meetings come in full by default (maxH2HMeetings = 0); most pairs have met a handful of times or never. The match itself is flagged
isThisMatch and is never billed; matches played after it are left out, so the history of a
past match stops at its own date. The Head-to-head view lists the direct meetings, one row
each.
Rows are billed one by one, at most 30 a match: the rest of the history comes in the same record for free. At the default depth a match holds about 10 to 28 rows in most cases, so it is billed 10 to 28 — the measured counts are under What a run actually costs below.
On top of that comes a form summary per side, counted only from matches played before this one: wins and losses, a form string of the last 10 results, the date of the last match and rest days, which tournaments come up most, and a win-loss record on each surface and on the surface of this match. That is the input for surface-specific models, and it arrives without a second Actor.
Point by point
includePointByPoint returns every game and tiebreak with its full point sequence, and every point carries who served, who won it and whether it was a break, set or match point. Tiebreaks come with the server of each point and the mini-breaks, and a doubles match tiebreak to 10 is handled correctly. Match totals (pointByPointTotals: points won, breaks, break points faced, mini-breaks) were checked against Flashscore's own Total Points Won on 756 finished matches: exact on 641, pointsComplete: false on 78 where a gap in the source breaks the scoring rules, and 1 to 9 points apart on 37 where the source's point-by-point and statistics feeds disagree. The game in play is included but not charged. One extra request per match.
Score fields
- Ready-made score fields:
score(sets won),scoreText("6-7(3), 4-6", written from the home side),winnerNameand a profileurlfor each player. - Tie-break points on every set (
tiebreakHome/tiebreakAway, null when there was no tie-break). - Doubles: each side carries a
partnerobject with the second player's name, id, country and current ranking. home.countryId/away.countryId(andpartner.countryId) are the source's numeric country ids.- Every match carries the tournament's stable id (
tournamentId) — the key to the source's season archives — and, where the competition has seasons,tournamentSeasonIdandtournamentStageId. Our League Standings Actor has no league tables for tennis; its ATP and WTA rankings carryplayerId, which equalshome.idandaway.idhere, so matches join with the rankings of their players.
Rankings, tournament level, seeds and tennis filters
currentRank, currentRankPoints and currentRankDate give each player's current ATP or WTA ranking (singles or doubles, matching the match) as published on currentRankDate. This is the ranking at the time of the run, not on the day of the match; players without a ranking get null.
Every match carries tournamentName ("Shanghai"), tournamentCountry ("China"), isQualification, tennisTier (grand-slam, atp-1000, atp-500, atp-250, atp-finals, wta-finals, wta-1000, wta-500, wta-250, wta-125, challenger, itf, davis-cup, billie-jean-king-cup, other — from the 2026 ATP and WTA calendars) and bestOf (5 for men's Grand Slam singles, otherwise 3). Turn on includeSeeds to get seed per player from the tournament draw: 1, 2, WC, Q, LL, PR, Alt — one extra request per tournament draw, the same request resolveRounds reads rounds from when both are on.
surfaces, matchType (singles or doubles), tennisTours and tennisTiers combine with AND: ATP plus clay gives ATP clay matches only, and ["hard", "indoor"] means indoor hard courts. The free-text tours filter keeps a match that matches any one of its strings. Match IDs ignore these filters.
News, highlights and where to watch
Three optional feeds, all in the same match record:
includeNews— news articles linked to the match: title, outlet, image and publication time. Notable matches carry dozens of articles.includeHighlights— official video highlight links with preview images, available once the match has been played.includeBroadcasts— TV channels and streaming services the source lists for the match, by country, plus bookmaker live streams. Each item haskind(tvorbookmaker-stream), the source'schannelIdorbookmakerId, andcountryHint, the country as the channel name writes it in brackets (Usa,GBr), not an ISO code. The source's partner links lead to advertising trackers, so theirurlis null. For some matches (many football ones) the source sends the channel names only: those items havechannelIdandurlnull. When the list has no TV channel at all, one more request reads the source's shorter listing, which depends on the country the request comes from.
One request per match per option, two for broadcasts of a match without a channel list. Each is billed only when data exists; broadcasts only when at least one TV channel is listed, so bookmaker streams alone are free.
Season archives: history beyond the daily window
The daily feeds cover seven days back and seven ahead. For anything older, pass
league and season URLs: https://www.flashscore.com/tennis/atp-singles/us-open-2025/ — Grand Slam
archives reach back to 1990, qualification included.
Old matches return whatever detail the source still holds for them, which thins out
the further back you go.
{ "leagueUrls": ["https://www.flashscore.com/tennis/..."] }
Archive records without a start time. Some archive pages keep only the day of a match, with a placeholder time at midnight in Europe or at midnight UTC. When at least 90% of the games on a page older than a year sit at such a midnight, those records carry startTimeKnown: false, an empty startTime and startDate set to the day of the match, while startsAt and startTimestamp keep the placeholder; dateFrom and dateTo compare the day of the match. The check goes by the times on the page, so it can be wrong for an old league that really played at those hours. Records of the daily feeds and of the past year always have startTimeKnown true.
Specific matches by ID
Paste Flashscore match IDs or match URLs into matchIds and get exactly those matches,
from any date the source keeps. Unknown IDs and matches of another
sport are listed in the run warnings and never billed. Match IDs
override dateRange, league URLs, players and every competition, status and tennis filter;
maxMatches, dateFrom and dateTo still apply.
Date ranges
Set dateFrom and dateTo (YYYY-MM-DD). On their own they pick days inside the
15-day window; together with league URLs, teams or match IDs they keep only matches in
that range, so a one-month slice of a season is billed as one month.
Dates outside the window by tournament. When dateFrom and dateTo reach outside the 15-day window and tours names a tournament, the run reads that tournament's archives instead of the daily feeds, up to six archives a run (the newest first; the rest are named in the run warnings) and one per Standby request: tours: ["Wimbledon"] with 12 and 13 July 2025 read the ATP and WTA singles archives of Wimbledon 2025 and returned both finals. Known in advance are the four Grand Slams (ATP and WTA singles), Indian Wells, Miami, Monte Carlo, Madrid, Rome, Cincinnati, Shanghai and Paris (ATP singles), Wuhan and Beijing (WTA singles), though only Wimbledon was checked with a run; any other name is looked up in today's daily list, so it has to have a match today. A name that cannot be resolved, or an archive the source does not return, is named in the run warnings.
Team and player search
Pass names instead of URLs — teams: ['Sinner' or 'Sabalenka'] — and the Actor resolves
them through the source's own search, then pulls the full known schedule and as much
history as you ask for (teamResultsPages, about 40 matches per page).
Paste a Flashscore team or player link instead, for example
https://www.flashscore.com/player/sinner-jannik/6HdC3z4H/, or the 8-character ID from it, to skip the name search and get
exactly that team or player.
Monitor mode: pay only for what changed
Turn on monitorMode and the Actor remembers every match it has already delivered in a
named key-value store in your account. The next run emits — and bills — only new
and changed matches: new fixtures, kickoff moves, score updates, line moves, final
results. On a schedule, an unchanged day costs only the run itself: the start event
and the run minimum (see the costs below), usually under half a cent a run.
Use monitorName to keep separate watchlists apart. monitorKey, the older name of the
same field, still works in saved tasks, schedules and API calls: the same value keeps the
same memory, and when both are set monitorName is used.
Every record the monitor emits says why: changeType is new for a match it has not
seen before and changed for one it has, and monitorChangedFields names what changed
— score, status, schedule, odds, stats, summary, lineups and so on — so a
kickoff move can be handled differently from a final score. monitorChangedFieldsText
holds the same list as one string ("score,status") for a spreadsheet column, and the
Changes view shows it next to changeType. A part that did not arrive because the
source failed
(unavailableParts) is not a change: the match is not output again for it, and when the
part comes back unchanged it is not billed again. With odds on, odds.previous holds the
average and best prices this monitor output last time, with the time, and odds.changePct
the change of each average price in percent.
A changed match is billed for what changed, not for the whole record again. It
costs the base event, plus the extras of the parts named in monitorChangedFields: a
moved line bills the odds, a fuller stat sheet bills the stats, while an unchanged
summary or lineup is not billed twice. Within odds, the three odds events are billed
separately: a moved match-winner price (pre-match or in-play) bills match-odds, a moved
total, handicap or both-teams-to-score line match-odds-markets, a changed price list
match-odds-offers. Row-billed parts — head-to-head rows, completed
games or periods of point by point, player lines, news articles — are billed only for
rows beyond those already paid for that match. The record itself still arrives complete.
Free parts do not make a match changed: a changed bookmaker stream alone does not output the match again, and a match output for another reason carries it up to date.
What a monitor run checks. In monitorMode, maxMatches caps the matches a run
checks, not only the ones it outputs. Matches that are over and started more than a day ago
are skipped without a request while they stay the same, and do not count toward it. When
more candidates are left than maxMatches, half of the checks (rounded up) go every run to
matches in play, matches past their start time, finals not output yet and then the top
competitions, and the other half to the rest, the ones checked longest ago first, so a big
watchlist is covered over several runs while the top matches are checked in every one.
The monitor remembers the unfinished matches of the first day it reads until their final is
output, including matches cut by maxMatches or by the status filter while they were in
play, and reads them again when they fall outside the selected days — from a feed already
read, or from the feed of the day they started, one request per day — first among the
checks, and billed only if they changed. Schedule it hourly with dateRange
yesterday-today, or daily with last-3-days, so a match that ends after midnight still
gets its final score; a monitor that reads a single day says so in its status. With
matchStatuses the monitor outputs only matches in those statuses: a monitor of live
matches only does not output final scores, so add finished to get them.
One monitor per name at a time. Do not run two monitors with the same monitorName at
once: both would output and charge the same changes. On a frequent schedule turn on the
schedule's isExclusive option, which skips a run while the previous one is still going —
that is the reliable protection. The Actor also tries to detect an overlap: a run that finds
the memory saved by another run of the same name within the last few minutes, while that run
is still going, outputs nothing and says why in its status. It relies on that run having
saved its memory midway (about once a minute while it changes something), so a short overlap
can pass unnoticed; then the two memories are merged at the end, and the status warns about
it.
Regular loads: deduplicate and update
Every record carries matchId, the source's own ID, which stays the same across runs, and
scrapedAt, the moment the record was output. A match read twice — two runs over overlapping
days, or a monitor that emits it again after a change — comes as two records: keep the one with
the larger scrapedAt.
Do not let a failed part overwrite good data. When the source fails to send a part of a match
(statistics, summary, lineups, box score, head-to-head, odds and so on), the record names it in
unavailableParts (unavailablePartsText in the Matches and Changes views), leaves it
null and is not charged for it. A later record of the same match can carry such a gap where an
earlier one had the data, and in monitorMode the part that comes back unchanged is not output
again. So take the newest record and fill the parts it names from the latest earlier record that
has them. In pandas:
import pandas as pddf = pd.read_json("dataset.json").sort_values("scrapedAt")latest = []for _, rows in df.groupby("matchId", sort=False):row = rows.iloc[-1].copy()gaps = row.get("unavailableParts")for part in gaps if isinstance(gaps, list) else []:if part in rows and row[part] is None:filled = rows[part].dropna()if len(filled):row[part] = filled.iloc[-1]latest.append(row)latest = pd.DataFrame(latest)
In a database or a sheet, upsert on matchId when the new scrapedAt is later, and for the
parts named in unavailableParts keep the stored value (COALESCE(new.stats, old.stats)).
monitorMode sends only new and changed matches, and the Changes view shows changeType and
monitorChangedFieldsText ("score,status") for each. outputDatasetName appends every record to
one named dataset across runs, so the history stays in one place; a named dataset is kept until
you delete it, Apify bills its storage, and it has no views (see Getting a spreadsheet).
Compact output
Set omitEmptyFields to drop every null, empty list and empty object from the records.
Measured on 8 finished ATP matches without extras on 14 October 2026: 52 top-level fields
shrink to about 38 and the record from 2.2 KB to 1.9 KB. Items inside lists keep their positions, so
period-by-period lists stay aligned. Leave it off for CSV and spreadsheet exports, where
a fixed set of columns is easier. With it off, records of runs with the same options have
the same keys; some keys appear only with their option or mode (odds.markets, odds.offers,
odds.live, the American prices, odds.previous and the other monitor fields, seeds), or only
when there is something to say (unavailableParts, a lifted box-score column the match has),
so a dataset that collects runs with different options has a varying set of keys.
Betting odds
Turn on includeOdds and every match carries match-winner prices from each bookmaker of
the countries you pick — opening price and current price side by side — plus the market
average, the best price and the bookmaker margin. Finished matches keep their closing
prices, so already-played days can be backfilled.
The match-winner market is two-way: there are no draws in tennis. odds.market and odds.scope name it.
Every odds record adds no-vig probabilities (odds.impliedHome, odds.impliedAway,
from 0 to 1), the best price of each outcome with the bookmaker that offers it
(odds.bestHome and odds.bestHomeBookmaker, and so on), the average opening price of the same
bookmakers (odds.openingHome...), how many bookmakers the averages cover (odds.bookmakersCount),
the book on the best prices (odds.bestPriceOverround, percent above 100) and odds.arbitrage,
true when the best prices of different bookmakers cover every outcome.
odds.movement shows where the match-winner market has gone since opening: for every
outcome, how many bookmakers shortened the price and how many drifted it.
odds.movement.homeDirection sums it up for the home side only, not for the whole market. It is
part of the match-winner odds and is not billed separately. Set oddsFormat to american and
averages, best prices, every bookmaker and in-play prices also come in American format
(odds.homeAmerican: +150, -110); decimal prices always stay.
Prices come from the odds feed behind Flashscore's own Odds tab, keyed by the same match id
and by the participant ids of the fixture, so a price never lands on the wrong team.
oddsCountries picks whose bookmakers you see. On ATP matches the British storefront lists up to 20 bookmakers. The country is a
parameter of the request, not of your proxy, so the list is the same from any exit address,
Apify's US servers included. Pick several countries to merge their bookmakers into one
record: each bookmaker appears once with its country, and averages and best prices use the
merged list. One request per country per match. All 17 countries on one
Ligue 1 match, read on 14 October 2026 (Ukraine listed none that day):
oddsCountries | Storefront | Bookmakers | Among them |
|---|---|---|---|
GB | Great Britain | 20 | bet365, William Hill, Betfair, Paddy Power, Skybet |
US | United States (New Jersey) | 5 | DraftKings, Fanduel, BetMGM.us, Fanatics, bet365.us |
IT | Italy | 13 | Sisal, SNAI, Eurobet.it, GoldBet, Lottomatica |
ZA | South Africa | 8 | Betway Africa, Sportingbet, Sunbet, Betxchange, Easybet |
FR | France | 8 | Betclic.fr, Winamax, Unibet.fr, Pmu, bwin.fr |
SE | Sweden | 7 | Unibetse, BetMGM.se, MrGreen, Coolbet, Expektse |
ES | Spain | 6 | Codere, Sportium.es, Luckia.es, Retabet, Versus.es |
MX | Mexico | 6 | Calientemx, 1xBet, Novibet, Sportium.mx, Winpot |
DE | Germany | 6 | Interwetten, bwin, Betano.de, Oddset.de, Winamax.de |
PL | Poland | 6 | STS.pl, eFortuna.pl, Betclic.pl, Superbet.pl, LV BET |
CA | Canada (Ontario) | 5 | 888sport, TonyBet, PointsBet.ca, Sports Interaction, Betano.ca |
CZ | Czech Republic | 4 | Tipsport.cz, iFortuna.cz, Betano.cz, Chance.cz |
NG | Nigeria | 2 | Bet9ja, Betking |
NL | Netherlands | 2 | bet365.nl, Sevelev.nl |
AR | Argentina | 1 | Unibet |
AU | Australia | 1 | Unibetau |
UA | Ukraine | 0 | — |
Prices of a match that has started are its closing prices. The pre-match market closes
at the start, and the source keeps showing its last prices. On a match in play, finished,
interrupted, abandoned or canceled, odds.priceState is closing, every bookmaker and every
row of the price list carries active: false, and odds.arbitrage is null: a closed market
offers no bet to place. Upcoming and postponed matches carry pre-match. odds.capturedAt
says when the prices were read. When matches in play come with closing prices only, the run
status says so and points to the in-play prices under Live matches.
Only matches with prices. Set onlyWithOdds and a match no bookmaker prices is neither
returned nor billed; the run checks up to twice as many candidates as maxMatches to fill the rest.
When the odds feed fails for a match and there is no fallback, odds is null, the record names
odds in unavailableParts and the odds are not billed.
Turn on includeOddsMarkets as well and the record adds over/under and Asian
handicap lines — the average price on each side and how many bookmakers stand behind
each line. Each block names its part
of the match in scope: of the two full-match variants, with and without overtime, the
one more bookmakers price. Tennis lines carry their unit, sets or games.
It all comes from the same response as the match-winner prices, so there are no extra
requests — on an ATP Shanghai match measured on 13 October 2026: 14 over/under and 13 handicap lines in games. A match with no total or handicap line
gets odds.markets null and is not billed for markets.
Every price, flat. Turn on includeOddsOffers for every price of every bookmaker as a
flat list in odds.offers: market, part of the match, selection, line from the home side,
opening and current price, movement since opening and whether it is still offered. A tennis match carries about 780 rows; filtered to match winner and totals for the full match it comes to about 200 rows (46 KB).
Filter by market (oddsOfferMarkets) and part of the match (oddsOfferScopes) to keep
records small. No extra request; charged once per match as match-odds-offers, only when
the list is not empty. The Every price view gives one row per price and Bookmakers one
row per bookmaker; the codes are listed under Output fields.
Live matches
Turn on includeLive and matches in play carry their current state: the score inside
the game being played, who is serving, and the running score — the actual 30-15
inside the current game, not just the set score.
Status comes in two fields: status is scheduled, live, finished, postponed,
canceled, abandoned (called off, no result) or interrupted
(stopped and not finished yet, so it can still resume and turn finished), and
statusDetail names the set in play — 1st set to 5th set, 1st set tiebreak — or how the match ended or stopped: retired, walkover, interrupted, suspended. An interrupted match has neither endedAt nor winner, even when one player leads on sets: it is not over yet. A match whose start is held back stays scheduled, with statusDetail
delayed or pending.
Live state costs one request for the whole run, not one per match. Once a match is over,
live is null: the live feed keeps a finished match for a few minutes, and that tail is
neither returned nor billed as live state. live.capturedAt says when the state was read.
The live feed is read after the daily list, and its stage wins: with includeLive or the
Live now filter, status, statusDetail and the clock follow it, so a match that ended in
between comes as finished, with its winner and endedAt, and the Live now filter leaves it out.
Only matches in play. Set matchStatuses to Live now and the run returns just the
matches in play — the Actor reads the live feed first and filters before charging you, so you
don't pay for a full day of fixtures to find the handful that are on. Measured on a tennis day: 12 live matches out of 115 scheduled.
Matches in play can only be on today's or yesterday's list, so the run
reads just those days whatever dateRange says; Finished alone skips the days ahead.
The filter on its own does not add the live state: turn on includeLive as well for
the score inside the current game and who is serving, billed as match-live.
In-play prices. includeLiveOdds adds odds.live to matches in play: the average and best
in-play match-winner prices with the bookmaker behind each best price (odds.live.home,
odds.live.bestHome, odds.live.bestHomeBookmaker...), how many bookmakers the averages cover
and how many take bets right now (odds.live.bookmakersCount, odds.live.activeBookmakers), and
each bookmaker's own prices (homePrice, drawPrice, awayPrice), up to 10 bookmakers, one request
per bookmaker. Each bookmaker row also carries its first in-play price (openingHome...: the first
live price, not the pre-match one) and where the price stands against it (homeDirection,
drawDirection, awayDirection: up above it, down below it, null when equal), and the averages
count how many bookmakers hold each price above or below their first in-play price
(odds.live.homeUpBookmakers, odds.live.homeDownBookmakers and so on). An ATP match in play on 13 October 2026 had live prices from 7 British bookmakers. It is part
of the odds charge.
The Live state view lists the matches in play with period, clock and live score.
Round of the match
The daily feed carries a stage name for some competitions (League phase, Play Offs).
Switch on resolveRounds and the round is read from the tournament itself: Final,
Semi-finals, 1/8-finals. The cost is per tournament in the run, not per match:
- Tennis reads the tournament draw: one request per draw (main draw and qualifying are
separate draws), shared with
includeSeeds. The draw lists matches in play too.
Input
{"dateRange": "custom","dayOffsets": [-1, 0, 1],"tennisTours": ["atp", "wta"],"includeStats": true,"includeH2H": false,"maxMatches": 200}
dateRange accepts presets (today, yesterday, yesterday-today, last-3-days, next-week, whole-window...);
with dateRange: "custom" the dayOffsets array is used instead: 0 today, -1
yesterday, 1 tomorrow. dateFrom and dateTo take calendar dates instead (see
Date ranges above).
Day boundaries follow a time zone, and you choose it. timezone takes a time zone name such as America/New_York or Europe/Berlin, with daylight saving applied, or an offset such as +05:30. Left empty, days follow UTC. With UTC days a US game that starts at 8 pm Eastern or later (7 pm in winter) lands on the next calendar day, so set America/New_York for the American game day. A match is kept only if it starts on one of the chosen days in that zone: the source's day feeds overlap the neighbouring days by about an hour, and without this cut a daily run would bill such a match twice. A match in play that started yesterday still counts as today's. The older timezoneOffset (whole hours, no daylight saving) works when timezone is empty; its 0 means this Actor's default, so ask for UTC days with timezone: "UTC".
The daily feeds reach seven days back and seven days ahead, but tennis order of play is published only two or three days out: on 13 October 2026 the last matches sat two days ahead, so next-week returns those days and the rest come back empty until the schedule appears. Offsets outside the window are dropped with a warning instead of
silently returning nothing.
tours keeps only competitions whose name contains one of the strings as whole words,
ignoring case, so Wimbledon, Shanghai, Wuhan, ATP all work. A whole tour is easier to pick with tennisTours (atp, wta, challenger, itf). Leave it empty for everything.
More filters for the daily list. teamFilter keeps only matches of the given players among the selected days — names as whole words, or the 8-character IDs from Flashscore links. startsWithinHours keeps matches that start from now until that many hours ahead, and days that cannot hold such matches are not read. These filters read nothing extra; for a player's history beyond the window use teams.
Order and maxMatches. maxMatches caps the run. When matches are picked by days (no league URLs, players or match IDs), main-draw singles come first, then main-draw doubles, then qualifying, each from Grand Slams through the ATP and WTA Finals, 1000, 500 and 250 events, the Davis Cup and Billie Jean King Cup, WTA 125 and Challengers down to ITF; inside each level the source's order stays. tours, the tennis filters, the status filter and dates narrow the set and keep this order.
The order covers only the days the run reads. Without filters, days are read four at a time, and reading stops once the days read hold maxMatches matches (with any filter every selected day is read). whole-window is read from today outward — today, yesterday, tomorrow, two days back, two days ahead and so on — so a small maxMatches gets the matches closest to today, and the far edges of the window are not read at all; the other presets are read day by day in calendar order. In monitorMode the order applies day by day, so a new fixture for tomorrow never pushes today's match out of the run, and maxMatches caps the matches checked: when there are more candidates, half of the checks go to matches in play, finals not output yet and the top of this order, the other half to the rest by rotation (see Monitor mode). League URLs, players and match IDs keep the source's order.
Run status, warnings and source failures
The run's status line says how the input was understood and how far the run got — for example "Reading days: 4 of 7 read, 3996 matches found" — and at the end what was output, with how many records carry each part, what maxMatches cut off and every warning about the input: a competition name with no match (with the closest names when it looks misspelt), a league URL that is not a league page or belongs to another sport, a team the search did not find. A run that comes back empty says why, and a run stopped by your spending limit says how much more the rest would need.
A failure of the source is kept apart from missing data. Days whose feed did not load, broken day feeds and league pages the source did not return are named in the status. So are the per-match feeds — statistics, summary, head-to-head, point by point, news, highlights, broadcasts, odds — when they fail: such a record lists the missing parts in unavailableParts and is not billed for them. When the first batch of days does not load at all, the run fails at once instead of retrying the rest of the window, and it is charged only the start and the run minimum of a failed run.
Long runs and limits. While matches are processed the status shows the progress and about how long the rest will take, and warns when that runs past the run's timeout. Shortly before the timeout (15% of the run's time, but no less than 25 and no more than 45 seconds, and no more than half of it) the run stops starting new work: records already on their way are finished and charged, the status says how many matches were output, RUN_SUMMARY lists the rest in notOutputMatchIds, and the run ends on its own before the timeout. A second run with those IDs as matchIds gets just them, and none of them was charged in the first run. When your maximum cost per run cannot cover every selected match, the run says so before it starts on them: about how many it covers and what all of them would cost.
When there is something to explain, the same goes into the RUN_SUMMARY record of the run's key-value store — Run summary in the Output tab: status (done, empty, stopped-early, or timing-out when the run is about to reach its timeout), scope, output, selected, unchanged, cutByMaxMatches, cutNotice, emptyReason, stopReason, warnings, sourceFailures, notOutputMatchIds, and runMinimumUsd with runMinimumNote (the run minimum charged in the run). A clean run writes none. A monitor run writes it only when it stopped early, part of the source failed or it came back empty for a reason other than no changes: its input is the same in every scheduled run, so its input warnings stay in the status and the log.
What a run actually costs
Billing is per event, and an event is charged only when the data actually exists. The
base event is $0.00078 per match, and every run starts with the apify-actor-start
event, $0.00005; the rows below add up the events each configuration actually fires:
| Run | What is billed | Per match |
|---|---|---|
| Default — the input form ships this way | match, stat sheet and summary | $0.00168 |
| Typical | plus odds and head-to-head at the default depth | $0.00198 to $0.00216 |
| Everything on | plus totals and handicaps, every bookmaker price, highlights, broadcasts, live state, point by point | $0.00418 |
Per 1 000 matches: scores only $0.78, the default settings $1.68, typical $1.98 to $2.16, everything on about $4.18. Apify's free plan includes $5 of usage a month (pricing): about 2 900 matches at the default settings, or 6 400 with scores only.
The last row is arithmetic on row-billed items, so treat it as an order of magnitude rather than a fixed price:
- head-to-head — $0.00001 a row, at most 30 billed rows a match ($0.0003); rows beyond that come free in the same record. The default — 10 rows per side plus all direct meetings — holds 10 to 28 rows for most matches, more for long rivalries. Ten tennis matches measured on 12 and 13 October 2026 came to 10 to 28 rows, the most for Tsitsipas – Fritz with 8 meetings. Setting
maxH2HRecordsto 0 takes a singles match to about 200 to 300 rows (308 for Tsitsipas – Fritz on 12 October 2026), still billed as 30. The row of the match itself is free and does not count. - point by point — $0.00003 a completed game or tiebreak; the game still in play is returned free. The three-set match measured for this table ran 30 games.
- every bookmaker price — $0.0005 a match, however many rows the list holds, and only when it is not empty.
- news — $0.00001 an article, and notable matches carry dozens. News is not counted in the table above.
Options you leave off cost nothing, and an option that returns no data is not billed at all: a fixture with no stat sheet, no odds and no box score is charged as a plain match, and a part that the source failed to deliver is not billed either. In monitor mode a changed match is billed only for the parts that changed (see Monitor mode above).
A spending limit is kept record by record. If you set a maximum cost per run, a match whose events no longer fit into what is left of it is not returned at all, so every record you receive is paid for in full and nothing arrives unbilled.
Run minimum. A run that reads a lot and returns little — a narrow filter over the whole 15-day window, a monitor run with nothing new, a competition between seasons — still uses compute, storage and traffic on the platform. Such a run is topped up with the run-minimum event, $0.00001 a unit, to a share of that usage at Apify's list prices (compute after the first 5 seconds, which Apify covers):
- a run that succeeds is topped up at the end to 1.5 times its usage;
- a run that fails, is aborted gracefully, or stops at or shortly before its timeout is topped up to 1.25 times its usage, which only covers the cost after Apify's 20% share. An abort without the graceful option (the default in Console and the API) stops the run at once, and it pays only what was charged along the way. A run still going 20 seconds before its timeout (half the time left in a short run) gets the top-up then, counting as paid only what the run will still charge in those seconds: records already on their way, and the matches its recent batches show it can still output at the base price — nothing while it is still reading the source, and for a monitor run only the records on their way. A run that finishes in time pays little or nothing for it, a run that the timeout cuts off pays it in full; whatever it charged is not refunded;
- a run longer than a minute charges part of it as it goes, after each batch of days, league, team or batch of matches, so that a crash halfway does not leave it unpaid. Matches already selected for output count toward it (a monitor run counts only what it has already output). This part is capped at 1.25 times the usage so far, counts toward the final top-up and is not refunded if later batches bring the data that would have covered the run;
- users on the Apify free plan never pay it, and neither do Standby requests, where you pay the platform usage directly.
Everything the run was charged, the start included, counts toward the minimum, so a run whose records cover its usage pays no final top-up, and a thin run usually comes to under half a cent in total. If you pick a residential proxy group yourself, its traffic counts at Apify's $8 per GB; the default datacenter proxy adds nothing of the kind. The run status names the amount and the reason (for example, from a local monitor run with nothing new: "Run minimum: $0.00008 was added, because the run's other charges ($0.00005) came to less than 1.5x the platform usage of this run (compute, storage and traffic, about $0.00009)"). The minimum never goes past your maximum cost per run: before each batch the run checks that the minimum for more work still fits and otherwise stops reading and says so, and a day feed that does not answer is not retried once the retry would no longer fit. The check runs between requests, so a single slow request can still take the run a little past what the limit covers; the top-up then stops at the limit.
How this compares
| Capability | This Actor | Other Store Actors (14 Oct 2026, Apify Free plan prices) |
|---|---|---|
| Price per match, scores only | $0.00078 | $0.00025 to $0.0075 a match on 15 other Actors; one more charges $0.008 a run for a whole tournament |
| Price per match with detail | $0.00168 at the default settings, $0.00418 with every option on | $0.00225 to $0.025 a match with statistics, point by point or odds on ten others; $0.008 a run for one match with details on one more |
| Small request (13-14 Oct 2026 build) | 6.1 s (3 ATP matches with odds and markets) | 2.2 to 11.1 s for 1 to 3 matches on ten other Actors |
| Match statistics | 17 to 21 metrics per match and per set | 17 to 20 per match and 16 to 19 per set on nine other Actors |
| Point by point | every point: server, winner, break, set and match points; tiebreaks point by point with mini-breaks | returned by 11 other Actors: one without point marks, two without set and match points, one with a tiebreak as a single game |
| Head-to-head with a form summary | all meetings and 10 past matches per player by default, up to about 155; cut at the match date; form, rest days, record per surface | returned by seven other Actors, up to 50 rows per block where the depth is stated |
| Ranking, seed and tournament level | ATP or WTA rank and points, seeds (WC, Q, LL, PR, Alt), 15 levels | a ranking on 13 other Actors, in the match record on nine of them; seeds on nine |
| Filters by surface, tour and level | yes, combined with AND | one tennis scraper filters by surface and draw type, another by match type, gender and tournament level |
| Bookmaker odds | up to 20 British bookmakers; 17 countries to pick and merge; opening, current and closing; every price on request | 20 British bookmakers on three Flashscore-based Actors, two of them with a country choice; one country of five on one; ten countries a run on one |
| Monitor mode: pay only for changes | yes, billed only for the parts that changed | change-only modes on two other Actors |
| Standby HTTP API | yes, up to 100 matches a request | one other Actor |
| Proxies | datacenter, no extra cost | residential on three SofaScore-based Actors |
How the data is checked
- Retirements and walkovers are marked.
statusDetailisretiredorwalkover, and head-to-head rows carry the same instatus, so an unfinished match is not counted as an ordinary win. - Doubles ids match the names. The source's feeds disagree on the order of the two player ids of a pair: the daily feed lists them in reverse against the names, the player feeds in name order. The Actor reads each feed its own way, so
partner.idbelongs topartner.name. - Point-by-point totals are checked against the source. On 756 finished matches of a 13 October 2026 run that have both point-by-point and statistics,
pointByPointTotalsmatched Flashscore's own Total Points Won exactly on 641. 78 are flaggedpointsComplete: falsebecause the source sequence breaks the scoring rules. On the other 37 the source's point-by-point and statistics feeds disagree by 1 to 9 points (1 to 3 on 32 of them). - Structural invariants: a sweep on 29 August 2026, on an earlier build, checked 140 matches of tennis, football, basketball and baseball: percentages never above 100, event minutes that parse as numbers (football's with added time), a side on every event, attendance inside a plausible range and a match-level slice whenever a stat sheet exists. Zero violations.
- Odds are joined by the shared match id and participant ids, never guessed from names and kickoff times, so a price cannot land on the wrong fixture or team.
Speed and compute
Measured on the Apify platform on 13 and 14 October 2026 with 1024 MB of memory; the times include starting the container. Rows with a build number ran on that build on 14 October; the others ran on earlier builds, which did not yet read the whole window from today outward or write the records of a batch in the order the matches were selected. Builds after 1.0.3 were not timed: they start Node directly without the crawlee package and set up the proxy only when a request needs it. Take the times as the order of magnitude. Compute units are what a run uses on the platform. In a regular run you do not pay for them on top of the events, except through the run minimum of a thin run; in Standby you pay the platform usage directly:
| Run | Matches | Time |
|---|---|---|
| Three ATP matches of the day with odds and markets | 3 | 6.1 s |
| The Store test input on build 1.0.3: the whole window, the form as it ships | 20 | 8.3 s |
| Every tennis match in the 15-day window, every option on (Apify free plan) | 2 813 | 1163 s |
Each detail option adds about one request per match, and requests run in parallel, so the time grows slower than the number of matches. The data is billed per event, not per second.
No browser, no API key, plain HTTP requests. The default Apify datacenter proxy is enough, so no residential-proxy cost ends up in the price.
Output fields
The dataset schema of this Actor (its fields section, which the API returns with the run's dataset and MCP clients and code generators read) describes every field with its type and meaning, many of them with an example. Every type can also be null. Percentages run from 0 to 100 (homePercent, margin, bestPriceOverround), probabilities from 0 to 1 (impliedHome, impliedDraw, impliedAway); odds.changePct is a change in percent and can be negative or above 100. Times are ISO 8601 in UTC (startsAt, scrapedAt, capturedAt), except startDate and startTime, which are in the run's time zone.
Top-level fields
| Field | Type | Meaning |
|---|---|---|
matchId | string | Flashscore match ID, 8 characters. Stable across runs: the key to deduplicate, update and join records. |
sport | string | Sport slug, such as football, tennis, basketball, hockey, baseball, handball or table-tennis. |
startDate | string | Start date in the run's time zone (see timezone), YYYY-MM-DD. |
startTime | string | Start time in the run's time zone, HH:MM; null when startTimeKnown is false. |
startTimeKnown | boolean | False for an archive record the source keeps without a start time, only the day (a page older than a year with at least 90% of its games at midnight in Europe or UTC): startTime is then null and startDate is the day of the match, while startsAt and startTimestamp keep the source's placeholder time. True for the daily feeds and the past year. |
timezone | string | Time zone of startDate and startTime: the timezone input or this Actor's default, as an IANA name or a UTC offset. |
homeTeam | string | Name of the home side (player 1 in tennis); the same as home.name. |
awayTeam | string | Name of the away side (player 2 in tennis); the same as away.name. |
homeScore | number | Score of the home side as a number: sets won in tennis and other set sports, goals, points or runs elsewhere, without a penalty shoot-out. Null before the start. |
awayScore | number | Score of the away side as a number, as homeScore. |
tournament | string | Competition as the source writes it: COUNTRY: League - Stage; in tennis TOUR - EVENT: Tournament (Country), surface. |
tournamentName | string | Short name of the competition: without country and stage, in tennis without tour and surface. |
tournamentId | string | Stable ID of the competition at the source: the key to its season archives. |
tournamentSeasonId | string | ID of the competition season, under the same name in the League Standings Actor. Null where the competition has no seasons. |
tournamentStageId | string | ID of the season stage, under the same name in the League Standings Actor. |
tournamentLogoUrl | string | URL of the competition logo. |
tournamentCountry | string | Country or region of the competition as the source names it. |
categoryId | number | The source's numeric ID of the section the competition belongs to: a country for team sports, a category such as ATP - Singles in tennis. |
tour | string | Tennis tour as the source writes it: ATP - SINGLES, WTA - DOUBLES, CHALLENGER MEN - SINGLES, ITF WOMEN - SINGLES and so on. |
isQualification | boolean | True for a qualifying match. |
tennisTier | string | grand-slam, atp-1000, atp-500, atp-250, atp-finals, wta-finals, wta-1000, wta-500, wta-250, wta-125, challenger, itf, davis-cup, billie-jean-king-cup or other, from the 2026 ATP and WTA calendars. |
bestOf | number | Match format: 5 for men's Grand Slam singles, otherwise 3 in tennis; frames or legs as the source gives them in snooker and darts. |
matchType | string | singles or doubles, where the sport has pairs. |
surface | string | Court surface: hard, clay, grass or carpet, with indoor where the source says so. |
round | string | Round or stage: from the daily feed where it carries one (League phase, Final), otherwise only with Match round (resolveRounds). |
tournamentUrl | string | Page of the competition at the source. |
startsAt | string | Scheduled start, ISO 8601 in UTC. |
startTimestamp | number | Scheduled start, Unix seconds. |
endedAt | string | When the match ended, ISO 8601 in UTC: only for finished and abandoned matches. |
status | string | scheduled, live, finished, postponed, canceled, abandoned (called off, no result) or interrupted (stopped, may still resume). |
statusDetail | string | Period in play (1st half, 2nd set, 3rd period) or how the match ended or stopped (after extra time, after penalties, retired, walkover, awarded, delayed). |
home | object | Home side (player 1 in tennis): name, IDs, links and, in tennis, ranking and seed. |
away | object | Away side (player 2 in tennis), as home. |
score | object | Final or current score: sets won in set sports, goals, points or runs elsewhere; after a shoot-out the score of play without it. |
scoreText | string | Ready-made score: "6-7(3), 4-6" in tennis, "2-1" or "1-1 (pen. 4-3)" elsewhere, " ret." after a retirement, "w/o" for a walkover. |
setScores | array of object | Score of every set with its number; in tennis with tiebreak points (null when the set had no tiebreak). |
regulationScore | object | Score after regulation time (90 minutes, three periods, nine innings); differs from score after overtime or a shoot-out. |
note | string | The source's note: first-leg result, series standing, postponement reason. |
winner | string | home or away; null for a draw, an unfinished or interrupted match. |
winnerName | string | Name of the winner as the source writes it. |
matchUrl | string | Page of the match at the source. |
odds | object | Match-winner odds with averages, best prices and every bookmaker; markets, every price and in-play prices with their options. Needs Match-winner odds (includeOdds). |
live | object | State of a match in play; null otherwise. Needs Live state (includeLive). |
pointByPoint | array of object | Every game (in tennis) or period with the running score point by point. Needs Point by point (includePointByPoint). |
pointByPointTotals | object | Totals over completed games: points, breaks, break points faced, mini-breaks. Tennis, with Point by point. |
summary | object | Venue, attendance, referees, score and duration by period, and the event timeline. Needs Match summary (includeSummary). |
stats | object | Team statistics per match and per period. Needs Match statistics (includeStats). |
h2h | object | Recent matches of each side, direct meetings and a form summary per side. Needs Head-to-head history (includeH2H). |
news | array of object | Articles linked to the match. Needs Match news (includeNews). |
highlights | array of object | Video highlight links, after the match. Needs Video highlights (includeHighlights). |
broadcasts | array of object | TV channels and streams listed by the source. Needs TV and streaming listings (includeBroadcasts). |
scrapedAt | string | When the record was output, ISO 8601 in UTC. The later of two records of one match is the newer one. |
unavailableParts | array of string | Parts that were asked for but did not arrive because the source failed (stats, summary, lineups, playerStats, pointByPoint, h2h, news, highlights, broadcasts, odds); they are not billed. odds is also listed when one of several oddsCountries did not answer: the record then carries the other countries' bookmakers, and its odds are not billed. Absent when nothing failed. |
unavailablePartsText | string | unavailableParts joined with commas, for one spreadsheet column (the Matches and Changes views). Absent when nothing failed. |
changeType | string | new or changed: why the monitor emitted the match. Monitor mode only. |
monitorChangedFields | array of string | Groups that changed since the monitor last emitted the match: score, status, schedule, odds, stats, summary, lineups and others. Null for a new match. Monitor mode only. |
monitorChangedFieldsText | string | monitorChangedFields joined with commas, for one spreadsheet column. Null for a new match. Monitor mode only. |
Fields of the nested parts (their descriptions are in the dataset schema):
homeandaway:name,slug,id,country,code,imageUrl,countryId,url,currentRank,currentRankPoints,currentRankDate,seed,seedNumber,entryType,partnerodds:home,draw,away,bestHome,bestDraw,bestAway,bestHomeBookmaker,bestDrawBookmaker,bestAwayBookmaker,openingHome,openingDraw,openingAway,bookmakersCount,margin,impliedHome,impliedDraw,impliedAway,homeAmerican,drawAmerican,awayAmerican,bestHomeAmerican,bestDrawAmerican,bestAwayAmerican,bestPriceOverround,arbitrage,priceState,capturedAt,market,scope,source,country,countries,liveBookmakerIds,bookmakers,movement,markets,offersCount,offers,live,previous,changePctodds.bookmakers[](the Bookmakers view):bookmaker,bookmakerId,homePrice,drawPrice,awayPrice,openingHome,openingDraw,openingAway,active,country,homeAmerican,drawAmerican,awayAmericanodds.offers[](the Every price view):bookmakerId,bookmaker,country,market,scope,selection,line,unit,price,opening,movement,activeodds.markets.overUnder[](the Totals view):line,over,under,unit,bookmakers,scopelive:currentPoints,serving,liveScore,minute,minuteNumber,addedTime,period,clockStale,capturedAtodds.live:market,scope,home,draw,away,bestHome,bestDraw,bestAway,bestHomeBookmaker,bestDrawBookmaker,bestAwayBookmaker,homeAmerican,drawAmerican,awayAmerican,bestHomeAmerican,bestDrawAmerican,bestAwayAmerican,bookmakersCount,activeBookmakers,homeUpBookmakers,homeDownBookmakers,drawUpBookmakers,drawDownBookmakers,awayUpBookmakers,awayDownBookmakers,bookmakers,fetchedAt- rows of
stats.scopes[].groups[].items[](the Statistics view):name,homeText,awayText,isHeadline,key,homeValue,awayValue,homePercent,homeMade,homeAttempts,awayPercent,awayMade,awayAttempts - rows of
h2h.homeRecent[],h2h.awayRecent[]andh2h.meetings[](the Head-to-head view):section,playedAt,meetingId,surface,meetingTournament,country,homeName,awayName,result,score,scoreRegulation,penalties,url,tournamentCode,homeId,awayId,homeSlug,awaySlug,homeCountry,awayCountry,homePartner,awayPartner,meetingHomeScore,meetingAwayScore,scoreAfterExtraTime,meetingWinner,meetingStatus,resultDetail,side,advanced,stageWon,tabs,isThisMatch,isAfterThisMatch
Market and part codes of odds.market, odds.scope and the price list (oddsOfferMarkets, oddsOfferScopes); a selection is home, draw, away, over, under, yes, no, a score such as 2:1 or a pair such as home/draw:
| Market | Meaning |
|---|---|
HOME_DRAW_AWAY | Match winner with draw (1X2) |
HOME_AWAY | Match winner, no draw |
OVER_UNDER | Over/Under |
ASIAN_HANDICAP | Asian handicap |
EUROPEAN_HANDICAP | European handicap |
DOUBLE_CHANCE | Double chance |
DRAW_NO_BET | Draw no bet |
CORRECT_SCORE | Correct score |
HALF_FULL_TIME | Half-time/Full-time |
ODD_OR_EVEN | Odd/Even |
BOTH_TEAMS_TO_SCORE | Both teams to score |
TO_QUALIFY | To qualify |
NEXT_GOAL | Next goal |
TOP_POSITION_MERGED | Top position |
TO_WIN_AND_TOP_POSITION | To win and top position |
WIN_EACH_WAY | Win each way |
| Part | Meaning |
|---|---|
FULL_TIME | Full time |
FULL_TIME_OVER_TIME | Full time including overtime |
FIRST_HALF | 1st half |
SECOND_HALF | 2nd half |
FIRST_PERIOD | 1st period |
FIRST_QUARTER | 1st quarter |
FIRST_SET | 1st set |
SECOND_SET | 2nd set |
Tennis API: call it from code
The API tab of this page has ready code with this Actor's full name and your token; below,
<owner> stands for the owner part of that name. Called without maxMatches and dateRange, the
API takes up to 200 of today's matches with statistics and summary, as the input schema defaults
say, while the form starts with 20 over the whole window: set both in code. maxTotalChargeUsd
caps what a run may charge you, and a run that ends without SUCCEEDED has its reason in
statusMessage. A synchronous call waits for the run and returns the dataset items; a run longer
than 300 seconds answers with 408, so keep synchronous calls small and use a client library for big
runs. Ask the dataset for the fields you need (fields) or a view (view) to keep responses small.
curl -X POST "https://api.apify.com/v2/acts/<owner>~tennis-scraper-atp-wta/run-sync-get-dataset-items?maxTotalChargeUsd=0.50&fields=matchId,startDate,homeTeam,awayTeam,homeScore,awayScore,status" \-H "Authorization: Bearer $APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"dateRange": "yesterday", "tennisTours": ["atp"], "includeOdds": true, "maxMatches": 20}'
Python, with apify-client:
from decimal import Decimalfrom apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("<owner>/tennis-scraper-atp-wta").call(run_input={"dateRange": "yesterday", "tennisTours": ["atp"], "includeOdds": True, "maxMatches": 20}, max_total_charge_usd=Decimal("0.50"))status = getattr(run.status, "value", run.status) if run else Noneif status != "SUCCEEDED":print("Run did not succeed:", status, run.status_message if run else "")else:for item in client.dataset(run.default_dataset_id).iterate_items(fields=["matchId", "startDate", "homeTeam", "awayTeam", "homeScore", "awayScore", "status"]):print(item)
JavaScript, with apify-client:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('<owner>/tennis-scraper-atp-wta').call({"dateRange": "yesterday", "tennisTours": ["atp"], "includeOdds": true, "maxMatches": 20}, { maxTotalChargeUsd: 0.5 });if (run.status !== 'SUCCEEDED') {console.error('Run did not succeed:', run.status, run.statusMessage);} else {const { items } = await client.dataset(run.defaultDatasetId).listItems({ fields: ['matchId', 'startDate', 'homeTeam', 'awayTeam', 'homeScore', 'awayScore', 'status'] });for (const item of items) console.log(item);}
Use it from an MCP client
Apify's MCP server at https://mcp.apify.com exposes Store Actors as tools to MCP clients
such as Cursor, VS Code or GitHub Copilot. The client can find this Actor with the
search-actors tool and run it with call-actor, or you can pin it as a tool of its own:
https://mcp.apify.com?tools=<owner>/tennis-scraper-atp-wta, with the full name from the API tab. Sign in
with OAuth or send your Apify token as a Bearer header. The server skips Actors that need full
account access; this one does not need it. The dataset schema describes every output field for the
agent (see Output fields). Details: Apify MCP server.
Standby HTTP API
The Actor also runs in Apify Standby mode: send a GET request and the matches come back as JSON
in the response. Take the address from the Endpoints tab of this Actor's page and send your
Apify token in the Authorization: Bearer header. After a quiet spell a request can take a few
seconds more while the platform starts a Standby run, and if no run becomes available within 2
minutes the platform answers 429 (Apify docs).
GET <address from the Endpoints tab>/?dateRange=today&maxMatches=25
Query parameters mirror the input fields, within these limits per request: maxMatches 25 by
default and 100 at most, one league URL, three teams and two history pages per team.
monitorMode, monitorName (or its older name monitorKey), outputDatasetName and
proxyConfiguration work only in regular runs: a request with them returns every selected match
and names them in warnings. Lists (tours, teams, matchIds, oddsCountries,
matchStatuses, surfaces, tennisTours, tennisTiers and others) take a repeated key (tours=A&tours=B), a JSON array
(tours=["A","B"]) or commas; a value with a comma inside needs one of the first
two, except a full tournament name ending in its surface, as the tournament field writes it (tours=ATP - SINGLES: Shanghai (China), hard), which also works alone. A misspelt parameter
or an invalid value answers 400 with
{"error": {"type", "parameter", "message", "allowed", "runId"}}What a request costs. Every response bills every match it returns, as a regular run would: there is no monitor in Standby. On top of the events you pay the platform usage of the Standby run ("you pay the platform usage costs of the Standby runs in addition to the event charges", Apify docs): with this Actor's Standby settings it runs with 1 GB of memory and stays up for 300 seconds after the last request. The run minimum is not charged in Standby. A poll for in-play prices:
GET <address>/?matchStatuses=live&includeLive=true&includeOdds=true&includeLiveOdds=true&includeStats=false&includeSummary=false
costs $0.00118 a match per request (result, live state and odds); 10 matches every 30 seconds for two hours, 240 requests, come to $2.83 in events plus the platform usage.
The response is JSON with count, tookMs and matches (records in the same shape as the
dataset), plus warnings when something was trimmed, ignored or came back empty. When fewer
matches arrive than were selected, the response adds truncated: true, selected (how many
were selected for output) and reason: charge limit reached (the run's maximum total charge is
spent), charging unavailable or some matches could not be processed. With onlyWithOdds,
selected counts the slots, up to maxMatches, and scanned how many candidates were checked
for odds; when fewer matches with odds were found than maxMatches while unchecked candidates
remained, the reason is not enough matches with odds among those checked. Once the charge limit
is spent, every further request answers at once, without scraping, with count: 0,
reason: "charge limit reached", a warning and an empty matches list. When part of the source
did not answer, the response adds sourceFailures: the days whose feed did not load (days),
broken day feeds (brokenDays), league pages (leagues), parts of records by name and count
(parts) and matches skipped on an error (matches); the records themselves name the missing
parts in unavailableParts. When no requested day could be read, the request answers 502.
Schedules, webhooks and integrations
Everything the Apify platform offers around a run works here as for any other Actor:
- Schedules run the Actor or a saved
task on a cron expression with a fixed input. Pair a schedule with
monitorModeand each run delivers only what changed since the last one; set the schedule'sisExclusiveoption so a new run does not start while the previous one is still going. - Webhooks call your URL when a run succeeds or fails, so a pipeline can pick up the dataset the moment it is ready.
- Google Drive uploads a file of the results after each successful run (see the FAQ for a Google Sheet).
- Slack posts run status to a channel; Zapier, Make and n8n pass the results to other apps without code; Airbyte loads datasets into a warehouse.
Cron expressions for the usual jobs (UTC; the schedule takes a time zone of its own):
| Job | Cron | Input |
|---|---|---|
| Yesterday's results every morning | 0 6 * * * | dateRange: "yesterday" |
| Results and fixtures every hour with monitor mode | 0 * * * * | dateRange: "yesterday-today", monitorMode: true |
| Matches in play every 5 minutes | */5 * * * * | matchStatuses: ["live"], includeLive: true, monitorMode: true |
| The week's odds every Monday | 0 7 * * 1 | dateRange: "next-week", includeOdds: true |
Good for
- Betting and trading models — serve and return splits per set next to the odds of the same match.
- Player form tracking — head-to-head plus recent matches with surface for both sides.
- Match previews — fixtures with tournament, surface, level and round already parsed out.
Limits
- Odds follow the bookmaker market. Upcoming, live and finished matches carry prices — finished ones keep the closing line. Minor divisions and fixtures far
ahead may carry no prices at all — that is the market, not the scraper. Each country has
its own licensed bookmakers, so the list changes with
oddsCountries(pick several to merge them). - A fallback odds source covers the gaps, with less detail. When the main odds feed has
no price for a match, the Actor tries BetExplorer for tennis. Those records carry
the market average and best price only — no per-bookmaker table and no markets — and from
Apify's US servers that source shows far fewer bookmakers.
odds.sourcetells which one you got. - Stats exist only for matches that have started. Fixtures for tomorrow return teams and kickoff time but an empty stat sheet — that is the source, not the scraper.
- Coverage follows Flashscore itself: strong for top competitions, thinner for minor ones.
- Rankings are current, history is not.
currentRankis the ranking on the day of the run, not on the day of the match, while head-to-head of a past match stops at that match. - Tournament levels follow the 2026 ATP and WTA calendars. For older seasons four category changes are accounted for: Doha (ATP 250 until 2024), Dallas (ATP 250 until 2025), Seoul (WTA 500 in 2024 and 2025) and Singapore (WTA 250 until 2025). Any other tournament gets its 2026 level in every season, or its last level if it is no longer on the calendar.
- Head-to-head of older matches is thin. The source publishes recent form as of today, and rows played after the match are dropped, not replaced, so the older the match, the less recent form it keeps. Direct meetings played before the match stay.
FAQ
Can I get ATP and WTA point-by-point data through an API?
Yes. Turn on includePointByPoint: every game and tiebreak comes with its full point sequence and, for every point, who served, who won it and whether it was a break, set or match point. includeStats (on by default) adds aces, double faults, serve percentages, break points, winners and unforced errors per match and per set.
Can I call it as a tennis API from Python or JavaScript? Yes. Every Actor on Apify is also an HTTP API: a synchronous call returns the matches as JSON in one request, and the Apify clients for Python and JavaScript run bigger loads; see Tennis API: call it from code. Results also export to CSV and Excel.
Do I need a Flashscore account, an API key or residential proxies? No. The Actor reads public feeds with plain HTTP requests, and the default Apify datacenter proxy is enough.
How far back and ahead does it go?
The daily feeds cover seven days back and seven ahead, though tennis order of play appears only two or three days out. Older matches come through leagueUrls (season archives), teams (a team's or player's history) or matchIds, which finds a single match on any date; and dateFrom with tours reads the season archive of the competition named (see Date ranges).
Why do some matches come without statistics, lineups or odds?
For two reasons. Flashscore publishes these parts only for some competitions, and bookmakers do not price every fixture: a part that does not exist is not billed, and onlyWithOdds leaves unpriced matches out altogether. Or the source failed to deliver the part during the run: it is not billed either, and the record names it in unavailableParts, odds included (see Run status, warnings and source failures).
Why did my run return nothing, or fewer matches than I expected?
The run status says why: no matches on the chosen days, a competition name with no match (with the closest names when it looks misspelt), a league URL that is not a league page, the maxMatches cut, the spending limit, the timeout, or days and parts the source did not deliver. The same explanation goes into RUN_SUMMARY in the run's key-value store (Run summary in the Output tab).
How do I get a spreadsheet?
In the run's Output tab press Export, set View to Matches (or another table) and the format to Excel. The default All fields keeps the score and teams in CSV and Excel, but scattered among about 2,000 columns and with most nested data cut off. Through the API add view=overview&format=xlsx to the dataset items URL; see Getting a spreadsheet.
How do I get the results into a Google Sheet? Once: download the Excel file as above and open it with Google Sheets. After every run: the Google Drive integration, on the Integrations tab of the Actor or of your saved task, uploads the results to your Drive after each successful run, and Zapier or Make can pass them on to Google Sheets.
How do I keep a dataset current without paying twice for the same match?
Schedule the Actor with monitorMode on: each run emits only new and changed matches, and changeType and monitorChangedFields say what changed. A changed match is billed the base event plus only the parts that changed, and row-billed parts only for rows not paid for before. Keep the latest record of each matchId, and fill the parts it names in unavailableParts from the earlier record (see Regular loads).
What happens when a run reaches my spending limit?
When the limit cannot cover every selected match, the run says so first: about how many matches it covers and what all of them would cost. A match whose events no longer fit into what is left of the limit is not returned, so you never receive a record that was not paid for in full. The run status names the limit and how much more the rest would need, and RUN_SUMMARY lists the IDs of the matches not output (notOutputMatchIds): pass them as matchIds to get just those.
Which time zone are the dates in?
startDate and startTime are in the run's time zone, named in timezone (empty input means UTC in this Actor), and that zone also decides where one day ends for dateRange, dateFrom and dateTo. The other timestamps (startsAt, startTimestamp, scrapedAt) are UTC.
What does a typical match cost? $0.00168 at the default settings; the cost table above shows the other configurations and the price of 1 000 matches. Options you leave off cost nothing.
Why does a run that returned little show a run-minimum charge?
Reading the source costs compute, storage and traffic even when your filters keep two matches or none. Such a run is topped up to 1.5 times what it used on the platform, or 1.25 times when it fails, is aborted gracefully or reaches its timeout; see Run minimum under the costs. The run status names the amount. A run whose records cover its usage pays no final top-up, a run longer than a minute may have paid part of it along the way, and users on the Apify free plan and Standby requests never pay it.
Will the output change under my pipeline?
This is version 1.1. A removed field, a field renamed without its old name still working, or a changed default comes with a new major version and a line in the changelog on the Information tab. To stay on one build, pass its build number (such as 1.0.3, shown in the run's details) as build when you start a run through the API.
Related Actors
- League Standings & Top Scorers Scraper — ATP, WTA — ATP and WTA rankings, and league tables with top scorers for team sports.
- Tennis Player Career Scraper — Serve Stats, Surface Splits — a player's career match by match, with serve and return statistics.
- The same record for other sports: Football & Soccer Scraper — xG, Live Scores, Odds; Basketball Scraper — NBA, EuroLeague, Box Scores; Ice Hockey Scraper — NHL, KHL Box Scores & Odds; Baseball Scraper — MLB Box Scores, NPB, KBO, Odds; Multi-Sport Scraper — Table Tennis, Handball +21.
Support
Open an issue in the Issues tab of this Actor's page. Paste the link to the run (from the run's page in Console): an Actor issue with a run link shares that run, its input, log and storages with the developer automatically, and nothing else in your account (Apify docs). Say what you expected and what you got instead. Do not post your API token or any other secret.
Disclaimer
This is an unofficial Actor. It is not affiliated with, endorsed by, or sponsored by Flashscore or any league, club or competition named here. All trademarks belong to their respective owners. The Actor reads publicly available pages and returns the data they already show to any visitor.