# Tennis Stats Scraper - ATP & WTA Scores, Rankings (`datagrit/tennis-stats-scraper`) Actor

ATP and WTA tennis match results with set and tiebreak scores, seeds, rankings and player profiles for any date range.

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

## Pricing

Pay per event

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

### What does Tennis Stats Scraper do?

Tennis Stats Scraper returns ATP and WTA tennis matches for any date range as clean, flat rows: tournament, round, players with country and seed, status, winner, the games of every set and the tiebreak points of every tiebreak. The same run can add the current ATP and WTA singles rankings with points and movement, and player profiles with height, playing hand and season record, titles and prize money. It reads the public ESPN tennis JSON API, needs no proxy and no browser, and exports to JSON, CSV or Excel, the Apify API, n8n, Make or AI agents through MCP.

### Who is it for?

- **Sports bettors and modellers** who need results with set and tiebreak scores, retirements and walkovers for back-testing, or the published order of play for the next days.
- **Data scientists** building win-probability or rating models (Elo, Glicko) from several seasons of tour-level results in one run.
- **Journalists and fantasy players** who want a player's recent form, a head-to-head record or this week's results across all tournaments.
- **Apps and dashboards** that show a daily results feed; the only-new mode returns just what changed since the previous run.

### Example output

One match row (fields for sets 4 and 5 are null here and shortened):

```json
{
  "recordType": "match",
  "id": "186257",
  "tour": "ATP",
  "matchType": "singles",
  "tournamentName": "China Open",
  "tournamentId": "959",
  "season": 2026,
  "grandSlam": false,
  "round": "Qualifying 1st Round",
  "startTime": "2026-09-28T03:00:00.000Z",
  "status": "finished",
  "player1Name": "Yannick Hanfmann",
  "player1Country": "Germany",
  "player1CountryCode": "GER",
  "player1Seed": null,
  "player2Name": "Tomas Machac",
  "player2CountryCode": "CZE",
  "winner": 2,
  "winnerName": "Tomas Machac",
  "score": "7-6(9-7) 6-7(6-8) 6-4",
  "player1SetsWon": 1,
  "player2SetsWon": 2,
  "set1Player1": 6, "set1Player2": 7, "set1TiebreakPlayer1": 7, "set1TiebreakPlayer2": 9,
  "set2Player1": 7, "set2Player2": 6, "set2TiebreakPlayer1": 8, "set2TiebreakPlayer2": 6,
  "set3Player1": 4, "set3Player2": 6, "set3TiebreakPlayer1": null, "set3TiebreakPlayer2": null,
  "tiebreaks": 2,
  "retired": false,
  "walkover": false,
  "scoreConsistent": true,
  "resultNote": "Tomas Machac (CZE) bt Yannick Hanfmann (GER) 7-6 (9-7) 6-7 (6-8) 6-4",
  "sourceUrl": "https://www.espn.com/tennis/scoreboard/tournament/_/eventId/959-2026/competitionType/1"
}
```

| Data type | One row is | Key fields |
|---|---|---|
| matches | a singles or doubles match | tournament, round, start time, status, players, countries, seeds, winner, score, games and tiebreak points per set, retirement, walkover |
| rankings | a ranked player | tour, rank, previous rank, change, points, country and country code, age, ranking date |
| players | a player profile | country, birthplace, date of birth, height, weight, plays, pro debut, rank, season wins, losses, titles and prize money |

### How much does it cost?

You pay per row returned (match, ranking or player). Pricing depends on your Apify plan: a small fee when a run starts, then a price per result that is lower on paid plans. The Apify free plan includes monthly credit you can use to try it. Status rows (for example when nothing matched) are never charged, and matches outside your date range or filters are never returned or charged. You can set a maximum spend on the run and the Actor stops when it is reached. It reads a JSON API over plain HTTP, so runs are light on platform resources.

### Input

Keep **Data types** on `matches` or add `rankings` and `players`, pick a period, narrow it down if you need to, and run. For a daily feed, schedule it with **Only rows new since my last run**.

