# Flashscore Tennis Scraper — Live Scores, Results & Stats (`scrapersdelight/flashscore-tennis-scraper`) Actor

Scrape every tennis match on Flashscore for any day in its 9-day window — ATP, WTA, Challenger and ITF, singles and doubles — with set-by-set scores, tiebreaks, winner, live game state, players, country, surface, and optional full match statistics. No API key.

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

## Pricing

$0.50 / 1,000 per 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.

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

## 🎾 Flashscore Tennis Scraper — live scores, results, fixtures & match statistics

Every tennis match [Flashscore](https://www.flashscore.com/tennis/) publishes, for any day in its
9-day window, as one clean row: **ATP, WTA, Challenger and ITF**, singles *and* doubles, with the
set-by-set score, tiebreaks, the winner, the live game state, both players, their countries, the
tournament, the surface — and, if you want it, the **full match statistics sheet**.

No API key. No login. No browser. A full day of world tennis comes back in about **4 seconds**.

***

### 📊 What you get, measured — not estimated

Every number below was measured, not guessed. The parser was validated **offline against real
captured bytes** on **2026-09-02**: four live day feeds (offsets −2, −1, 0, +1), **1,249 match
records → 1,236 unique matches**, and **0 integrity failures**.

| Measurement | Result |
|---|---|
| Matches parsed in the validation sample | **1,236 unique** across 4 days |
| Base fields per match | **51** — **33 populate on 100% of rows** |
| Fields per row with statistics on | **120** (measured on a live Apify run) |
| Tournament headers decoded | **84 / 84** (tour, name, country, surface, qualification stage) |
| Winner + set-by-set score on *finished* matches (n = 566) | **97%** |
| Live game points + who is serving on *live* matches (n = 12) | **100%** |
| Both partners + partner countries on *doubles* (n = 359) | **100%** |
| Statistics sheets returned for finished matches (n = 25 sampled) | **24** — 2,452 stat rows, **~102 per match** |
| Statistics columns populated when a sheet exists | **68 / 68 at 100%** |
| Full unfiltered live run, 5 days (−3 … +1) | **1,340 matches in 4.1 s** |

Tours in that sample: ITF Men 454 · ITF Women 397 · Challenger Men 195 · WTA 112 · ATP 78.
Surfaces: hard 687 · clay 549.

***

### 🎯 What does this actor do?

It reads Flashscore's own live-score feed — the same feed the website's scoreboard is drawn from —
and turns it into a flat dataset you can put straight into a model, a spreadsheet or a database.

- 🏆 **Every tour, one row shape.** Grand Slam and ITF M15 come back identically structured.
- 🗓️ **Any day Flashscore serves:** 7 days of results back, today, and tomorrow's fixtures.
- 🎾 **Set-by-set scores with tiebreak points** — `7-6(5), 5-7, 4-6, 6-2, 6-1`, plus a structured
  `sets` array so you never have to parse a string.
- 🔴 **Live matches carry the live state:** current game points for both players and who is serving.
- 👥 **Doubles handled properly** — both partners and both partner countries on each side, not a
  mangled `"A./B."` string.
- 📈 **Optional full statistics:** aces, double faults, 1st/2nd serve percentages and points won,
  break points saved and converted, average 1st/2nd serve speed, winners, unforced errors, net
  points, service / return / total points and games won, distance covered — **for the match and for
  every individual set**.
- 🔗 **Stable public URLs** for the match, the tournament and both players.
- 🌍 **Timezone-aware day boundaries**, so "yesterday" means yesterday where your users are.

### 🚫 What it deliberately does *not* do

One site, one job. These are different row shapes and belong in different actors, and saying so is
more useful than shipping half of each:

- **Head-to-head history.** Flashscore's H2H feed measured **120–170 KB per match** and its rows are
  *past matches between two players* — a different product, and pulling it per match would multiply
  your bill for data most callers do not want.
- **Player rankings.** A ranking row is a player, not a match.
- **Point-by-point.** The point-by-point feed slug returns a literal `0` body on this endpoint
  (measured), so no honest actor can claim it from here today.
- **Odds.** Flashscore ships bookmaker affiliate blocks in the feed; they are stripped, not sold.

***

### 🚀 Quick start

```jsonc
{
  "daysBack": 1,            // yesterday + today
  "includeTomorrow": true,  // + tomorrow's fixtures
  "includeStats": false,
  "maxItems": 300
}
```

**Yesterday's ATP + WTA results with full statistics:**

```jsonc
{
  "daysBack": 1,
  "includeTomorrow": false,
  "tours": ["ATP", "WTA"],
  "matchStatus": "finished",
  "includeStats": true,
  "maxItems": 0             // 0 = every match that matches
}
```

**One tournament, the whole week:**

```jsonc
{
  "daysBack": 7,
  "tournamentQuery": ["US Open"],
  "includeStats": true,
  "maxItems": 0
}
```

**Everything a player played this week:**

```jsonc
{ "daysBack": 7, "playerQuery": ["Djokovic"], "includeStats": true, "maxItems": 0 }
```

**Live right now:**

```jsonc
{ "daysBack": 0, "includeTomorrow": false, "matchStatus": "live", "maxItems": 0 }
```

***

### 📤 Output

One row per match. Real row from a live run (statistics fields truncated):

```json
{
  "matchId": "hbCCx5Fj",
  "matchUrl": "https://www.flashscore.com/match/hbCCx5Fj/",
  "startTime": "2026-08-30T23:15:00.000Z",
  "startTimestamp": 1788131700,
  "endTime": "2026-08-31T03:55:45.000Z",
  "date": "2026-08-30",
  "dayOffset": -2,

  "tour": "ATP",
  "competition": "ATP - Singles",
  "discipline": "singles",
  "gender": "men",
  "tournament": "US Open",
  "tournamentCountry": "USA",
  "tournamentStage": null,
  "surface": "hard",
  "tournamentUrl": "https://www.flashscore.com/tennis/atp-singles/us-open/",

  "status": "finished",
  "statusCode": 3,
  "endedEarly": false,
  "winner": "home",
  "winnerName": "Navone M.",
  "note": "Playing under a closed roof.",

  "homeName": "Navone M.",
  "awayName": "Djokovic N.",
  "homeCountry": "Argentina",
  "awayCountry": "Serbia",
  "homeCode": "NAV",
  "awayCode": "DJO",
  "homePlayerUrl": "https://www.flashscore.com/player/navone-mariano/YoPUeL3o/",
  "awayPlayerUrl": "https://www.flashscore.com/player/djokovic-novak/AZg49Et9/",

  "homeSets": 3,
  "awaySets": 2,
  "setsScore": "3-2",
  "setsString": "7-6(5), 5-7, 4-6, 6-2, 6-1",
  "sets": [
    { "set": 1, "home": 7, "away": 6, "homeTiebreak": 7, "awayTiebreak": 5 },
    { "set": 2, "home": 5, "away": 7, "homeTiebreak": null, "awayTiebreak": null }
  ],

  "homeGamePoints": null,
  "awayGamePoints": null,
  "serving": null,

  "acesHome": 9, "acesAway": 10,
  "doubleFaultsHome": 8, "doubleFaultsAway": 10,
  "firstServePctHome": 63, "firstServePctAway": 55,
  "breakPointsSavedHome": 8, "breakPointsSavedHomeOf": 12,
  "avgFirstServeSpeedHome": 179, "avgFirstServeSpeedAway": 183,
  "winnersHome": 36, "winnersAway": 46,
  "unforcedErrorsHome": 34, "unforcedErrorsAway": 70,
  "distanceCoveredMHome": 3874, "distanceCoveredMAway": 3300,

  "statistics": [
    { "scope": "Match", "group": "Service", "stat": "Aces", "home": "9", "away": "10",
      "homeValue": 9, "awayValue": 10 },
    { "scope": "Set 1", "group": "Service", "stat": "1st serve percentage", "home": "46%",
      "away": "54%", "homeValue": 46, "awayValue": 54 }
  ],

  "scrapedAt": "2026-09-02T04:30:00.000Z"
}
```

#### Field notes

- `status` is `scheduled` · `live` · `finished`. `statusCode` is Flashscore's own finer code, passed
  through raw. `endedEarly` is `true` when a match finished on a code other than the normal one —
  a retirement, walkover or abandonment. Those rows keep whatever score was reached, and `winner`
  is `null` when no result was declared.
- `sets` is structured; `setsString` is the human string. `homeTiebreak` / `awayTiebreak` are the
  tiebreak *points* for that set, present only when a tiebreak was played.
- `serving` maps Flashscore's serve indicator to `home` / `away` on live matches.
- Every percentage stat that ships as `X% (won/total)` also gets `…Of` columns with the denominator,
  so you can re-derive rates yourself instead of trusting a rounded percentage.

***

### 💵 Pricing

| Event | Price | When it fires |
|---|---|---|
| **Per match returned** | **$0.0005** | Once per match written to your dataset |

That is **half the cheapest per-match price in this category** (measured against the live store on
2026-09-02), and the statistics sheet is **included in the same charge** — there is no separate
per-statistics fee. 1,000 matches with full statistics = **$0.50**.

Matches removed by your filters are never charged, and billing is budget-aware: rows are delivered
and charged in the same call, so if you set a spend cap the run stops cleanly instead of handing you
rows you already paid past.

***

### ❓ FAQ

**How far back can I go?**
Seven days. Measured on 2026-09-02: day offsets −7 through +1 return data; −8 and older return an
empty body, and +2 through +7 return a valid but empty response — Flashscore simply does not publish
tennis further out. Ask for a date outside that window and the actor logs a warning and skips it
rather than pretending it found nothing.

**Do I need a proxy?**
No. The feed answered HTTP 200 from a home IP, from Apify's own network, from Apify datacenter
proxies and from Apify residential proxies when this actor was built. A proxy option is there if you
want to spread very large sweeps across IPs.

**Does it need an API key or a Flashscore account?**
No. The feed is the same public one the website's own scoreboard reads.

**How fast is it?**
A full unfiltered 5-day sweep returned **1,340 matches in 4.1 seconds**. With statistics on, each
match costs one extra request, run 8 at a time by default.

**How many matches is a normal day?**
Roughly 500–600 across all tours — the sample day carried 571 matches in 66 tournament sections.
That is why `maxItems` defaults to 300; set it to `0` for everything.

**Are doubles supported?**
Yes, and properly: `homePlayer1` / `homePlayer2`, `awayPlayer1` / `awayPlayer2` and both partner
countries. 359 doubles matches in the validation sample, all four fields at 100%.

**Are ITF and Challenger matches included?**
Yes — they are the bulk of the feed. In the validation sample ITF and Challenger were 1,046 of the
1,236 matches, versus 190 for ATP + WTA combined.

**Can I get statistics for a match that has not finished?**
Live matches usually publish a partial sheet. Scheduled matches have none, so the actor skips them
automatically instead of burning a request per fixture.

**Some finished matches have no statistics. Why?**
Flashscore does not publish a statistics sheet for every event — small ITF draws in particular. In
the sample, 24 of 25 finished matches had one. The row still ships, with an empty `statistics`
array, so you can see the gap instead of silently missing rows.

**What happens if Flashscore blocks a request?**
Blocked requests are counted, named in the run's status message, and never reported as "no matches".
If nothing at all could be read, the run fails loudly with the transport reason rather than handing
you an empty dataset that looks like a quiet day of tennis.

**What if the feed's signing token changes?**
The actor pins the current token and, on a 401/403, re-reads it automatically from Flashscore's own
public script before retrying.

**Can I filter to just one tournament or one player?**
Yes — `tournamentQuery` and `playerQuery` are case-insensitive substring matches. Flashscore prints
names as `Surname X.`, so search by surname.

**Does it return odds?**
No. Bookmaker affiliate blocks are stripped from the feed and not sold on.

***

### ⚖️ Legal & fair use

This actor reads a publicly available feed with no login, no paywall and no credential of any kind.
It fetches at a polite rate and does not attempt to defeat any access control.

`https://global.flashscore.ninja/robots.txt` returns **404** — that host publishes no robots policy.
`https://www.flashscore.com/robots.txt` (checked 2026-09-02) states for the generic crawler:

```
User-agent: *
Disallow: /standings/
Disallow: /draw/
Disallow: /newsfeed/
Disallow: */x/js/browsercompatibility*.js
```

None of those paths are requested by this actor.

Sports facts — who played, what the score was — are not themselves copyrightable in most
jurisdictions, but the compilation, the site's terms of service and your own jurisdiction are your
responsibility. Use the output for analysis, modelling and research; do not republish Flashscore's
compilation wholesale. This actor is not affiliated with or endorsed by Flashscore or Livesport s.r.o.

# Actor input Schema

## `daysBack` (type: `integer`):

How many past days to scrape, counting back from today. 0 = today only, 7 = the full week of results Flashscore keeps online. Flashscore serves nothing older than 7 days, so this is capped at 7.

## `includeTomorrow` (type: `boolean`):

Also scrape the next day's schedule. Flashscore publishes tennis one day ahead and no further, so this is the only forward-looking day available.

## `dates` (type: `array`):

Scrape exact calendar dates as YYYY-MM-DD, e.g. 2026-09-01. Overrides Days back / Include tomorrow. Any date outside Flashscore's 7-days-back to 1-day-ahead window is skipped with a warning in the log.

## `timezoneOffset` (type: `integer`):

Which timezone decides where one day ends and the next begins, and how start times are grouped. 0 = UTC (default), -5 = US Eastern, 1 = Central Europe. Match start times are always returned in UTC ISO format regardless.

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

Only return matches from these tours. Values seen live: ATP, WTA, CHALLENGER MEN, CHALLENGER WOMEN, ITF MEN, ITF WOMEN. Case-insensitive, partial match allowed ("ITF" catches both ITF tours). Leave empty for every tour.

## `discipline` (type: `string`):

Restrict to singles or doubles matches. Doubles rows carry both partners per side.

## `matchStatus` (type: `string`):

Restrict to scheduled (not started), live (in progress right now) or finished matches. `any` returns all three.

## `surfaces` (type: `array`):

Only return matches played on these surfaces, e.g. hard, clay, grass. Flashscore prints the surface on the tournament header; matches whose header carries no surface are excluded when this is set.

## `tournamentQuery` (type: `array`):

Only return matches whose tournament name contains one of these strings, e.g. "US Open" or "M15". Case-insensitive substring match.

## `playerQuery` (type: `array`):

Only return matches involving a player whose name contains one of these strings, e.g. "Djokovic". Flashscore prints names as "Surname X.", so search by surname. Case-insensitive substring match.

## `includeStats` (type: `boolean`):

Fetch the full statistics sheet for every finished or live match: aces, double faults, 1st/2nd serve percentages and points won, break points saved and converted, average serve speeds, winners, unforced errors, net points, service/return/total points and games won, and distance covered — for the match and for each individual set. Costs one extra request per match, and is charged at the same per-match rate (no extra fee). Scheduled matches are skipped automatically because they have no statistics yet.

## `statsConcurrency` (type: `integer`):

How many statistics requests to run in parallel when Include match statistics is on. 8 is comfortable; raise it for very large date ranges, lower it if you see blocked requests in the log.

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

Stop after this many matches. A busy tennis day is roughly 500-600 matches across all tours, so the default is deliberately small — raise it once you know the scope you want. 0 = no limit.

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

Optional. The Flashscore feed answered 200 from Apify's own network, from Apify datacenter proxies and from Apify residential proxies when this actor was built, so no proxy is needed by default. Switch one on if you run very large sweeps and want the requests spread across IPs.

## Actor input object example

```json
{
  "daysBack": 1,
  "includeTomorrow": true,
  "dates": [],
  "timezoneOffset": 0,
  "tours": [],
  "discipline": "any",
  "matchStatus": "any",
  "surfaces": [],
  "tournamentQuery": [],
  "playerQuery": [],
  "includeStats": false,
  "statsConcurrency": 8,
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `matches` (type: `string`):

The dataset of scraped tennis matches (one item per 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 = {
    "daysBack": 1,
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/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 = {
    "daysBack": 1,
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/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 '{
  "daysBack": 1,
  "maxItems": 50
}' |
apify call scrapersdelight/flashscore-tennis-scraper --silent --output-dataset

```

## MCP server setup

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