# Tennis Scraper (`deriverge/tennis-scraper`) Actor

ATP and WTA match results from ESPN, one row per singles or doubles match with the round, games per set, tiebreak points and ESPN's result line. One date returns the full draws of every tournament running that day, qualifying rounds included.

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

## Pricing

from $0.50 / 1,000 results

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

Tennis Scraper reads ESPN's ATP and WTA scoreboards and returns one row per match: tournament, draw, round, both players or doubles pairs with their countries, games per set with tiebreak points, and ESPN's result line, such as `(2) Andrey Rublev (RUS) bt Kyrian Jacquet (FRA) 7-6 (7-0) 6-1`. Pick a date, raise **Maximum rows** above the prefilled 100 and click **Start**: every tournament running that day comes back with its whole draw, qualifying included. Rankings, point-by-point data and player statistics are not included. It costs $1.00 per 1,000 results on the Free plan ($0.50 on Business), and the $5 of monthly credit in Apify's Free plan covers about 5,000 results.

### What does a match row contain?

| Field | Description |
|---|---|
| `league`, `name` | Tour (`atp` or `wta`) and tournament name, e.g. `AITO Hangzhou Open`. |
| `draw` | The event within the tournament: `Men's Singles`, `Men's Doubles`, `Women's Singles` or `Women's Doubles`. |
| `round` | Round as ESPN names it, from `Qualifying 1st Round` and `Qualifying Final` through `Round 1`, `Round 2`, `Quarterfinal` and `Semifinal` to `Final`. |
| `status`, `statusDetail`, `completed` | `pre`, `in` or `post`. After a match `statusDetail` reads `Final`, `Retired`, `Walkover` or `Canceled`, during one `2nd Set`, and before one the start time in US Eastern time. |
| `period` | Sets played so far. |
| `home`, `away` | The two sides in the order ESPN lists them, since tennis has no home player. Each has `kind` (`athlete` in singles, `pair` in doubles), `name`, `location` (the country) and `winner`. |
| `home.periodScores`, `home.tiebreaks` | Games won in each set, and the tiebreak points of each set, `null` where a set had no tiebreak. |
| `result` | ESPN's score line with seeds in brackets, `ret` after a retirement and `w/o` for a walkover. `null` before the match starts. |
| `startsAt` | Scheduled start in UTC. |
| `venue.name` | City and country of the tournament, e.g. `Hangzhou, China PR`. |
| `url` | The tournament's scoreboard on espn.com. |

A doubles pair is one side. Its `name` joins both players (`Francisca Jorge / Matilde Jorge`), `location` joins their countries when they differ (`Great Britain / Poland`), and a deciding match tiebreak shows up as a third set won 1 to 0, as in `6-3 3-6 1-0 (10-3)`. **Tennis news** rows carry ESPN's tennis stories with `title`, `summary`, `publishedAt` and `url`.

### How to scrape ATP and WTA results

1. In **Leagues**, keep `atp` and `wta` or remove one. Each tour includes singles and doubles.
2. Leave **What to return** on **Matches and results**, or pick **Tennis news** for ESPN's latest tennis stories.
3. Enter the same date in **From date** and **To date**. ESPN answers with the draws of every tournament running that day, so one date covers a tournament week, and a longer range adds the following weeks with each match once.
4. Set **Game status** to **Finished** for results only, or to **Not started yet** for upcoming matches.
5. Click **Start**. The **Output** tab shows the matches as a table; download it as CSV, Excel or JSON, or call the actor through the API:

```json
{
  "leagues": ["atp", "wta"],
  "mode": "scoreboard",
  "dateFrom": "2026-09-28",
  "dateTo": "2026-09-28",
  "statusFilter": "final",
  "maxItems": 1000
}
```

### Example output

A men's singles semifinal from a run on 28 September 2026. Jacquet is `home` only because ESPN lists him first.