- **Data types** – `matches`, `rankings` and/or `players`.
- **Date from, Date to** – match dates in UTC, YYYY-MM-DD, for example a whole season. Future dates return the order of play that is already published. Leave Date from empty to use **Last days**.
- **Last days** – length of the window that ends on Date to (today by default) when Date from is empty; 3 by default.
- **Tours** – ATP, WTA and/or Mixed; empty for all.
- **Players** – names or ESPN player IDs. A name only has to be contained in the player's name; accents and case are ignored.
- **Head-to-head only** – with two or more Players, keep only matches between two of them.
- **Tournaments** – part of the tournament name or the ESPN tournament ID.
- **Match types** – singles and/or doubles. **Match statuses** – scheduled, in\_progress, finished, retired, walkover, postponed, cancelled, suspended.
- **Include qualifying** – switch off for main draws only. **Maximum rank** – rankings only, for example 10.
- **Oldest first** – matches come newest first by default, so Maximum results never cuts off today's results; switch this on for date order.
- **Only rows new since my last run** – see the FAQ. **Maximum results** – total limit for the run.

Input values that are not recognised are listed in the run status; if a list contains no valid value at all, the run fails with a message.

### What data do you get?

#### Matches

Every match of the ATP and WTA tournaments ESPN covers, including the Grand Slams with mixed doubles, qualifying rounds and doubles. Doubles rows name both partners of each pair. The tour of a match follows its draw: the women's matches of a combined event such as the China Open are WTA, mixed doubles are Mixed. Junior draws and legends (over-35 and over-45) events are skipped and counted in the run status. A match that ESPN lists in both the ATP and the WTA feed is returned once; if the two feeds differ at that moment, the more advanced state (finished over in progress over scheduled) is kept and the run status says so.

#### Scores and data quality

Scores are given from the winner's point of view with tiebreak points in brackets and a deciding match tiebreak (doubles and mixed doubles) in square brackets such as `[10-4]`; `ret.` marks a retirement and `w/o` a walkover. ESPN writes match tiebreaks in two ways, either as a 1-0 set with tiebreak points or as a plain set such as 10-4; both come out as `[10-4]`, with the points in the tiebreak fields of that set and 1-0 as its games. A plain 10-8 last set at a Grand Slam before 2023 stays as games, because it can be an advantage set. For finished matches, `scoreConsistent` is false when the published set scores do not add up to the recorded winner or ESPN's result note still says "leads" or "is tied with"; this happens for a few older matches that were never completed at the source. The run status counts how many completed matches carry set scores and how many players have a known country, and the run fails instead of returning empty scores if the source stops publishing them.

#### Rankings and players

Rankings are the current singles lists from ESPN's ranking endpoint, which holds the top 150 of the ATP and of the WTA (asking for more returns the same 150). The run status gives the share of ranked players with points and with a country code. Player profiles come from ESPN's player records with the season statistics of the player's tour.

### Is it legal to scrape this data?

The Actor reads the public JSON endpoints that serve ESPN's own tennis scoreboard, ranking and player pages. It does not log in, use cookies or bypass any access control. Match results, rankings and player facts such as height or date of birth are published facts about professional athletes. How you use the data, for example in a commercial product, is your responsibility. This description is not legal advice.

### FAQ

**Which tournaments are covered?** The ATP and WTA tour events that ESPN lists, from the Grand Slams down to the tour's smaller events, including qualifying and doubles. Challenger and ITF events are not in these feeds. Match data is available from about 2010; for 2005 ESPN lists the tournaments but no matches, and the run status reports tournaments without match data. Tournament names are the names ESPN uses today, also for older editions.

**Is court surface, match duration or point-by-point data included?** No. The source publishes scores, sets, tiebreaks, seeds and statuses, not surface, serve statistics, odds or point-by-point data.

**How long does a run take?** ESPN returns only the tournaments that start or end inside the requested dates, so the Actor starts its first request 23 days before Date from; that way a tournament already under way, such as the second week of a Grand Slam, is always included, and matches outside your dates are dropped. It asks for one calendar month per tour in a single request and waits about 0.7 seconds between requests. A daily feed of the last three days is two requests; January 2026 for both tours (1,298 matches, including the Australian Open) is two requests; a full season is about 24.

**How does the only-new mode work?** The Actor remembers, in a storage on your account, the rows it actually returned to you, separately for each combination of filters. Dates are not part of that combination, so a scheduled run with Last days keeps one feed. A match returned while scheduled or in progress is returned again once it is finished, and a ranking row again when a new ranking is released. Rows dropped by your filters or cut off by Maximum results are not remembered.

**How often should I schedule it?** Every few hours during tournaments for a results feed, daily for a history archive. A run that finds nothing new returns one free status row.

**Which player IDs can I use?** The ESPN IDs from the `player1Id`, `player2Id` or `playerId` fields, for example 3623 for Jannik Sinner. Names are looked up among the current top 150 of each tour and among the matches read in the same run.

