# Flashscore Tennis Scraper - Live Scores, Results & Set Scores (`neverempty/flashscore-tennis-scraper`) Actor

For tennis bettors, analysts and score apps: ATP, WTA, Challenger and ITF matches with players, round, start time, set scores with tiebreaks, winner, server and live game score. 535 of 535 matches matched Flashscore's page in a 3-day check. Monitoring returns only matches that changed.

- **URL**: https://apify.com/neverempty/flashscore-tennis-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Sports, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.19 / 1,000 match returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Flashscore Tennis Scraper - Live Scores, Results & Set Scores

For tennis bettors, data analysts and score-app builders: every ATP, WTA, Challenger and ITF match on Flashscore with players, tournament, round, start time, set-by-set games with tiebreaks, sets won, winner and match status, plus the server and the current game score while a match is live. In a check on 2026-09-21, 535 of 535 matches over 3 days matched Flashscore's own page field by field. Turn on monitoring and a schedule receives only the matches whose score or status changed since the last run.

Export as JSON, CSV or Excel.

### What you get

One row per match, for any day from 7 days back to 7 days ahead:

- **Players**: both sides as Flashscore shows them (`Sinner J.`), name slug, Flashscore player id and country; doubles pairs with each partner listed.
- **Tournament**: name, full header (`WTA - SINGLES: Seoul (South Korea), hard`), tour (ATP, WTA, Challenger Men, Challenger Women, ITF Men, ITF Women, Teams), singles / doubles / team tie, surface, qualification flag, tournament page and id.
- **Round**: `Final`, `1/16-finals` and so on, read from the match page (Flashscore's daily feed does not carry it).
- **Score**: sets won, games per set with tiebreak points, a readable `scoreText` such as `6-2 6(7)-7(9) 3-6`, the winner.
- **Status**: scheduled, live (with the set in play and whether it is a tiebreak), finished, retired, walkover, awarded, cancelled, interrupted, postponed and more, with Flashscore's own status text.
- **Live**: who is serving and the points of the current game (`15`, `30`, `40`, `A`).

Between 2026-09-15 and 2026-09-20 Flashscore's daily tennis feed carried 194 to 376 matches a day.

### How it matches Flashscore's page

The rows come from the same feed Flashscore's website reads, assembled the way the page assembles it:

1. **The daily feed can be minutes old.** It is served from a cache (ages up to 6 minutes were measured on 2026-09-21), so the page applies Flashscore's recent-changes feed on top right after loading. This Actor does the same; without it, live matches were a game behind and one finished match still showed "Set 2". Every row says whether that step worked in `liveUpdatesApplied`. The cache also has copies of different ages; when the copy that answered is older than two minutes, the Actor asks again (up to twice) and keeps the freshest, and `feedAgeSeconds` says how old the daily feed behind the row was. If the copy is older than the recent-changes feed reaches back, a match that started or ended in between can still show its earlier state, exactly as Flashscore's page does right after loading; in monitoring mode such a step backwards is never returned as a change.
2. **Each day's feed also carries matches that belong to the day before or after.** Flashscore's page does not show them on that day, and neither does this Actor: a match is returned under the day it starts on (in your time zone), and a team tie that runs over several days is returned once per run.
3. **A cancelled match shows no sets won** on Flashscore even when a few games were played. The games per set are kept; `player1Score` and `player2Score` are null.

The check behind the numbers above: Flashscore's tennis page was opened in a browser on 2026-09-21 for three days, and at the same moment the daily feed and the recent-changes feed were read. Every match on the page (535) was compared on names, sets won, games and tiebreaks for sets 1 to 5, current points, the serve marker, the winner's bold name, the status text, the start time and the day it is listed on: 535 of 535 matched.

### Input

| Field | What it does |
|---|---|
| `days` | Days to read: `today`, `yesterday`, `tomorrow`, a number from `-7` to `7`, or `YYYY-MM-DD`. Empty = today. Days outside Flashscore's 15-day window are refused before anything is requested. |
| `utcOffsetHours` | Your time zone as hours from UTC (0 = UTC). Decides where a day starts, as on Flashscore in that time zone. `startTime` is always UTC. |
| `tours` | `atp`, `wta`, `challenger-men`, `challenger-women`, `itf-men`, `itf-women`, `teams-men`, `teams-women`, `other`. Empty = all. |
| `matchTypes` | `singles`, `doubles`, `teams`. Empty = all. |
| `matchStatuses` | `live`, `finished`, `scheduled`, `other` (cancelled, interrupted, postponed...). Empty = all. |
| `players` | Keep a match if one of these appears in a player's name, slug or doubles partner (partial, case-insensitive). |
| `tournaments` | Keep a match if the tournament header contains one of these (partial, case-insensitive). |
| `includeRound` | On (default) = one small extra request per returned match to read the round from its page (only the first 32 KB are read). Off = faster, `round` is null. |
| `maxMatches` | Maximum rows to return (default 200). Not used in monitoring mode. |
| `monitoringMode` | Return only matches that changed since the row last returned for them. |
| `changeDetail` | In monitoring mode: `status` (any change of Flashscore's status - start, end, next set, tiebreak, break, medical timeout - or of the sets won), `games` (default, also every game and tiebreak point) or `points` (also every point and the server). |
| `resetMonitoringState` | Forget the remembered rows for this filter set, so everything counts as new. |
| `useProxy` | On (default) = if Flashscore answers 401, 403, 429 or 503, retry through an Apify proxy. The feed answered from a plain Apify datacentre address when this Actor was built. |

Example: today's live and finished WTA matches in Tokyo time.

```json
{
    "days": ["today"],
    "utcOffsetHours": 9,
    "tours": ["wta"],
    "matchStatuses": ["live", "finished"]
}
```

### Output

A live match returned by a monitoring run (`change` and the `previous...` fields appear only in monitoring mode):

```json
{
    "source": "flashscore.com",
    "status": "ok",
    "matchId": "SOlLdOBb",
    "matchUrl": "https://www.flashscore.com/match/SOlLdOBb/",
    "date": "2026-09-21",
    "utcOffsetHours": 0,
    "startTime": "2026-09-21T05:40:00.000Z",
    "tournament": "Seoul (South Korea)",
    "tournamentHeader": "WTA - SINGLES: Seoul (South Korea), hard",
    "tournamentId": "Q3ZRxpBf",
    "tournamentUrl": "https://www.flashscore.com/tennis/wta-singles/seoul/",
    "category": "WTA - Singles",
    "tour": "WTA",
    "matchType": "singles",
    "surface": "hard",
    "isQualification": false,
    "round": "1/16-finals",
    "roundStatus": "ok",
    "matchStatus": "live",
    "statusText": "Set 2",
    "statusCode": 18,
    "isLive": true,
    "isCompleted": false,
    "currentSet": 2,
    "isTiebreak": false,
    "playerOrder": "as-listed",
    "player1Name": "Rakhimova K.",
    "player1Slug": "rakhimova-kamilla",
    "player1Id": "boU2UKtN",
    "player1Country": "Uzbekistan",
    "player1Players": null,
    "player2Name": "Kudermetova P.",
    "player2Slug": "kudermetova-polina",
    "player2Id": "8O9jYpK8",
    "player2Country": "Uzbekistan",
    "player2Players": null,
    "winner": null,
    "winnerName": null,
    "scoreUnit": "sets",
    "player1Score": 1,
    "player2Score": 0,
    "player1SetsWon": 1,
    "player2SetsWon": 0,
    "sets": [
        { "set": 1, "player1Games": 7, "player2Games": 6, "player1Tiebreak": 7, "player2Tiebreak": 4 },
        { "set": 2, "player1Games": 4, "player2Games": 4, "player1Tiebreak": null, "player2Tiebreak": null }
    ],
    "scoreText": "7(7)-6(4) 4-4",
    "servingPlayer": 1,
    "servingPlayerName": "Rakhimova K.",
    "currentGamePlayer1Points": "0",
    "currentGamePlayer2Points": "0",
    "info": null,
    "liveUpdatesApplied": true,
    "feedAgeSeconds": 41,
    "change": "score-changed",
    "previousMatchStatus": "live",
    "previousScoreText": "7(7)-6(4) 3-3",
    "previousPlayer1Score": 1,
    "previousPlayer2Score": 0,
    "previousReturnedAt": "2026-09-21T07:22:49.000Z",
    "scrapedAt": "2026-09-21T07:27:49.000Z"
}
```

Notes on the columns:

- `playerOrder` is `as-listed`: player 1 is the upper name on Flashscore, not necessarily the winner. Use `winner` (1 or 2) and `winnerName`.
- `player1Score` / `player2Score` are sets won; for team ties (`matchType: "teams"`, `scoreUnit: "rubbers"`) they are rubbers won, `sets` is empty and `player1SetsWon` is null.
- `player1Players` / `player2Players` list both partners for doubles, null otherwise.
- `statusCode` is Flashscore's own status number; `statusText` is Flashscore's text for it; `matchStatus` is one of `scheduled`, `live`, `finished`, `retired`, `walkover`, `awarded`, `cancelled`, `interrupted`, `postponed`, `abandoned`, `delayed`, `awaiting-updates`, `to-finish` (or `ended` / `unknown` for a status number Flashscore adds later).
- `servingPlayer`, `servingPlayerName`, `currentGamePlayer1Points` and `currentGamePlayer2Points` are filled only while a match is live or interrupted (Flashscore keeps showing the points where an interrupted match stopped).
- `roundStatus`: `ok`, `not-shown` (the match page shows no round, as for Davis Cup rubbers), `unreadable` (the match page could not be read) or `not-requested` (`includeRound` off).
- `info` is Flashscore's note on the match, such as `Played on clay.` or `Change of nomination.`
- `change` (monitoring): `"first-check"` (not returned before for this filter set), `"status-changed"` or `"score-changed"`.
- Odds and TV listings are not included.

#### Rows that are not charged

When something is not returned, a row says why. These rows have no `matchId` and cost nothing:

| `status` | Meaning |
|---|---|
| `no-results` | Flashscore lists no matches for the day(s) asked for. |
| `no-filter-match` | Matches were read, none fit your filters. |
| `no-change` | Monitoring: no match changed since its last returned row. |
| `not-returned` | More matches fit than `maxMatches`; the row says how many. |
| `budget-reached` | The run hit its maximum total charge; the row says how many matches were not returned. |
| `day-out-of-range` | Flashscore did not return the day (outside its 7-back / 7-ahead window). |
| `blocked` | Flashscore refused the request even after retrying. |
| `unreadable` | Flashscore's answer could not be read. Nothing is guessed. |
| `invalid-input` | The input could not be used; nothing was requested. |

### Monitoring

Run the Actor on a schedule with `monitoringMode` on. The first run returns every match that fits your filters. Later runs compare each match with the row last returned for it and return only the ones that changed at your `changeDetail` level, with the previous score and status next to the new ones.

- Every match compared is a **check**, also the first time a match is seen. A match that was already returned as finished (or cancelled) and has not changed is not charged a check.
- A match is listed on the day it starts. For a schedule that runs across midnight, put `["yesterday", "today"]` in `days`, or a late match that finishes after midnight drops off the list before its result is returned.
- With `matchStatuses: ["live"]` a match leaves your filter when it ends, so its final result is not returned. Keep `finished` in the filter to receive results.
- A match that looks older than the row already returned for it (an older cached copy of Flashscore's feed, or a score Flashscore corrected downwards) is not returned as a change.
- Rows are remembered per filter set (`tours`, `matchTypes`, `matchStatuses`, `players`, `tournaments`, `changeDetail`) for 10 days after the match starts, so two schedules with different filters do not affect each other.
- `maxMatches` is not used in monitoring mode, so no match is left out behind the limit.
- A row is remembered only after it was delivered and charged. If a run hits its maximum total charge, the matches it could not return are not remembered as returned, so they still count as changed when they are compared again.
- Do not put the same filter set in two schedules that can run at the same time: the remembered rows live in a key-value store without atomic updates, so this cannot be fully prevented.

### Pricing

Pay per event:

- **$3.00 per 1,000 match rows**, charged only for rows with `status: "ok"`.
- **$0.30 per 1,000 match checks** in monitoring mode.

Example: 30 matches you follow, checked every 5 minutes for 10 hours, is 3,600 checks, which is **$1.08**, plus $0.003 for each change row. Checking every match of a busy day (about 300) every minute would be 432,000 checks a day, so use filters for frequent schedules.

### Notes

- Unofficial. Not affiliated with Flashscore or Livesport. Data is what Flashscore shows publicly and can change or be corrected by Flashscore.
- Only Flashscore's tennis pages and feeds that its robots.txt allows are read; draws, standings and news are not.
- Requests are spaced out: a 0.4 s pause before each request, and at most 3 match pages in flight at once when reading rounds.

# Actor input Schema

## `days` (type: `array`):

Which days to read: today, yesterday, tomorrow, a number of days from today (-7 to 7), or a date as YYYY-MM-DD. Flashscore's daily feed covers 7 days back and 7 days ahead of today; other days are refused before anything is requested. Empty = today. Each day is read once; a match is returned under the day Flashscore's own page lists it on.

## `utcOffsetHours` (type: `integer`):

Where a day starts and ends, as on Flashscore when your browser is in that time zone. 0 = UTC. Example: -5 for New York in winter, 1 for Paris in winter, 9 for Tokyo. startTime is always in UTC.

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

Keep only these tours. Empty = all. Challenger Women are WTA 125 events. Teams = Davis Cup / Billie Jean King Cup / Laver Cup ties (one row per tie); the individual Davis Cup rubbers are listed under ATP.

## `matchTypes` (type: `array`):

Keep only these. Empty = all.

## `matchStatuses` (type: `array`):

Keep only these. live = in play (also sets and tiebreaks in progress). finished = finished, retired, walkover or awarded. scheduled = not started. other = cancelled, interrupted, postponed, delayed and similar. Empty = all.

## `players` (type: `array`):

Keep a match only if one of these appears in either side's name or name slug (case-insensitive, partial). Flashscore writes names as 'Sinner J.' and slugs as 'sinner-jannik', so sinner or jannik both work. Doubles partners are matched too.

## `tournaments` (type: `array`):

Keep a match only if the tournament header contains one of these (case-insensitive, partial). Example: us open, davis cup, qualification.

## `includeRound` (type: `boolean`):

The round is not in Flashscore's daily feed. On = one extra small request per returned match to read it from the match page (only the first 32 KB are read). Off = faster, round is null.

## `maxMatches` (type: `integer`):

How many match rows to return. You are charged for the rows you actually receive. A busy day carries 200-400 matches across all tours. Not used in monitoring mode, which always checks every match that fits your input.

## `monitoringMode` (type: `boolean`):

Off = return every match that fits your input. On = remember the last row returned for each match and, on later runs, return only matches whose score or status changed (see Change detail). The first run returns everything. Each match compared is a check with a small fee, also the first time; a match already returned as finished that has not changed is not charged. For a schedule that runs across midnight use days yesterday and today, and keep finished in Match status, or final results can be missed. Remembered per filter set, for 10 days.

## `changeDetail` (type: `string`):

What counts as a change in monitoring mode. status = any change of Flashscore's match status (start, end, next set, tiebreak, break, medical timeout and similar) or of the sets won. games = also every game won in any set (and tiebreak points). points = also every point of the current game and the server.

## `resetMonitoringState` (type: `boolean`):

Clear the remembered rows for this filter set before this run, so every match counts as new again.

## `useProxy` (type: `boolean`):

The feed answered from a plain Apify datacentre address when this Actor was built (2026-09-21). On = if Flashscore answers 401, 403, 429 or 503, the request is retried through a proxy. Off = retried directly, then returned as a free row.

## Actor input object example

```json
{
  "days": [
    "today"
  ],
  "utcOffsetHours": 0,
  "tours": [],
  "matchTypes": [],
  "matchStatuses": [],
  "players": [],
  "tournaments": [],
  "includeRound": true,
  "maxMatches": 200,
  "monitoringMode": false,
  "changeDetail": "games",
  "resetMonitoringState": false,
  "useProxy": true
}
```

# Actor output Schema

## `results` (type: `string`):

One row per tennis match on Flashscore for the days asked for: players with slug, id and country (both partners for doubles), tournament, tour, singles/doubles/team tie, surface, round, start time, match status with Flashscore's status text, sets won, games per set with tiebreak points, score text and winner; while live, the set in play, the server and the current game points; in monitoring mode, what changed and the previous score and status. Days with no matches, filters that match nothing, refused requests and runs that hit their maximum charge come back as free rows that say why.

# 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 = {
    "days": [
        "today"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/flashscore-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 = { "days": ["today"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/flashscore-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 '{
  "days": [
    "today"
  ]
}' |
apify call neverempty/flashscore-tennis-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/flashscore-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/Bjb8N4nDIZBa8HhvQ/builds/moEeV27jG149vlJJU/openapi.json