```json
{
  "key": "espn:atp:183374",
  "type": "game",
  "sport": "tennis",
  "league": "atp",
  "leagueName": "ATP Tour",
  "gameId": "183374",
  "name": "AITO Hangzhou Open",
  "shortName": "AITO Hangzhou Open",
  "round": "Semifinal",
  "startsAt": "2026-09-28T09:40:00.000Z",
  "status": "post",
  "statusDetail": "Final",
  "completed": true,
  "period": 2,
  "clock": null,
  "seasonYear": 2026,
  "seasonType": null,
  "home": {
    "id": "7509",
    "kind": "athlete",
    "name": "Kyrian Jacquet",
    "shortName": "K. Jacquet",
    "location": "France",
    "score": null,
    "winner": false,
    "record": null,
    "position": 1,
    "homeAway": "home",
    "periodScores": [
      6,
      1
    ],
    "tiebreaks": [
      0,
      null
    ],
    "logo": "https://a.espncdn.com/i/teamlogos/countries/500/fra.png"
  },
  "away": {
    "id": "2642",
    "kind": "athlete",
    "name": "Andrey Rublev",
    "shortName": "A. Rublev",
    "location": "Russia",
    "score": null,
    "winner": true,
    "record": null,
    "position": 2,
    "homeAway": "away",
    "periodScores": [
      7,
      6
    ],
    "tiebreaks": [
      7,
      null
    ],
    "logo": "https://a.espncdn.com/i/teamlogos/countries/500/rus.png"
  },
  "venue": {
    "name": "Hangzhou, China PR",
    "city": null,
    "state": null,
    "country": null,
    "indoor": null
  },
  "attendance": null,
  "neutralSite": null,
  "broadcasts": [],
  "odds": null,
  "url": "https://www.espn.com/tennis/scoreboard/tournament/_/eventId/1001-2026/competitionType/1",
  "draw": "Men's Singles",
  "result": "(2) Andrey Rublev (RUS) bt Kyrian Jacquet (FRA) 7-6 (7-0) 6-1"
}
```

### How ESPN lists a tournament week

A request for one day returns every match ESPN has for each tournament running on it: finished rounds, matches in play and scheduled ones. On 27 September 2026 a run without dates returned 505 matches from 9 tournaments, the Chengdu Open and the AITO Hangzhou Open on the ATP side and seven WTA events, among them the Korea Open and the Singapore Tennis Open. Later rounds are listed before their players are known, with `TBD` as the name and the unfilled text `M/d - 'TBD'` in `statusDetail`, and they fill in as the draw moves on.

For a live feed, turn on **Return only what changed since the last run**, give the run a **Watch name** and schedule it. Each run returns new matches and matches whose status text, games per set, tiebreak points or winner changed since the run before. A match in play therefore comes back as its games move, and a later-round match comes back when ESPN puts a player in its `TBD` slot.

### How much does it cost to scrape tennis results?

| | Free plan | Starter | Scale | Business |
|---|---|---|---|---|
| 1,000 results | $1.00 | $0.80 | $0.65 | $0.50 |

You pay only for the events in the table. There is no start fee, and compute time and proxies are included.

The 505 matches of that run cost $0.505 on the Free plan and $0.2525 on Business.

### Limits

- There are no rankings, player statistics such as aces or break points, point-by-point data, court surface or prize money. Seeds appear only inside `result`.
- ESPN listed no betting lines for ATP or WTA matches in September 2026, so `odds` stays `null`.
- Canceled matches come back as `post` with `completed` set to `false` and no `result`. **Finished** leaves them out.
- One date can hold several hundred matches. Tours are read in the order given, so once `atp` alone fills **Maximum rows**, `wta` is not read at all: runs for 25 September 2026 with the prefilled 100 returned ATP matches only. A run without the field stops at 2,000.
- A run covers at most 120 days.
- `venue` gives the city and country only, without the court.

### FAQ

#### Is it legal to scrape tennis results from ESPN?

Results, draws and schedules are public sports facts that ESPN shows to anyone, and the actor reads them from ESPN's public JSON feeds without an account. Player names appear because they are part of the result; nothing else about the players is collected. You are responsible for how you use and republish the data.

