SofaScore Tennis Scraper — Live Scores, Stats & Point-by-Point avatar

SofaScore Tennis Scraper — Live Scores, Stats & Point-by-Point

Pricing

from $4.00 / 1,000 per match returneds

Go to Apify Store
SofaScore Tennis Scraper — Live Scores, Stats & Point-by-Point

SofaScore Tennis Scraper — Live Scores, Stats & Point-by-Point

Scrape every ATP, WTA, Challenger and ITF tennis match on SofaScore by date, tournament or player — set-by-set scores, 42 match statistics, point-by-point rally data, serve and return splits, H2H record, seeds, prize money, court and surface. No API key.

Pricing

from $4.00 / 1,000 per match returneds

Rating

0.0

(0)

Developer

Scrapers Delight

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

🎾 SofaScore Tennis Scraper — live scores, match statistics & point-by-point

Every ATP, WTA, Challenger and ITF tennis match on SofaScore — singles and doubles — as one wide, flat row: the set-by-set score with tiebreaks, both players with their world ranking, seed, country and full bio, the court and surface, the winner, 42 match statistics per player for the whole match and for each individual set, the complete point-by-point rally log, the momentum curve with break flags, and the two players' head-to-head record.

Find matches three ways: by date (a whole day's world card), by tournament, or by player. Plus a live mode for whatever is on court right now.

No API key. No login. No browser. No CAPTCHA.


📊 What you get, measured — not estimated

Every number here came from a real Apify run on 2026-09-03. Nothing is extrapolated.

MeasurementResult
Matches in the measurement run60 (mode date, unfiltered, 149 s)
Distinct columns on the dataset262
Columns populated on 100% of rows75
Statistics sheet on matches that had actually started44 / 44 — 100%
Point-by-point on finished matches41 / 41 — 100%
Individual points logged across those 60 matches6,667
Court / venue59 / 60 — 98%
Head-to-head record50 / 60 — 83%
Crowd prediction votes55 / 60 — 92%
Winner on finished matches41 / 41 — 100%
Duplicate matches0 (60 unique event IDs)
API requests for those 60 enriched matches509, of which 8 failed after retry (1.6%), 0 blocked

The lower tail holds up too. A second run restricted to Challenger + ITF, finished matches only, returned 30 matches in 98 s: 29 / 30 carry a full statistics sheet and 29 / 30 carry point-by-point — 28 / 30 carry both (the two gaps are different matches: one doubles rubber SofaScore publishes no statistics for, and one singles match with no point-by-point feed), across Challenger 23 / ITF Women 7, on red clay 11 / hardcourt 19 — and 8 of those rows were doubles, each carrying both partners on each side.

Player mode: Carlos Alcaraz12 matches in 28 s, correctly scored (7-6(2), 6-3, 6-2) with the tiebreak points parsed out.


🎯 What does this actor do?

It reads SofaScore's own public JSON API — the same feed the sofascore.com scoreboard is drawn from — and flattens it into a dataset you can put straight into a model, a spreadsheet or a database.

  • 🏆 Every tour, one row shape. A Grand Slam final and an ITF W15 first round come back identically structured.
  • 🗓️ Any day, forwards or backwards. Past results and future fixtures both work; daysBack backfills a week or a month without listing every date.
  • 🎾 Set-by-set scores with tiebreak points7-6(2), 6-3, 6-2 — plus a structured sets array so you never have to parse a string.
  • 🔴 Live matches carry live state: the current game point for both players (0/15/30/40/A), who served first, and the set in progress.
  • 📈 42 statistics per player, per period. Aces, double faults, 1st and 2nd serve in and won, break points saved and converted, winners and errors split by stroke (forehand, backhand, volley, groundstroke, lob, overhead, drop shot, return), unforced errors, return points, tiebreaks — for the whole match and for each individual set.
  • 🎯 Point-by-point. Every point of every game of every set, with the running game score, who was serving, and who won the game. This is the field most tennis feeds simply do not carry.
  • 🌊 Momentum. SofaScore's per-game "tennis power" value with a flag on the games where a break actually happened.
  • 🤝 Head-to-head. The two players' career record against each other, as three numbers on the row.
  • 👤 Player bios. World ranking and the event entry ranking (they differ), seed, height, weight, plays right/left, birth date, birthplace, residence, current-year and career prize money with currency.
  • 🏟️ Court and surface. The actual court ("Grandstand", "Louis Armstrong Stadium"), city, country and ground type.
  • 👥 Doubles handled properly — both partners on each side as structured objects, not a mangled "A./B." string.
  • 🔗 Real public URLs for the match, the tournament and each player, read out of SofaScore's own HTML rather than guessed.

🚫 What it deliberately does not do

One site, one job. These are different row shapes and belong in different actors:

  • ATP / WTA rankings tables. They work on this API (500 rows each, verified) but a ranking row is a player, not a match. Different object, different actor.
  • Career or season aggregates. "Player stats" here means the statistics of this match plus the player's bio, not their season totals.
  • H2H match history. The row carries the head-to-head record (wins–wins–draws). The list of every past meeting is a different row shape.
  • Bookmaker odds ship off by default and are not in the title. The endpoint works and you can switch includeOdds on — but redistributing betting odds is a licensing question for your jurisdiction, so it is an explicit opt-in, never a surprise.

🚀 Quick start

Run it with nothing set and you get today's card, 40 matches, fully enriched.

{ "mode": "date", "maxMatches": 20 }

A whole day of world tennis, results only:

{ "mode": "date", "dates": ["2026-09-02"], "matchStatus": "finished", "maxMatches": 0 }

One tournament's whole season:

{ "mode": "tournament", "uniqueTournamentIds": ["2449"], "wholeSeason": true, "maxPagesPerTournament": 5 }

One player's recent matches, fast and cheap (no enrichment):

{ "mode": "player", "playerNames": ["Carlos Alcaraz"], "maxMatches": 30, "enrichMatches": false }

A week of WTA results:

{ "mode": "date", "daysBack": 6, "tours": ["WTA"], "matchStatus": "finished", "maxMatches": 0 }

⚙️ Input

FieldTypeDefaultWhat it does
modeenumdatedate · live · tournament · player. Auto-corrects: paste tournament IDs or player names and it switches for you.
datesstring[]today (UTC)YYYY-MM-DD. Past results and future fixtures both work.
daysBackint0Also include the N days before each date. 6 = a full week.
uniqueTournamentIdsstring[]e.g. 2449 from sofascore.com/tennis/tournament/atp/us-open/2449.
wholeSeasonboolfalseTournament mode: walk the current season backwards instead of using dates.
maxPagesPerTournamentint3Season pages to walk (30 matches per page, contiguous, no overlap).
playerNamesstring[]Resolved against SofaScore's own search, e.g. Iga Swiatek.
playerIdsstring[]SofaScore player IDs if you already have them (every row carries homeId / awayId).
maxMatchesPerPlayerint30How far back to walk each player's history.
matchStatusenumanyfinished · inprogress · notstarted.
toursstring[]Case-insensitive filter on tour + tournament name: ATP, WTA, Challenger, ITF, US Open.
enrichMatchesbooltrueMaster switch for the deep data (4–7 extra requests per match).
includeStatisticsbooltrueThe 42-statistic sheet, per match and per set.
includePointByPointbooltrueEvery point of every game.
includeMomentumbooltruePer-game momentum + break flags.
includeH2HbooltrueCareer head-to-head record.
includeVotesbooltrueCrowd predictions.
includeOddsboolfalseBookmaker markets. Opt-in.
maxMatchesint400 = no limit. A full world day is 1,000+ matches.
maxConcurrencyint8Parallel requests.
proxyConfigurationproxyApify RESIDENTIALRequired — see Honest limits.

Capped runs are not random. Tournaments are visited in SofaScore's own priority order (biggest tour and largest following first), so maxMatches: 20 returns the matches people actually watch, not 20 arbitrary ITF qualifiers.


📤 Output — the full field table

One row per match. Fill % below is measured on the 60-match run; where a field is structurally absent for a whole class of match (a fixture has no statistics because it has not been played), the honest denominator is given too.

Identity & state — 100% fill

FieldTypeNotes
eventIdintSofaScore's match ID
slug, customIdstringURL parts
matchUrlstringsofascore.com/tennis/match/<slug>/<customId> — verified 200
startTimestamp, startTimeint / ISOUnix + ISO-8601 UTC
statusCode, statusType, statusDescriptionint / stringfinished · inprogress · notstarted; "Ended", "4th set"
finalResultOnly, feedLocked, hasGlobalHighlightsboolFeed depth flags
firstToServeint1 = home, 2 = away (73%, live/finished only)
changeTimestamp, lastChangesint / string[]Last feed update
scrapedAtISO

Competition — 100% fill

tour (ATP / WTA / Challenger / ITF Women…), tourSlug, tourFlag, tournamentName, tournamentSlug, tournamentId, tournamentPriority, tournamentStartTimestamp, tournamentEndTimestamp, uniqueTournamentId, uniqueTournamentName, uniqueTournamentSlug, tournamentUrl, tournamentGroup ("US Open"), tournamentGroupCategory ("Grand Slam"), titleHolder, tennisPoints, uniqueTournamentUserCount, hasEventPlayerStatistics, surface, seasonId, seasonName, seasonYear, round, roundName, roundSlug, matchCategory (singles/doubles), matchLevel, gender.

Players — home* and away*

FieldFillNotes
homeId, homeName, homeFullName, homeShortName, homeNameCode, homeSlug, homeUrl, homeGender, homeCountry, homeCountryCode, homeCountryCode3, homeNational, homeUserCount, homeEntryRanking, homePlayerId100%Entry ranking = the rank on the event's entry list
homeWorldRanking98%The live world ranking — differs from the entry ranking
homePlays, homeBirthDate, homeBirthDateTimestamp, homePrizeCurrent, homePrizeCurrentCurrency, homePrizeTotal, homePrizeTotalCurrency, homeUnderage97–98%
homeHeight95%metres
homeBirthplace, homeBirthCity, homeBirthCountry93–95%
homeWeight77%kg
homeClass68%SofaScore seeding class
homeResidence, homeResidenceCity, homeResidenceCountry57–62%
homeSeed50%Only seeded/qualifier entries have one — "5", "Q", "WC"
homePartnersdoubles onlyBoth partners as {id, name, slug, country, url}8 / 8 doubles rows in the Challenger run
homeTurnedPro0% on this sampleSofaScore did not return it for any of the 60 players; the column is declared so it populates where it exists

Score

FieldFillNotes
homeScore / awayScore, homeScoreDisplay / awayScoreDisplay73% (100% of started matches)Sets won
scoreline73%"7-6(2), 6-3, 6-2"
sets73%[{set, label, home, away, homeTieBreak, awayTieBreak}]
homeSet1…5 / awaySet1…573 / 52 / 22 / 3%Falls off naturally — most matches are 2–3 sets
homeGamePoint / awayGamePoint73%Live game point 0 / 15 / 30 / 40 / A
winner, winnerName100% of finished matches (68% overall)home / away
lastPeriod, periodLabels5% / 100%
setDurationsSeconds, matchDurationSeconds73%Per-set and total, in seconds
currentPeriodStartTimestamp73%

Court — 98%

venueName ("Grandstand"), venueStadium, venueCity, venueCountry, venueCountryCode, venueId.

Statistics — 100% of matches that had started (73% of all rows)

Both a flat form for spreadsheets and a nested form for full fidelity.

  • Flat, whole-match: homeAces / awayAces, homeDoubleFaults, homeFirstServeAccuracy + homeFirstServeAccuracyTotal (89 of 155), homeSecondServeAccuracy(+Total), homeFirstServePointsAccuracy(+Total), homeSecondServePointsAccuracy(+Total), homeServiceGamesTotal, homeBreakPointsSaved(+Total), homePointsTotal, homeServicePointsScored, homeReceiverPointsScored, homeMaxPointsInRow, homeGamesWon, homeServiceGamesWon, homeMaxGamesInRow, homeWinnersTotal, homeForehandWinners, homeBackhandWinners, homeVolleyWinners, homeGroundstrokeWinners, homeLobWinners, homeOverheadWinners, homeDropShotWinners, homeReturnWinners, homeErrorsTotal, homeForehandErrors, homeBackhandErrors, homeGroundstrokeErrors, homeOverheadStrokeErrors, homeReturnErrors, homeUnforcedErrorsTotal, homeForehandUnforcedErrors, homeBackhandUnforcedErrors, homeVolleyUnforcedErrors, homeGroundstrokeUnforcedErrors, homeLobUnforcedErrors, homeDropShotUnforcedErrors, homeFirstReturnPoints(+Total), homeSecondReturnPoints(+Total), homeReturnGamesTotal, homeBreakPointsScored, homeTiebreaks — and the away* twin of each. 98 flat statistic columns.
  • Nested, every period: statistics = { "ALL": {...}, "1ST": {...}, "2ND": {...}, … }, each stat carrying {name, group, home, away, homeDisplay, awayDisplay} and, for ratio stats, homeTotal / awayTotal. statisticsPeriods lists which periods came back.

SofaScore reuses one key (serviceGamesTotal) for both "Service games played" and "Return games played". A naive flatten drops one of them; here the Return-group value is emitted as returnGamesTotal so both survive.

Point-by-point — 100% of finished matches (73% of all rows)

pointByPoint = [{ set, games: [{ game, homeGames, awayGames, serving, scoring, points: [{ homePoint, awayPoint, homePointType, awayPointType, pointDescription }] }] }], re-sorted into ascending set and game order (SofaScore serves it newest-first). pointByPointPoints is the point count — 6,667 across the 60-match run.

Momentum, H2H, votes, odds

FieldFillNotes
tennisPower67%[{set, game, value, breakOccurred}]
momentumPoints, momentumBreaks67%Counts
h2hHomeWins, h2hAwayWins, h2hDraws83%Career duel record
votesHome, votesAway, votesTotal92%Crowd predictions
oddsopt-in (0% by default)[{marketName, marketGroup, marketPeriod, choices:[{name, fractionalValue, initialFractionalValue, change, winning}]}]
enriched, enrichmentErrors100% / 13%Which enrichment endpoints failed for that row, if any

💵 Pricing

Plain pay-per-event. No monthly fee, no actor-start fee, no platform-usage surcharge.

EventPriceWhen it fires
Per match returned (match-scraped)$0.004 — $4.00 / 1,000Once per match delivered to your dataset. Matches your filters removed are never charged.
Per match enriched (match-enriched)$0.004 — $4.00 / 1,000Only when enrichment is on and at least one enrichment endpoint actually answered for that match.

So a plain score row is $0.004 and a fully loaded row — 42 statistics, per-set splits, every point of the match, momentum, H2H, court, both bios — is $0.008.

How that compares, verified live on 2026-09-03: the busiest tennis actor on the store charges $0.008 per result flat (with a $0.02 minimum charge) for a row that carries no point-by-point and no statistics sheet. A point-by-point specialist charges $0.075 per match record plus a $0.05 start fee. This actor is half price on the base row, matches the flat incumbent's price for a far deeper row, and comes in ~9× under the point-by-point specialist with no start fee.

Billing is charge-first. Rows are charged before they are pushed, and if your run's max total charge cannot cover an enrichment, the row is delivered un-enriched rather than billed for something you did not receive. Delivered always equals billed.


⚠️ Honest limits

Read this part. It is the part most store listings leave out.

1. A residential proxy is mandatory, not optional

SofaScore runs an edge IP allow/deny list (Server: Varnish, Retry-After: 0, a 48-byte body). Measured on 2026-09-03 from real runs:

EgressResult
A home broadband IP, curl403 × 3/3
Bare Apify platform IP (no proxy), HTTP/1.1 and HTTP/2403 × 8/8
Apify auto / datacenter proxy200 × 2/4 (h1), 200 × 1/4 (h2) — unusable
Apify RESIDENTIAL, HTTP/1.1200 × 3/4
Apify RESIDENTIAL + HTTP/2200 × 4/4 ← what this actor uses

The input schema defaults to Apify Proxy with the RESIDENTIAL group and the code re-applies that default if the field arrives empty. If you point it at datacenter proxies it will mostly fail, and it will tell you so in the run status message — it will never report a block as "no matches found". If no proxy can be created the run exits cleanly with an explanation rather than failing.

This is not a JS challenge, a CAPTCHA or a login wall — no browser, no cookies and no credentials are involved. It is purely which IP the request leaves from.

2. Residential transport is flaky per request; that is handled, not hidden

Measured on the 60-match run: 509 requests, 8 failed after every retry (1.6%), 0 blocked. Transport probing beforehand measured 11 retries across 65 raw fetches (~17%) answering Proxy responded with 590 UPSTREAM504 on the first try, so every URL is retried up to 5 times on a fresh proxy session each attempt. A match whose enrichment call still fails is delivered with those fields null and an enrichmentErrors note — 8 of 60 rows on the measurement run — and the run is never failed over one missing sub-request.

3. Not every match has statistics or point-by-point

This is the honest denominator behind the headline "73%":

  • Matches that have not started yet carry neither — there is nothing to count. 16 of the 60 rows were fixtures.
  • Of the matches that had started, 44 / 44 (100%) returned a statistics sheet, and of the finished matches 41 / 41 (100%) returned point-by-point.
  • In the Challenger/ITF run, 29 / 30 finished matches had both; the one gap was a doubles rubber.
  • hasEventPlayerStatistics came back false on all 60 rows even where statistics were served — it is not a useful predictor, so do not filter on it.

If you only want rows guaranteed to carry the deep data, set matchStatus: "finished".

4. Fields that were empty on this sample

homeTurnedPro / awayTurnedPro returned 0% across 60 players. homePartners / awayPartners are populated only on doubles (8/8 on the doubles rows measured). odds is empty unless you opt in. These columns are declared rather than dropped so the dataset schema stays stable, but do not plan around them.

5. This is an undocumented internal API and routes do churn

One route this feed used to expose (/sport/tennis/scheduled-events/{date}) now returns 404 — it died before this actor was built, and nine other plausible routes 404 as well. SofaScore can retire the working routes the same way with no notice. If the actor ever starts returning nothing, that is the first thing to check, and it is why every route is exercised on each run rather than assumed.

https://www.sofascore.com/robots.txt (fetched 2026-09-03, HTTP 200) contains, verbatim:

User-agent: Bytespider
Disallow: /
User-agent: *
Disallow: /*/2017-
Disallow: /*/2018-
Disallow: /*/2019-
Disallow: /*/2020-
Disallow: /*/2021-
Disallow: /*/2022-
Disallow: /*/2023-
Disallow: /*/2024-
Disallow: /*/2025-
Disallow: /images/share/
Disallow: /standings/
Disallow: /*/standings/

(plus the same standings path in each translated language). This actor reads api.sofascore.com/api/v1/... and links to /tennis/match/..., /tennis/player/... and /tennis/tournament/... — none of which appear in that list. https://api.sofascore.com/robots.txt answers 403 and publishes no policy of its own.

Only publicly visible scores and statistics are collected. No login, no personal accounts, no private data. Sports results are factual data, but you remain responsible for how you use and redistribute them in your jurisdiction — that goes double if you switch bookmaker odds on.


❓ FAQ

Do I need a SofaScore account or API key? No. The feed is public. You do need a residential proxy — see Honest limits §1.

Why is a proxy required when other scrapers do not need one? SofaScore blocks datacenter IP ranges at the edge, and Apify's platform IPs are in them. It is an IP list, not a bot challenge, so a residential exit clears it. Apify Proxy's RESIDENTIAL group is preselected for you.

How many matches are there in a day? Between 800 and 1,200 worldwide across ATP, WTA, Challenger, ITF, singles and doubles. The 2026-09-03 card had 88 unique tournaments. Set maxMatches: 0 for all of them.

How long does a full day take? The 60-match enriched run took 149 s. Enrichment is ~7 requests per match, so a full 1,000-match enriched day is roughly 40–60 minutes; the same day without enrichment is a few minutes. Raise maxConcurrency and the run timeout for big pulls.

Can I get just the scores cheaply? Yes — set enrichMatches: false. You still get 164 columns: tournament, both players, rankings, countries, set-by-set score with tiebreaks, live game point and winner, at $0.004 per match.

Does it do doubles? Yes. matchCategory says doubles, and homePartners / awayPartners carry both players on each side with their own IDs, slugs, countries and URLs.

Does it do live matches? Yes — mode: "live" returns whatever is on court right now, with the current game point and the set in progress. Schedule it every few minutes for a live feed.

How far back can I go? As far as SofaScore keeps the season. wholeSeason walks a tournament's history 30 matches per page with zero overlap between pages, and daysBack backfills up to 30 days per seed date in one run.

How do I find a tournament ID? Run once by date and read uniqueTournamentId off any row — or take the number off the end of a SofaScore tournament URL.

What is the difference between homeEntryRanking and homeWorldRanking? homeEntryRanking is the rank the player entered this event with (frozen at the acceptance deadline). homeWorldRanking is their live ranking today. They often differ by several places, and both are on every row.

What are homePointType and pointDescription in the point-by-point? SofaScore's own point classification codes (which side won the point and how — ace, double fault, break point, and so on). They are passed through unchanged rather than guessed at.

Will my run fail if SofaScore blocks it? No. A block exits the run cleanly with a status message naming the block. A failed run would be permanent on this actor's public record, and it would also be a lie — "blocked" and "no matches" are different answers and are reported differently.

Does it charge me for matches it filtered out? No. Filtering happens before billing, rows are charged before they are pushed, and enrichment is charged only when an enrichment endpoint actually answered.


Not affiliated with, endorsed by or sponsored by SofaScore. "SofaScore" is a trademark of its owner and is used here only to describe what this actor reads.