**What happens when the source changes?** If the scoreboard changes shape, or set scores, winners, ranking points or season statistics disappear, the run fails with a message instead of returning rows with empty values.

**Something looks wrong.** Open an issue with the input you used; changes at the source are fixed quickly.

### Related Actors

Other public-data Actors from the same publisher are listed on the Store profile.

# Changelog

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

# Actor input Schema

## `dataTypes` (type: `array`):

What to return: matches (results and schedule with set and tiebreak scores), rankings (current ATP and WTA singles rankings, top 150 per tour) and players (player profiles with season record and prize money; needs Players). One run can combine several types. Default: matches.

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

First match date to return, YYYY-MM-DD (UTC). Leave empty to use Last days. Match data is available from about 2010; for older seasons ESPN lists tournaments without matches. Future dates return the order of play that is already published.

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

Last match date to return, YYYY-MM-DD (UTC). Leave empty for today (or for Date from, when Date from is in the future).

## `lastDays` (type: `integer`):

When Date from is empty the run covers this many days ending on Date to (today by default). 3 means today and the two days before.

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

Optional. ATP (men), WTA (women) and/or Mixed (mixed doubles at the Grand Slams). Leave empty for all. The tour of a match follows its draw, so the women's matches of a combined event such as the China Open are WTA. Rankings exist for ATP and WTA; player profiles are filtered by the tour of the player.

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

Optional. Player names or ESPN player IDs. Matches: keeps matches in which any listed player plays (the name only has to contain the text, accents and case are ignored, so Zverev matches both Alexander and Mischa Zverev). Rankings: keeps those players. Players data type: returns the profile of every player whose name contains the text among the current ATP/WTA top 150 and the matches read in the run, or of the given ESPN ID (for example 3623).

## `headToHeadOnly` (type: `boolean`):

Matches only: keep just the matches in which two different listed players face each other (for example Players = Sinner, Alcaraz returns their meetings). Needs at least two Players.

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

Optional. Keep only matches of tournaments whose name contains one of these texts (for example Wimbledon, Open) or whose ESPN tournament ID equals the value (for example 188 for Wimbledon).

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

Optional. singles and/or doubles. Leave empty for both.

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

Optional. Keep only matches with these statuses: scheduled, in\_progress, finished, retired, walkover, postponed, cancelled, suspended. Leave empty for all. Use finished, retired and walkover for completed results only.

## `includeQualifying` (type: `boolean`):

Include qualifying-round matches. Turn off for main-draw matches only.

## `maxRank` (type: `integer`):

Rankings only: keep players ranked this high or better, for example 10 for the top 10. 0 keeps the whole list (ESPN publishes the top 150 per tour).

## `onlyNewSinceLastRun` (type: `boolean`):

Return only rows that earlier runs with the same filters have not delivered to you. The Actor remembers the rows it actually returned, per filter combination (dates excluded, so a scheduled run with Last days keeps one feed), in a storage on your account. A match delivered as scheduled or in progress is delivered again once it is finished; a ranking row again when a new ranking is released. Rows dropped by your filters or cut off by Maximum results are not remembered and can still come later.

## `oldestFirst` (type: `boolean`):

Return matches in chronological order (oldest first) instead of the default newest first. Useful for building a history archive in date order.

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

Stop after this many rows in total. Matches come newest first (see Oldest first), so the limit never cuts off today's results; raise it for long date ranges (a busy month of both tours is 1,300 to 3,000 matches).

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

Optional proxy. Leave disabled: the ESPN JSON API is public and answers requests from data-center addresses.

## Actor input object example

```json
{
  "dataTypes": [
    "matches"
  ],
  "lastDays": 3,
  "tours": [],
  "players": [],
  "headToHeadOnly": false,
  "tournaments": [],
  "matchTypes": [],
  "matchStatuses": [],
  "includeQualifying": true,
  "maxRank": 0,
  "onlyNewSinceLastRun": false,
  "oldestFirst": false,
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

All extracted records as a dataset.

# 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 = {
    "dataTypes": [
        "matches"
    ],
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("datagrit/tennis-stats-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 = {
    "dataTypes": ["matches"],
    "maxItems": 50,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("datagrit/tennis-stats-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 '{
  "dataTypes": [
    "matches"
  ],
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call datagrit/tennis-stats-scraper --silent --output-dataset

```

## MCP server setup

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