#### How are retirements and walkovers shown?

`statusDetail` says `Retired` or `Walkover`, `completed` is `true` and the `winner` flag is set. A retirement keeps the games played so far, e.g. `6-2 1-0 ret`; a walkover has empty set scores and `w/o` at the end of `result`.

### Related scrapers

- [ESPN Sports Scraper](https://apify.com/deriverge/sports-data-scraper)
- [Golf Leaderboard Scraper](https://apify.com/deriverge/golf-scraper)
- [Soccer Scraper](https://apify.com/deriverge/soccer-scraper)
- [Formula 1 Scraper](https://apify.com/deriverge/f1-scraper)

### Support

This actor is built and maintained by deriverge s.r.o., a software company based in the Czech Republic. If a run fails or a field you need is missing, please open an issue in the **Issues** tab or write to us at info@deriverge.com. We respond in English and Czech. Runs can be scheduled in Apify Console or started from the **API** tab, which has examples for Python, JavaScript and cURL and works with Make, Zapier, n8n and the Apify MCP server. If the actor saves you time, a short review helps other people find it.

# Changelog

This Actor's version history is a separate document: https://apify.com/deriverge/tennis-scraper/changelog.md

# Actor input Schema

## `leagues` (type: `array`):

Tours to read: `atp` (men) and `wta` (women). Both include singles and doubles.

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

Matches and results is one row per match for every tournament ESPN lists on the chosen dates. Tennis news is ESPN's latest tennis stories.

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

First day of the fixture range. The source answers one day per request, so the actor splits the range into days and merges the results. Leave both dates empty for today. You can also give a moving date instead of a fixed one: today, tomorrow, yesterday, or a day offset such as +7 or -3. A saved run or a schedule then keeps returning the current fixtures instead of going empty once the dates pass.

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

Last day of the fixture range, inclusive. Ranges longer than 120 days are cut at 120 so a typo cannot run up a bill. You can also give a moving date instead of a fixed one: today, tomorrow, yesterday, or a day offset such as +7 or -3. A saved run or a schedule then keeps returning the current fixtures instead of going empty once the dates pass.

## `statusFilter` (type: `string`):

Games removed by this filter are never charged.

## `newOnly` (type: `boolean`):

Keeps a snapshot per watch name (or per saved task) and returns only matches that are new or whose status, games in a set, tiebreak points or winner changed. Schedule it every few minutes and you have a live feed that bills only for real changes.

## `watchName` (type: `string`):

Name of the snapshot used by the change mode, for example "my-league". Runs from a saved task get a snapshot automatically even without a name.

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

Hard cap on returned rows across all leagues in the run.

## Actor input object example

```json
{
  "leagues": [
    "atp",
    "wta"
  ],
  "mode": "scoreboard",
  "dateFrom": "2026-09-25",
  "dateTo": "2026-09-25",
  "statusFilter": "all",
  "newOnly": false,
  "maxItems": 100
}
```

# Actor output Schema

## `rows` (type: `string`):

One row per game, team, athlete, standing or article, in one schema across every sport.

## `changes` (type: `string`):

Rows that appeared, disappeared or whose status or score moved compared with the previous snapshot of the same watch name or task.

## `summary` (type: `string`):

Per-league request counts, skipped leagues with reasons, and totals.

# 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 = {
    "leagues": [
        "atp",
        "wta"
    ],
    "dateFrom": "2026-09-25",
    "dateTo": "2026-09-25",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("deriverge/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 = {
    "leagues": [
        "atp",
        "wta",
    ],
    "dateFrom": "2026-09-25",
    "dateTo": "2026-09-25",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("deriverge/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 '{
  "leagues": [
    "atp",
    "wta"
  ],
  "dateFrom": "2026-09-25",
  "dateTo": "2026-09-25",
  "maxItems": 100
}' |
apify call deriverge/tennis-scraper --silent --output-dataset

```

## MCP server setup

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