# Sofascore Tennis Scraper — ATP & WTA Set Scores (`incognito_mode/sofascore-tennis-scraper`) Actor

Scrape ATP, WTA, Challenger and ITF tennis from Sofascore. Every match returns the set-by-set score with tiebreaks, both players with country, live ranking and seed, plus surface, round, tier, duration and winner. Live, finished and upcoming draws. No API key or login — runs on the free plan.

- **URL**: https://apify.com/incognito\_mode/sofascore-tennis-scraper.md
- **Developed by:** [Elena Vance](https://apify.com/incognito_mode) (community)
- **Categories:** Developer tools, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 tennis matches

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Sofascore Tennis Scraper — ATP & WTA Set Scores

Scrape tennis matches from Sofascore with the **set-by-set score**, both
players, seeds, live rankings, surface, round and result. ATP, WTA, Challenger,
WTA 125, ITF, UTR, Davis Cup and the Billie Jean King Cup. Singles and doubles.
No Sofascore API key, no login.

Results are stored in an Apify dataset you can export as **JSON, CSV, Excel,
XML or HTML**.

### What you get

A real run, `{ "tours": ["atp", "wta"], "dateFrom": "2026-08-26" }` — 165
matches from the four ATP/WTA draws played that day:

| Player 1 | Player 2 | Score | Round | Surface | Duration |
| --- | --- | --- | --- | --- | --- |
| Seongchan Hong | Nikoloz Basilashvili **\[19]** | **6-3 7-6(1)** | Qualification Round 1 | Hardcourt outdoor | 110 min |
| Jack Secord **\[WC]** | Tristan Schoolkate | **3-6 2-6** | Qualification Round 1 | Hardcourt outdoor | 86 min |
| Kristina Penickova **\[WC]** | Harriet Dart **\[29]** | **7-5 0-6 3-6** | Qualification Round 2 | Hardcourt outdoor | 124 min |
| Michael Antonius **\[WC]** | Henry Searle | **6-1 6-2** | Qualification Round 1 | Hardcourt outdoor | 67 min |

Every row also carries the per-set breakdown as structured data (games,
tiebreak points and duration per set), both players' IDs, countries and ATP/WTA
rankings at match time, the tournament tier and ranking points, the venue, and
a direct link to the match — see [Output](#output) for the full shape.

***

### Why this is cheap to run

**Two reasons: the tour filter, and no proxy.**

#### The tour filter is the price of the run

A single day of tennis carries around **150 tournaments** — but only about
**six** of them are ATP or WTA. The other 96% is the ITF and UTR development
circuit, which most buyers never want and would otherwise pay to download and
throw away.

Measured on a real date (2026-08-29, 147 tournaments in total):

| Tours selected | Match type | Tournaments fetched |
| --- | --- | --: |
| `["atp", "wta"]` *(default)* | singles | **4** |
| `["atp", "wta"]` | all | 6 |
| `+ challenger` | singles | 11 |
| `+ itf-men, itf-women` | singles | 40 |
| every tour | all | 147 |

Leaving the default alone makes a run roughly **20x cheaper** than scraping
the whole calendar. Choosing "Singles" also skips the separate doubles draws,
which are about a quarter of a day's tournaments again.

#### No premium plan and no residential proxy

This Actor talks to `sofascore.com/api/v1` **directly** — no headless browser.
Sofascore screens requests on their TLS handshake, which this Actor clears
with a browser-grade HTTP client rather than by paying for egress.

Sofascore does refuse a share of datacenter IPs, Apify's own runners included,
so a run on the platform normally reaches Sofascore through a proxy. What
matters is *which* proxy — the Actor escalates cheapest-first and stops at the
first tier that works:

1. **Direct** — free. This is what a run from your own machine usually uses.
2. **Datacenter proxy** — **included in every Apify plan, the free tier
   included.** Up to 12 IPs are rotated here. Across five platform runs the
   most any single run needed was 4, and every run got through on this tier.
3. **Residential proxy** — the metered one, reached only if all 12 free IPs
   above were refused.

So there is **no residential-proxy add-on requirement** and no egress bill to
absorb. Across eight platform runs, every one was served by a free datacenter
IP.

### Two modes

| Mode | Input | What it fetches |
| --- | --- | --- |
| **Live** (`liveOnly: true`) | none needed | Every tennis match in progress right now, on the tours you chose. One fast call. Live rows carry the running set score and the current game point. |
| **Daily schedule** (default) | `dateFrom` / `dateTo` | Matches for each date in range. Works out which tournaments played that day, keeps the ones on your tours, ranks them by Sofascore's own follower count and fetches the top `maxTournamentsPerDate` — or pass `tournamentIds` to skip discovery entirely. |

Both **past and future** dates work. A forward-dated run returns the published
draw: players, seeds, rankings and round, with the score fields empty.

### What makes this different

- **The set score, assembled for you.** `scoreString` is the line a human
  reads — `6-3 7-6(1)` — with the loser's tiebreak points in brackets, plus a
  structured `sets` array carrying games, tiebreak points and duration per set.
- **A tour filter that is a real cost control**, not a convenience. See the
  table above.
- **Seeds survive as seeds.** `WC`, `Q`, `LL` and `A` are 30% of all seeds in
  a Grand Slam draw. They are kept verbatim in `seed`, with `seedNumber`
  offered separately for the numeric ones — nothing is silently dropped by an
  integer cast.
- **Retirements and walkovers are labelled.** Sofascore files both under
  status `finished`, so a naive results filter quietly mixes in matches that
  were never played. `resultType` names them (`completed` / `retired` /
  `walkover`) and `isCompleted` is false for both.
- **Live state is not left on finished rows.** Sofascore leaves the last
  game's point on a match that ended hours ago; this Actor drops it, so a
  historical row never carries a live-looking game score. Same for the
  tiebreak fields, which carry stray game points (`40`) on sets that never had
  a tiebreak — only a genuine 7-6 set reports one.
- **A $0.00005 start fee**, against up to $0.05 on comparable tennis Actors.
  On a small run that gap is most of the bill.
- **Failures are never billed.** Rows are charged only once stored; a
  tournament that cannot be read is reported in the run summary and costs
  nothing, and unusable input returns an unbilled diagnostic row instead of
  failing the run.

### Input

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `tours` | array | `["atp","wta"]` | `atp`, `wta`, `challenger`, `wta-125`, `itf-men`, `itf-women`, `utr-men`, `utr-women`, `davis-cup`, `billie-jean-king-cup`, `united-cup`, `exhibition`, `international`. **The main cost control.** |
| `matchType` | string | `singles` | `singles`, `doubles` or `all`. |
| `liveOnly` | boolean | `false` | Live snapshot instead of a date range. |
| `dateFrom` / `dateTo` | string | today | `YYYY-MM-DD`, inclusive, max 31 days per run. |
| `tournamentIds` | array | — | Sofascore unique-tournament IDs; skips discovery. The tour filter is not applied to these. |
| `maxTournamentsPerDate` | integer | `30` | Cap on tournaments fetched per date, after the tour filter. |
| `maxDiscoveryPages` | integer | `2` | A whole tennis day fits in 2 pages. |
| `maxItems` | integer | `500` | Hard cap on stored matches. |
| `proxyConfiguration` | object | off | Leave off; escalation is automatic. |

> Grand Slams are ATP and WTA events — they are already included in the
> default. There is no separate `grand-slam` tour, even though Sofascore's own
> category list shows one; that entry is a UI filter with a negative ID and
> matches no tournament. Filter on `tier == "grand-slam"` in the output
> instead.

### Output

One row per match:

```jsonc
{
  "eventId": 16931831,
  "matchUrl": "https://www.sofascore.com/tennis/match/pablo-llamas-ruiz-lloyd-harris#id:16931831",
  "startTimeIso": "2026-08-28T15:00:00+00:00",

  "tour": "ATP", "tourSlug": "atp",
  "tournamentId": 2449, "tournamentName": "US Open, Men",
  "eventName": "US Open, New York, USA, Qualifying",
  "tier": "grand-slam",              // grand-slam | p1000 | p500 | p250 | lower
  "surface": "Hardcourt outdoor",
  "rankingPoints": 2000,
  "matchType": "singles", "gender": "M",
  "round": { "name": "Qualification Final", "slug": "qualification-final", "number": 250 },
  "venue": { "court": "Court 14", "city": "New York", "country": "USA" },

  "status": { "code": 100, "description": "Ended", "type": "finished" },
  "resultType": "completed",         // completed | retired | walkover | canceled | live | scheduled
  "isCompleted": true,
  "winner": 1, "winnerName": "Lloyd Harris",

  "player1": {
    "id": 157808, "name": "Lloyd Harris", "shortName": "L. Harris",
    "country": "South Africa", "countryCode": "ZAF",
    "ranking": 175, "seed": null, "seedNumber": null,
    "setsWon": 2, "isWinner": true, "partners": []
  },
  "player2": { "…": "same shape; `partners` holds both players of a doubles pair" },

  "setsWonPlayer1": 2, "setsWonPlayer2": 0,
  "scoreString": "7-6(3) 6-3",
  "sets": [
    { "set": 1, "player1": 7, "player2": 6,
      "player1TieBreak": 7, "player2TieBreak": 3, "durationSeconds": 3752 },
    { "set": 2, "player1": 6, "player2": 3,
      "player1TieBreak": null, "player2TieBreak": null, "durationSeconds": 2310 }
  ],
  "matchDurationSeconds": 6062,
  "currentPoint": null,              // live matches only: { player1, player2, set }
  "firstToServe": 1
}
```

### What this Actor does **not** return

- **Point-by-point rallies and per-match serve/return statistics.** This Actor
  is the match and its score. Sofascore publishes deeper per-match statistics
  through a different endpoint.
- **Who is serving right now.** Sofascore publishes `firstToServe` — who
  served the opening game — and no current-server field, so none is invented.
- **A doubles pair's ranking.** The payload carries no pair-level rank and
  none is synthesised. Individual players' singles rankings are present.
- **ATP/WTA ranking tables.** Each match carries both players' rankings
  inline; for the full tables see the related Actor below.
- **A "match tiebreak" flag.** A deciding-set match tiebreak is reported by
  Sofascore in the games field itself, so it appears as e.g. `4-10` rather
  than in brackets.

### Example runs

```jsonc
// Yesterday's ATP and WTA results
{ "tours": ["atp", "wta"], "dateFrom": "2026-08-26" }

// Everything live right now, every tour, singles and doubles
{ "tours": ["atp", "wta", "challenger", "itf-men", "itf-women"],
  "matchType": "all", "liveOnly": true }

// The upcoming US Open draw, both singles events
{ "tournamentIds": [2449, 2601], "dateFrom": "2026-09-01", "dateTo": "2026-09-03" }

// A month of Challenger results for a model
{ "tours": ["challenger"], "dateFrom": "2026-08-01", "dateTo": "2026-08-31",
  "maxItems": 5000 }
```

### Related actors

- **[Sofascore Live Scores & Fixtures](https://apify.com/incognito_mode/sofascore-live-scores-scraper)** — the same matches across 19 sports, football-shaped.
- **[Sofascore Rankings](https://apify.com/incognito_mode/sofascore-rankings-scraper)** — the full ATP, WTA, ATP live, WTA live and UTR ranking tables.
- **[Sofascore Match Stats, xG & Lineups](https://apify.com/incognito_mode/sofascore-match-analytics-scraper)** — deeper per-match statistics.
- **[Sofascore Team & Player Data](https://apify.com/incognito_mode/sofascore-team-player-scraper)** — player profiles and career data.

# Actor input Schema

## `tours` (type: `array`):

Which tours to scrape. THIS IS THE MAIN COST CONTROL. A single tennis day carries around 150 tournaments, but only about 6 of them are ATP or WTA — the other 96% is the ITF and UTR development circuit. Leaving this on ATP + WTA makes a run roughly 20x cheaper and faster than scraping everything. Grand Slams are ATP/WTA events, so they are already included; there is no separate Grand Slam tour.

## `matchType` (type: `string`):

Singles only, doubles only, or both. Doubles draws are separate tournaments on most tours, so choosing "Singles" also skips fetching them — about a quarter of a day's tournaments. Davis Cup, United Cup and Billie Jean King Cup ties mix both in one tournament and are classified per match.

## `liveOnly` (type: `boolean`):

Fetch only matches currently in progress (one fast call, always "now" — no date range needed). Live rows carry the running set score and the current game point. Turn off to fetch a daily schedule instead, using Date from / Date to below.

## `dateFrom` (type: `string`):

First date to fetch (YYYY-MM-DD). Defaults to today (UTC). Future dates work and return the published draw — players, seeds, rankings and round, with the score fields empty. Ignored when "Live matches only" is on.

## `dateTo` (type: `string`):

Last date to fetch (YYYY-MM-DD), inclusive. Defaults to "Date from". Max 31 days per run. Ignored when "Live matches only" is on.

## `tournamentIds` (type: `array`):

Sofascore unique-tournament IDs to fetch directly for the date range, e.g. \[2449, 2601] for the US Open men's and women's draws (the ID is in the tournament's Sofascore URL). Skips discovery entirely, so a run stays fast and predictable across a wide date range. The Tours filter is NOT applied to these — naming a Challenger draw returns it whatever Tours says — but Match type still is, so set it to "Singles and doubles" if you are pointing at a doubles draw. Ignored when "Live matches only" is on.

## `maxTournamentsPerDate` (type: `integer`):

When "Tournament IDs" is empty, how many of the matching tournaments (ranked by Sofascore follower count) to fetch matches for on each date. Only tournaments that pass the Tours filter are counted, so the default is generous for ATP + WTA and only starts binding if you add the ITF or UTR circuits.

## `maxDiscoveryPages` (type: `integer`):

How deep to page through the tournament-discovery step on each date. A whole tennis day fits in 2 pages (about 150 tournaments), so the default already sees every tournament — unlike football, where it would be a ranked sample. Raise it only if Sofascore expands the calendar. Ignored when "Tournament IDs" is set.

## `maxItems` (type: `integer`):

Hard cap on how many matches are stored in the dataset. The run stops as soon as it is reached.

## `proxyConfiguration` (type: `object`):

Leave off. Every request calls Sofascore directly first, which costs nothing, and most runs never need anything else. If Sofascore does block an IP, the Actor escalates on its own: first to a datacenter proxy (included in every Apify plan, free tier included), rotating a few IPs, and only then to a residential one if your plan has them. No residential add-on is required. Set this only to pin a specific proxy configuration from the start — for example to scrape from a particular country.

## Actor input object example

```json
{
  "tours": [
    "atp",
    "wta",
    "challenger",
    "itf-men",
    "itf-women"
  ],
  "matchType": "singles",
  "liveOnly": false,
  "maxTournamentsPerDate": 10,
  "maxDiscoveryPages": 2,
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset containing every scraped tennis match.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "tours": [
        "atp",
        "wta",
        "challenger",
        "itf-men",
        "itf-women"
    ],
    "matchType": "singles",
    "liveOnly": false,
    "maxTournamentsPerDate": 10,
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/sofascore-tennis-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "tours": [
        "atp",
        "wta",
        "challenger",
        "itf-men",
        "itf-women",
    ],
    "matchType": "singles",
    "liveOnly": False,
    "maxTournamentsPerDate": 10,
    "maxItems": 50,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("incognito_mode/sofascore-tennis-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "tours": [
    "atp",
    "wta",
    "challenger",
    "itf-men",
    "itf-women"
  ],
  "matchType": "singles",
  "liveOnly": false,
  "maxTournamentsPerDate": 10,
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call incognito_mode/sofascore-tennis-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,incognito_mode/sofascore-tennis-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/wR8jEeG9m60TOH2eS/builds/2nmQKijgkgsEhYThZ/openapi.json
