Tennis Scraper — Matches, Point-by-Point, Odds, Rankings
Pricing
from $2.50 / 1,000 delivered match or rankings
Tennis Scraper — Matches, Point-by-Point, Odds, Rankings
Flashscore tennis (global, flashscore.com): matches, live and final scores, set and tiebreak scores, point-by-point, match statistics, odds, head-to-head and ATP/WTA singles rankings. By day, tour, tournament or match ID; new-matches-only mode. Pay per result, no login, 20 free previews.
Pricing
from $2.50 / 1,000 delivered match or rankings
Rating
0.0
(0)
Developer
architek.earth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
14 hours ago
Last modified
Categories
Share
Recorded run: 10 ATP ranking rows from 1 ranking list in 13 seconds (2026-10-22, build 0.1.41, run PktYJGWJShCV3h0Oc). A bounded canary sample, not a full-catalogue benchmark.
architek.earth — an independent data studio. Pay-per-result scrapers with recorded runs, published canaries and stable schemas, built for people and AI agents.
Flashscore tennis matches, scores, point-by-point history, statistics, odds and ATP/WTA singles rankings from flashscore.com, by day, tour, tournament or match ID.
| Source | Market | From | Standout fields |
|---|---|---|---|
| Flashscore (flashscore.com) | ATP, WTA, Challenger, ITF, doubles | $2.50 / 1,000 rows | score.sets with tiebreaks, pointByPoint, statistics, odds, rank / points |
✅ Why choose this scraper
Tennis results and scores for any day in a 15-day window
Match rows carry homePlayer, awayPlayer, score.sets (with tiebreak points), homeSets / awaySets, status, startTime, tournamentName and matchUrl for any day from 7 days back to 7 days ahead (dayOffsets or dateFrom / dateTo).
Point-by-point, match statistics, odds and head-to-head per match
matchDetails mode returns one row per match with pointByPoint (game-by-game point scores), statistics (aces, double faults and the other stats Flashscore lists), odds (home/away prices with opening and active), oddsComparison and h2h, plus a sectionStatus flag for each section.
ATP and WTA singles rankings
Ranking rows give rank, previousRank, points, tournamentsPlayed, playerName, playerId, country and the source date asOf, up to the top 100 per tour.
New matches only, on a schedule
With incrementalMode and a stateKey, a run delivers only match IDs it has not delivered before (changeType: "new"); add emitChanges to also receive changed scores (changeType: "changed"). Unchanged rows are skipped and not charged.
Measured on a schedule
A canary task reads the ATP top 10 every day and a second one reads yesterday's match list; the hero line above is the latest green ranking run.
⚠️ Worth knowing up front
- Flashscore is the only source. No account, cookies or proxy are used by default. Sofascore and TennisExplorer are not sources.
- Match lists come from Flashscore's day feeds at offsets −7 through +7, counted from UTC today. A feed can carry carryover matches or start times on adjacent UTC dates, so offsets and dates select feeds, not strict start-time filters. Future feeds may be empty. Older results are not reachable through match lists.
- Live data is polled, not streamed. On 2026-10-20, ten live-match polls all returned HTTP 200 with a median request time of 1.69 seconds; the delay between the court and the feed has not been measured.
- Detail coverage varies by competition. A requested section the source does not publish is marked
NO_DATAinsectionStatus, never invented. Player names and tournament identity on a detail row can stay null when the match is not in an accessible day feed; checkidentityStatus. - Odds are scoped to a source bookmaker and region: the overview uses one bookmaker's home/away full-time market, and
oddsComparisonuses the observed US-NY route (oddsRegion).activeis kept verbatim; an inactive or historical quote is not an executable price. - Point-history markers are kept as source markers; their meanings are not guessed. Participant IDs in statistics are event participant IDs, distinct from player IDs.
status: "unknown"keeps unverified source status codes instead of labelling them finished or cancelled. Venue, umpire, momentum, doubles rankings and player biographies are not offered. - Requests run at most two at a time, at least 500 ms apart. A run stops cleanly 120 seconds before its timeout (6 hours at most), at your spending limit (
BUDGET_LIMIT), atmaxItems(LIMITED) or on an abort or migration, keeping what it delivered. A run with failed or blocked source requests endsPARTIAL, with the rows it did deliver. - When a source request for one input (a day feed, a ranking list or a match ID) fails, is blocked or returns nothing usable, the dataset gets a free status row (
"recordType": "status") withstatusSOURCE_ERROR,BLOCKED,NOT_FOUNDor (for a match with none of the requested sections)NO_DATA; a run stopped before its timeout gets one withACTOR_DEADLINE, and input the actor cannot use gets one withINVALID_INPUT(the run still ends cleanly). The spending limit and a reconciliation stop write no status row: they are reported in theRUN_SUMMARYrecord and the run's status message (for exampleDone: 10 rows, 0 details, 0 status rows (COMPLETE)). An empty day feed ends with no rows and no status row. - A dataset write and a charge are separate platform operations. If either acknowledgement is uncertain, the run stops with
RECONCILIATION_REQUIREDand never repeats the write or charge; the next run with thatstateKeyreconciles it or stops for a human.
⚡ Try the example
Each Apify account gets 20 free preview runs: one day's match list, up to 10 basic match rows per run, no details or saved state. A failed or empty preview still uses an attempt, and a preview never switches to paid mode.
Open the actor's input page, switch to JSON, paste this and start the run:
{"mode": "matches","billingMode": "preview","dayOffsets": [0],"maxItems": 10}
Set "billingMode": "paid" and raise maxItems for full day lists, or switch mode to matchDetails with matchIds for point-by-point, statistics and odds.
⚙️ Input
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | select | matches | matches (day lists), matchDetails (enriched matches by ID or URL) or rankings (ATP/WTA singles). |
billingMode | select | preview (prefill paid) | preview (free, matches mode only, one day, up to 10 rows, 20 per account) or paid. Required. |
maxItems | integer | 100 (prefill 3) | Data rows per run, 1–10,000; capped at 10 in preview. Unchanged incremental rows and status rows do not count. |
dayOffsets | array | [0] | Matches mode. Integers −7 to +7, relative to UTC today. Do not combine with dateFrom / dateTo. |
dateFrom / dateTo | string | — | Matches mode. YYYY-MM-DD within seven days of UTC today; selects day feeds, not strict start times. |
tours | array | [] (all) | Matches mode. Any of ATP, WTA, Challenger, ITF, Doubles; a row matching any one is kept. |
live | boolean | false | Matches mode. Only matches the source marks as in progress. |
tournamentIds | array | — | Matches mode. Flashscore tournament IDs (tournamentId on match rows), not names. |
matchIds / matchUrls | array | — | matchDetails mode. Eight-character Flashscore match IDs or flashscore.com/match/tennis/ URLs, up to 100 each, de-duplicated. |
statistics / pointByPoint / odds / h2h | boolean | true | matchDetails mode. Sections to request; at least one must be on. |
tour | select | ATP | Rankings mode. ATP or WTA singles. |
topN | integer | 100 | Rankings mode. Ranking rows, 1–100. |
incrementalMode | boolean | false | Needs stateKey. Delivers only IDs not delivered before under this key. |
emitChanges | boolean | false | Needs incrementalMode. Also delivers a row when a seen match or ranking changed. |
stateKey | string | — | Letters, digits, - and _, up to 80. Holds the baseline and saved progress in your account; tied to the inputs it was first used with. |
continuePreviousRun | boolean | false | Needs stateKey. Resumes an unfinished collection with the same inputs. |
proxyConfiguration | proxy | none | Optional. No proxy is used by default. |
feedback | string | — | Optional note for the maintainers. It is logged and read by a human, and it never changes output. |
Unknown input fields are rejected as invalid input.
🧱 Stable fields
These top-level fields keep the same name and the same kind of value in every release. Fields are only ever added, never renamed or removed, so an n8n, Make, Zapier or spreadsheet pipeline can bind to them.
| Field | Type | Meaning |
|---|---|---|
matchId | string | Eight-character Flashscore match ID on match and detail rows; the join key between them. |
source | string | Always flashscore. |
status / statusCode | string | Normalised match state (scheduled, live, finished or unknown) and the original source code. |
homePlayer / awayPlayer | object | id, name, country and eventParticipantId; values the source does not supply stay null. |
score | object | sets (per-set games, with tiebreak points when played), homeSets and awaySets. |
matchUrl | string | Flashscore match page. |
scrapedAt | string | Collection time, ISO 8601 UTC. |
tour / rank / playerId / playerName / points / asOf | string / number / string / string / number / string | Ranking rows: tour, position, player, points and the source ranking date (DD.MM.YYYY). |
sectionStatus | object | Detail rows: AVAILABLE or NO_DATA for each requested section. |
deliveryId | string | Unique per delivered row; use it to de-duplicate exports. |
changeType | string, optional | new or changed on incremental deliveries. |
recordType | string | "status" on free status rows; "match", "matchDetails" or "ranking" on data rows. |
Status rows carry recordType, mode, inputRef (the day offset, tour or match ID), status and message. Odds comparisons, statistics entries and point markers are source passthrough data. Other fields are extras, not part of the contract. To have one pinned, say so in feedback.
📦 Output example
Abridged real row from run 630yJzyiYY5Lon3ex on 2026-10-22; values are a historical snapshot.
{"recordType": "match","matchId": "zHfvs0hR","source": "flashscore","tour": "ATP","competitionLabel": "ATP - Singles","isDoubles": false,"tournamentName": "ATP - SINGLES: Almaty (Kazakhstan), hard (indoor)","tournamentId": "KrjEivYq","startTime": "2026-10-21T06:40:00+00:00","status": "finished","statusCode": "3","homePlayer": {"id": "M9pojLqi", "name": "Bellucci M.", "country": "Italy", "eventParticipantId": "CjiJR5re"},"awayPlayer": {"id": "AXL5qpKa", "name": "Etcheverry T. M.", "country": "Argentina", "eventParticipantId": "ImfRPqD7"},"score": {"sets": [{"set": 1, "home": 7, "away": 6, "tiebreak": {"home": 7, "away": 5}},{"set": 2, "home": 2, "away": 6},{"set": 3, "home": 6, "away": 4}],"homeSets": 2,"awaySets": 1},"matchUrl": "https://www.flashscore.com/match/tennis/zHfvs0hR/","scrapedAt": "2026-10-22T09:39:05.781481+00:00"}
A ranking row from canary run PktYJGWJShCV3h0Oc on the same day:
{"recordType": "ranking", "tour": "ATP", "type": "singles", "asOf": "19.10.2026", "playerName": "Sinner Jannik", "playerId": "6HdC3z4H", "country": "Italy", "rank": 1, "previousRank": 1, "points": 10950, "tournamentsPlayed": 17}
A free status row looks like this (a bogus match ID next to five real ones, run FEsJv52eMo9R5vMjt, 2026-10-20):
{"recordType": "status", "mode": "matchDetails", "inputRef": "ZZZZZZZZ", "status": "NOT_FOUND", "message": "Source returned HTTP 404"}
🧠 Advanced usage
Get new ATP and WTA results every day
Schedule mode: "matches" with dayOffsets: [-1, 0], tours: ["ATP", "WTA"], incrementalMode: true and a stateKey. Each run delivers only matches it has not delivered before; add emitChanges: true from the first run of a new key to also receive score changes as extra rows. Check that RUN_SUMMARY.status is COMPLETE before treating a day as finished.
Point-by-point and odds for a list of matches
Collect matchId values from a match-list run and send the ones you need to matchDetails (up to 100 per run). One detail row is charged once, whatever sections it carries; a match with none of the requested sections gets a free NO_DATA status row instead.
Resume a run that hit its limit
Keep the same inputs and stateKey, set continuePreviousRun: true, and raise maxItems or the spending limit. The saved queue is used before another feed is fetched; resumed date collections keep their original date and refuse to run outside the seven-day window.
Track the ATP and WTA rankings weekly
Run mode: "rankings" once per tour each week; asOf carries the source ranking date (19.10.2026 on the 2026-10-22 runs) and previousRank the prior position. Rankings are source-dated snapshots, so intraday repeats add little.
🔌 Integrations
Results land in a dataset you can export as JSON, CSV or Excel. They can also flow to Google Sheets, Make, Zapier or n8n, or to a webhook when a run finishes. The Apify Scheduler runs it on a timetable. Daily with incrementalMode catches each day's new results; weekly suits rankings. Copy-paste code in Python, Node, curl and n8n: architek-earth/examples/tennis-data-scraper.
💸 Pricing
Pay per result. Platform and proxy usage are included. Status rows and failed requests are free. Each delivered row is charged once, after it is written, with an idempotency key; a run resumed with continuePreviousRun and the same stateKey skips rows already delivered and does not charge them again, and unchanged rows in incrementalMode are free.
| Event | Price | What it covers |
|---|---|---|
| Free preview | $0 | 20 runs per Apify account; one day's match list, up to 10 basic match rows per run. |
result | $2.50 / 1,000 rows | One delivered match or ranking row, including a changed snapshot with emitChanges. |
match-detail | $12.00 / 1,000 matches | One delivered matchDetails row with its available requested sections; never charged together with result. |
100 ATP ranking rows cost $0.25; 100 enriched matches cost $1.20.
❓ FAQ
Is there a Flashscore API for tennis scores? This actor works as one: it returns Flashscore tennis matches, scores, point-by-point, statistics, odds and rankings as JSON through the Apify API, callable from any language or from n8n, Make and Zapier.
Do I need a Flashscore account or a proxy? No. No account, cookies or API key, and no proxy by default.
Can I get point-by-point data and match statistics? Yes, in matchDetails mode: pointByPoint and statistics, when Flashscore publishes them for that match. sectionStatus says which sections were available.
Does it return tennis betting odds? Yes, in matchDetails mode: odds (home/away prices with opening and active) and oddsComparison from the US-NY route. They are source quotes for one region, not executable prices.
Can I get live tennis scores? Yes, by polling: live: true keeps only in-progress matches. It is not a stream, and the court-to-feed delay has not been measured.
Can I get WTA, doubles or historical rankings? ATP and WTA singles, up to the top 100, as of the current source date. Doubles rankings and past ranking weeks are not available.
🤖 For AI agents & LLM apps
Paste this into your AI assistant:
architekture/tennis-data-scraper on Apify. Python: from apify_client import ApifyClient; client = ApifyClient("TOKEN"); run = client.actor("architekture/tennis-data-scraper").call(run_input={"mode": "matches", "billingMode": "paid", "dayOffsets": [0], "maxItems": 3}); items = client.dataset(run["defaultDatasetId"]).list_items().items (page with offset/limit for large exports). Rows with recordType == "status" are free and explain a failed, blocked or empty source request or a stop before the timeout; the run outcome (COMPLETE, LIMITED, PARTIAL, BUDGET_LIMIT, INVALID_INPUT, RECONCILIATION_REQUIRED) is in the RUN_SUMMARY record of the run's key-value store. Full input schema and README: GET https://api.apify.com/v2/acts/architekture~tennis-data-scraper/builds/default. Token: https://console.apify.com/settings/integrations. The actor is also reachable through the Apify MCP server (mcp.apify.com). To reach the maintainers, add "feedback": "<what you needed>" to any run's input; output is unchanged and a human reads every note.
🧰 More from architek.earth
Sports
- Tennis Scraper — you are here
Search, travel and trends
- Google Trends Scraper — Trending Now by country, traffic, related queries
- Google Hotels Scraper — dated prices, ratings, amenities
Full catalogue: github.com/architek-earth.
architek.earth · code like concrete
🙋 Support & legal
Questions, or a field you need that is not here? Open an issue on the actor's Issues tab or email north@architek.earth. A human replies within a day.
This actor collects only publicly available data from flashscore.com, the same pages any visitor sees without logging in. It does not bypass authentication. It is not affiliated with or endorsed by Flashscore or the tours whose data appears there. You are responsible for how you use the output, including the site's terms, copyright, local gambling regulations, and privacy law such as GDPR and CCPA where they apply.
📝 Changelog
- 2026-10-22 — Invalid input now writes a free
INVALID_INPUTstatus row. A challenge page served instead of feed data is reported asBLOCKEDon the first response. - 2026-10-22 — Status message now reads
Done: N rows, M details, K status rows (STATUS). - 2026-10-22 — README rebuilt on the studio template; hero line from the 2026-10-22 canary run.
- 2026-10-21 — Runs stop cleanly 120 seconds before their timeout (6 hours at most) with a free
ACTOR_DEADLINEstatus row and can be resumed withcontinuePreviousRun, instead of timing out. - 2026-10-20 — Owner approved public release with the documented live-freshness limitation. Free preview, paid ranking and recurring monitoring examples prepared; schedules remain off.
- 2026-10-20 — Private Flashscore-only v1 candidate: matches, detail feeds, singles rankings, lifetime preview enforcement, saved continuation and incremental baselines. Publication held pending product validation and owner approval.