# Tennis Scraper - Matches, Live Scores, Results, H2H & Rankings (`orbots/tennis-scraper`) Actor

ATP, WTA, Challenger and ITF tennis data with no login: schedule, live scores with the server and game points, results with set-by-set scores, serve and return statistics, point by point, head-to-head records and ATP or WTA rankings. One row per match. From $0.005/run + $0.002/match.

- **URL**: https://apify.com/orbots/tennis-scraper.md
- **Developed by:** [Orbots](https://apify.com/orbots) (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

from $1.20 / 1,000 match (one row per match)s

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

## Tennis Scraper - Matches, Live Scores, Results, H2H & Rankings

> **Unofficial tool.** Orbots is not affiliated with, sponsored by, or endorsed by Flashscore (Livesport s.r.o.), TennisExplorer, the ATP, the WTA or the ITF. Those names are used here only to describe the public data this Actor collects.

Tennis data for **ATP, WTA, Challenger, ITF and team events**, with no login, no browser and no API key: the day's schedule, live scores with the server and the points of the current game, results with set-by-set scores and tiebreaks, serve and return statistics, the full point-by-point log, head-to-head records and the ATP or WTA ranking. One clean row per match, player pair or ranked player. **You only pay for rows that are actually delivered.**

### Why this Actor

- **Seven things in one Actor.** Pick a mode: `schedule`, `live`, `results`, `all`, `match`, `h2h` or `rankings`. Same row format for every match, whatever the source, so a schedule pulled in the morning and the results pulled at night line up by `match_id`.
- **Live scores that are actually live.** Live rows carry the set in play, games of every set, the points of the current game (`30:15`) and who is serving. Run it every few minutes with `onlyNew` and you get only the matches whose score moved.
- **Statistics and point by point.** Turn on `includeDetails` (or use the `match` mode) to get aces, double faults, first serve percentage, break points saved and converted, points and games won per set, every game's point sequence with break points marked, and set durations.
- **Head-to-head two ways.** `h2h` mode takes plain player names ("Alcaraz vs Sinner") and returns their record, every previous meeting with year, tournament, surface, round and score, and both profiles. `includeH2h` attaches to every match row the last matches of both players and their mutual meetings.
- **Results back to the 1990s.** The last week comes from Flashscore; any older date is read from the TennisExplorer archive, with the closing odds of each match.
- **Schedule as soon as it exists.** Matches appear the moment the order of play is published, which in tennis is usually the evening before. A run in the morning gives you the whole day ahead.
- **Rankings with history.** ATP or WTA singles ranking, up to 2000 players, current edition or any past Monday.
- **Filters that reduce what you pay.** Tours, singles or doubles, tournament name, player names and a hard cap per run. Rows filtered out are not fetched in detail and not charged.
- **Fast and cheap to run.** Pure HTTP against public endpoints. In our benchmark, a full day of ATP and WTA singles came back in seconds for well under a cent of platform usage (numbers below).
- **Honest billing.** `maxMatches` is respected exactly. A run that delivers nothing is not charged at all, not even the start fee.

### What you get

#### Match rows (`schedule`, `live`, `results`, `all`, `match`)

```json
{
  "type": "match",
  "source": "flashscore",
  "match_id": "M3TdTTy8",
  "match_url": "https://www.flashscore.com/match/tennis/M3TdTTy8/",
  "tour": "ATP",
  "gender": "men",
  "match_type": "singles",
  "tournament": "Chengdu",
  "tournament_place": "China",
  "surface": "hard",
  "tournament_url": "https://www.flashscore.com/tennis/atp-singles/chengdu/",
  "date": "2026-09-25",
  "start_time": "2026-09-25T05:10:00Z",
  "status": "finished",
  "home_name": "Kopriva V.",
  "away_name": "Shapovalov D.",
  "home_players": ["Kopriva V."],
  "away_players": ["Shapovalov D."],
  "home_country": "Czech Republic",
  "away_country": "Canada",
  "home_url": "https://www.flashscore.com/player/kopriva-vit/EHGKxKDL/",
  "away_url": "https://www.flashscore.com/player/shapovalov-denis/zXddgn9o/",
  "home_sets": 0,
  "away_sets": 2,
  "sets": [
    {"set": 1, "home": 2, "away": 6, "home_tiebreak": null, "away_tiebreak": null, "duration": null},
    {"set": 2, "home": 4, "away": 6, "home_tiebreak": null, "away_tiebreak": null, "duration": null}
  ],
  "score": "2-6 4-6",
  "winner": "away",
  "serving": null,
  "game_points": null,
  "scraped_at": "2026-09-25T07:00:09Z"
}
```

`status` is one of `scheduled`, `live`, `finished`, `retired`, `walkover`, `interrupted`, `postponed`, `cancelled`, `awarded`, `abandoned` or `other`. Live rows also fill `live_set`, `serving` (`home` or `away`) and `game_points`. Doubles rows list both players of each side in `home_players` and `away_players`.

With `includeDetails` each Flashscore row also carries:

- `stats`: per period (`match`, `set_1`, `set_2`...) and group (`Service`, `Return`, `Points`, `Games`), every statistic with `home` and `away` values, exactly as the source prints them (`"53% (34/64)"`).
- `point_by_point`: one entry per set with every game: score after the game, `server`, `winner`, a `break` flag and the point sequence (`["15:0", "30:0", "40:0 |B1|", ...]`; `|B1|` marks a break point for the first player).
- `duration` of the match and of each set.

With `includeH2h` each Flashscore row carries `h2h`: the last matches of both players and their previous meetings, each with date, tournament, surface, score and winner.

Rows read from the TennisExplorer archive (results older than 7 days) have `source: "tennisexplorer"`, a `te-` prefixed `match_id`, the closing odds in `odds_home` and `odds_away`, seeds, and no statistics.

#### Head-to-head rows (`h2h`)

```json
{
  "type": "h2h",
  "query": "Alcaraz vs Sinner",
  "player_a_name": "Alcaraz Carlos",
  "player_b_name": "Sinner Jannik",
  "player_a_url": "https://www.tennisexplorer.com/player/alcaraz-5ab70/",
  "player_b_url": "https://www.tennisexplorer.com/player/sinner-8b8e8/",
  "wins_a": 12,
  "wins_b": 9,
  "meetings_count": 21,
  "meetings": [
    {"year": 2026, "tournament": "Monte Carlo", "surface": "clay", "round": "F", "winner": "b", "winner_name": "Sinner J.", "loser_name": "Alcaraz C.", "score": "7-6(5) 6-3", "sets": [{"set": 1, "winner": 7, "loser": 6, "winner_tiebreak": null, "loser_tiebreak": 5}, {"set": 2, "winner": 6, "loser": 3, "winner_tiebreak": null, "loser_tiebreak": null}]}
  ],
  "player_a_profile": {"atp_rankings": "3", "birthdate": "5. 5. 2003", "height": "185 cm", "weight": "72 kg", "plays": "right"},
  "player_b_profile": {"atp_rankings": "1", "birthdate": "16. 8. 2001", "height": "188 cm", "weight": "76 kg", "plays": "right"},
  "scraped_at": "2026-09-25T07:00:09Z"
}
```

#### Ranking rows (`rankings`)

```json
{
  "type": "ranking",
  "tour": "ATP",
  "ranking_date": "2026-09-21",
  "rank": 3,
  "rank_change": 0,
  "previous_rank": 3,
  "player_name": "Alcaraz Carlos",
  "player_url": "https://www.tennisexplorer.com/player/alcaraz-5ab70/",
  "country": "Spain",
  "points": 5560,
  "scraped_at": "2026-09-25T07:00:09Z"
}
```

### Input

| Field | What it does |
|---|---|
| `mode` | `schedule` (not finished yet), `live` (in play now), `results` (finished), `all` (every match of the day), `match` (details of given match IDs), `h2h` (player pairs) or `rankings`. Default `schedule`. |
| `dateFrom`, `dateTo` | Days to collect, YYYY-MM-DD, UTC. Empty = today. The schedule covers what the source has already scheduled (today and, once the order of play is out, tomorrow; the feed reaches 6 days ahead); results cover the last 7 days from Flashscore and any older date from the TennisExplorer archive. At most 31 days per run. |
| `tours` | Any of `ATP`, `WTA`, `Challenger`, `ITF`, `Teams`. Default ATP, WTA, Challenger and Teams; ITF is opt-in because it is often half of a day. |
| `matchType` | `singles` (default), `doubles` or `all`. |
| `tournament` | Keep only tournaments whose name contains this text. |
| `players` | Keep only matches with one of these names on either side. |
| `maxMatches` | Hard cap on match rows per run. Default 500. |
| `includeDetails` | Statistics, point by point and set durations on every Flashscore row (`match-detail` event). Always on in `match` mode. |
| `includeH2h` | Last matches and mutual meetings on every Flashscore row (`h2h` event). |
| `matchIds` | `match` mode: Flashscore match IDs (the `match_id` of any row) or Flashscore match URLs. |
| `pairs` | `h2h` mode: one pair per line, "Alcaraz vs Sinner" or "Swiatek - Sabalenka". |
| `rankingTour`, `rankingTop`, `rankingDate` | `rankings` mode: `atp`, `wta` or `both`; how many players (up to 2000); a past edition (Mondays). |
| `onlyNew` | Only matches that are new or changed since earlier runs of the same mode and filters (status, score or start time moved). History is kept for 30 days in your own account. |
| `proxyConfiguration` | Off by default. Both sources answer Apify servers directly. |

Minimal runs:

```json
{ "mode": "results", "tours": ["ATP", "WTA"], "matchType": "singles" }
```

```json
{ "mode": "live", "tours": ["ATP", "WTA", "Challenger"], "matchType": "all", "onlyNew": true }
```

```json
{ "mode": "h2h", "pairs": ["Alcaraz vs Sinner", "Swiatek vs Sabalenka"] }
```

### When a run returns nothing

The Actor separates "something broke" from "there was nothing to return":

- Every request refused by the sources even after retries: the run **fails** with a message, so a schedule notices.
- No match matched the date and filters, no player matched a name, or `onlyNew` found nothing new: the run **succeeds with a status message** explaining why, and nothing is charged.

Player names in `h2h` mode are looked up on TennisExplorer; when a surname is shared ("Cerundolo"), add the first name. The row's `player_a_name` and `player_b_name` show who was matched.

### Notes and limits

- Flashscore covers today minus 7 to plus 6 days, but tennis schedules only exist once the order of play is published, so in practice the schedule is today and tomorrow. Results before that window come from TennisExplorer, which lists finished matches with odds but without statistics or player countries.
- Rounds are not part of the day feeds and are left `null` on match rows; head-to-head meetings do carry the round.
- Times are UTC. A TennisExplorer row whose start time is not listed keeps `date` and leaves `start_time` empty.
- Live data is read at the moment of the run; a run that takes a minute reports the scores as they were when each row was read.
- Data comes from public pages as they are at run time. Sources change their markup now and then; the Actor is monitored daily and repaired when that happens.

### Pricing: what a run actually costs

Pay per event. A run that delivers nothing costs nothing.

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run, on the first delivered row |
| Match (`match`) | $0.002 per match on the free plan, down to $0.0012 on higher plans | Every match row delivered |
| Match details (`match-detail`) | $0.005 | Every match enriched with statistics and point by point (`includeDetails`, or `match` mode) |
| Head-to-head (`h2h`) | $0.005 | Every H2H row in `h2h` mode, or every match row enriched with `includeH2h` |
| Ranking row (`ranking`) | $0.001 | Every ranked player delivered |

Example: a day of ATP and WTA singles, about 60 matches, costs $0.005 + 60 x $0.002 = **$0.125** on the free plan; the same day with statistics and point by point costs $0.425. The top 100 of the ATP ranking costs $0.105. Platform usage on top of that was between $0.0001 and $0.002 per run in our benchmark runs.

### Benchmark (measured on Apify, 2026-09-25)

| Run | Rows | Time | Requests |
|---|---|---|---|
| Schedule of the day, ATP and WTA singles | 18 | 6 s | 1 |
| Results of one day with statistics, point by point and H2H | 8 | 7 s | 33 |
| Two days of results from the 2024 archive, singles and doubles | 279 | 23 s | 2 |
| ATP and WTA rankings, top 120 each | 240 | 20 s | 6 |

### Changelog

- **0.1** (2026-09-25): first release. Schedule, live, results, all, match, h2h and rankings modes; details and head-to-head enrichment; TennisExplorer archive for older results; onlyNew for schedules.

# Actor input Schema

## `mode` (type: `string`):

Schedule = matches not finished yet (upcoming and in play). Live = matches in play right now, with the current set, game and server. Results = finished matches with set-by-set scores. All = every match of the day whatever its status. Match = statistics and point by point of the match IDs in "matchIds". H2H = head-to-head record of the player pairs in "pairs". Rankings = the ATP or WTA ranking.

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

First day to collect, YYYY-MM-DD, UTC. Leave empty for today. The schedule covers what is already scheduled (today and, once the order of play is out, tomorrow); results cover the last 7 days from Flashscore and any older date (back to the 1990s) from TennisExplorer.

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

Last day to collect, inclusive. Leave empty for a single day. At most 31 days per run.

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

Keep only these tours. Teams = Davis Cup, Billie Jean King Cup and similar. Leave empty for all of them (ITF alone is often half of a day).

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

Singles only, doubles only, or both.

## `tournament` (type: `string`):

Keep only tournaments whose name contains this text (case-insensitive), for example "US Open" or "Chengdu".

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

Keep only matches where one of these names appears (case-insensitive, partial match), for example "Alcaraz". One name per line.

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

Hard cap on match rows per run. The Actor stops at exactly this number and never charges beyond it. Leave empty for no cap.

## `includeDetails` (type: `boolean`):

For every match row from Flashscore: serve and return statistics per set, the full point-by-point log of every game (server, break points, winner) and set durations. Three extra requests per match, charged as a match-detail event. Match mode always includes them.

## `includeH2h` (type: `boolean`):

For every match row from Flashscore: the last matches of both players and every previous meeting between them. One extra request per match, charged as an h2h event.

## `matchIds` (type: `array`):

Flashscore match IDs (the match\_id of any row this Actor delivered, such as M3TdTTy8) or Flashscore match URLs. One per line.

## `pairs` (type: `array`):

One pair per line, written as "Alcaraz vs Sinner" or "Swiatek - Sabalenka". Names are looked up on TennisExplorer; use the surname plus the first name when a surname is shared.

## `rankingTour` (type: `string`):

ATP (men) or WTA (women) singles ranking, or both.

## `rankingTop` (type: `integer`):

How many players to return from the top, up to 2000.

## `rankingDate` (type: `string`):

A past ranking edition, YYYY-MM-DD (editions are published on Mondays). Leave empty for the current ranking.

## `onlyNew` (type: `boolean`):

Built for schedules. The Actor remembers the status and score it delivered for each match and returns only matches that are new or changed (started, score moved, finished), so a run every hour reports what happened instead of repeating the whole day and charging for it again. History is kept for 30 days in your own account.

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

Not required: both sources answer Apify servers directly. Enable a proxy only if your runs start getting throttled.

## Actor input object example

```json
{
  "mode": "schedule",
  "tours": [
    "ATP",
    "WTA",
    "Challenger",
    "Teams"
  ],
  "matchType": "singles",
  "maxMatches": 500,
  "includeDetails": false,
  "includeH2h": false,
  "matchIds": [],
  "pairs": [
    "Alcaraz vs Sinner"
  ],
  "rankingTour": "atp",
  "rankingTop": 100,
  "onlyNew": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every row collected by the run, in delivery order. Each row carries a "type" field: match, h2h or ranking.

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

One row per match: tour, tournament, surface, players, start time, status, set-by-set score, winner, live serve and game points, and (when requested) statistics, point by point and head-to-head.

## `matchesCsv` (type: `string`):

The same matches as a spreadsheet-ready CSV (flat columns only).

## `headToHead` (type: `string`):

H2H rows: both players, their record against each other, profiles and every previous meeting.

## `rankings` (type: `string`):

Ranking rows: position, change, player, country and points.

# 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 = {
    "matchIds": [],
    "pairs": [
        "Alcaraz vs Sinner"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("orbots/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 = {
    "matchIds": [],
    "pairs": ["Alcaraz vs Sinner"],
}

# Run the Actor and wait for it to finish
run = client.actor("orbots/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 '{
  "matchIds": [],
  "pairs": [
    "Alcaraz vs Sinner"
  ]
}' |
apify call orbots/tennis-scraper --silent --output-dataset

```

## MCP server setup

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