# Tennis Scores, Stats & Rankings Scraper (`frank_conifer_af4/tennis-scores-stats-scraper`) Actor

Tennis results, fixtures and live scores for ATP, WTA, Challenger and ITF with set-by-set scores, optional match stats (aces, double faults, serve %, break points), surface and seeds, plus ATP/WTA singles and doubles rankings in one clean schema.

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

## Pricing

from $2.50 / 1,000 tennis matches

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?

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 Scores, Stats & Rankings Scraper

Tennis results, fixtures, live scores, match stats and ATP/WTA rankings in one clean, ready-to-model dataset, from $2.50 per 1,000 matches.

### What does Tennis Scores, Stats & Rankings Scraper do?

It collects tennis match data for the ATP Tour, the WTA Tour (including WTA 125 events), the Grand Slams, the ATP Challenger Tour and the ITF World Tennis Tour. Pick results, fixtures, live scores or rankings (or several at once), choose the tours and dates, and every match comes back in the same schema: tournament, category, surface, round, start time, players with country, seed and ranking, set-by-set score with tiebreak points, the winner, and the source links. Switch on match stats to add aces, double faults, first-serve percentage, break points saved and converted, winners, unforced errors and match and set durations. Rankings mode returns the ATP and WTA singles and doubles ranking lists with points, movement and tournaments played.

### Why use it

- **One schema for every tour.** ATP, WTA, Challenger and ITF matches have the same fields, so a Grand Slam final and an M15 first round go into the same table, model or database without per-source code.
- **Cheap per match, nothing charged for noise.** $2.50 per 1,000 matches and $0.50 per 1,000 ranking rows. Filtered-out matches, status messages and the optional match stats cost nothing extra.
- **Built for models.** Set scores are numbers (`{ "p1": 7, "p2": 6, "tiebreak": { "p1": 7, "p2": 4 } }`), stats come as `{ pct, won, total }`, statuses are normalized (`finished`, `retired`, `walkover`, `cancelled`, `live`, `scheduled`), times are UTC ISO.
- **Robust sources with fallbacks.** ATP/WTA come from ESPN's public scoreboard JSON and switch to the Flashscore data feed for any day ESPN fails. Rankings fall back between the official WTA API, Flashscore and ESPN. A failed source produces a free status row that says what is missing instead of a silent gap.
- **Monitoring that only charges for news.** Only new results remembers delivered results between runs, so a schedule every 30 minutes returns (and charges) only matches that finished since the last run.
- **Always returns something useful.** If the dates you ask for have no completed matches (off-season, rest days), the Actor returns the most recent completed match day and tells you so.

### Source coverage

| Data | ATP | WTA | Challenger | ITF | Where it comes from |
|---|---|---|---|---|---|
| Results | Any date, years back | Any date, including WTA 125 | Last 7 days | Last 7 days | ESPN public scoreboard (ATP/WTA), Flashscore public data feed (Challenger/ITF, and ATP/WTA when ESPN fails) |
| Fixtures | As soon as the order of play is out | Same | Next 7 days | Next 7 days | Same as results |
| Live scores (with the current game score) | Yes | Yes | Yes | Yes | ESPN + Flashscore |
| Round, seed, entry (Q/WC/LL), court | Yes | Yes | Qualifying flag only | Qualifying flag only | ESPN |
| Surface and indoor flag | Matches in the last/next 7 days | Same | Yes | Yes | Flashscore |
| Match stats and durations | Last 7 days | Last 7 days | Last 7 days, where published | Where published | Flashscore |
| Player's current singles ranking | Top 150 | Top 150 | No | No | ESPN |
| Singles rankings | Any depth, dated, with tournaments played | Any depth, dated, with tournaments played | No | No | Flashscore (ATP, names checked against ESPN), official WTA API (WTA); ESPN top 150 as a fallback |
| Doubles rankings | Any depth | Any depth | No | No | Flashscore (ATP), official WTA API (WTA) |
| Betting odds | Not included | Not included | Not included | Not included | Not freely available in these sources |

### What data you get

Three row types: `match` (charged), `ranking` (charged) and `status` (free).

**Match rows** (`type: "match"`)

| Field | Description |
|---|---|
| `matchId`, `source` | ID at the source (`espn` or `flashscore`). `sourceIds` holds both IDs when the match was found in both |
| `tour`, `gender`, `matchType` | `atp`, `wta`, `challenger`, `itf` or `mixed` (Grand Slam mixed doubles); `men`/`women`/`mixed`; `singles`/`doubles` |
| `tournament` | `{ id, name, category, surface, indoor, location, startDate, endDate }`. Category is `Grand Slam`, `ATP Tour`, `WTA Tour`, `WTA 125`, `ATP Challenger`, `ITF Men` or `ITF Women` |
| `round`, `court` | For example `Quarterfinal`, `Qualifying 1st Round`, `Stadium Court` |
| `startAt`, `startTimeConfirmed` | UTC start time; `false` when the time is still "TBD" |
| `status`, `statusDetail` | `scheduled`, `live`, `finished`, `retired`, `walkover` or `cancelled`, plus the source's wording (for example `2nd Set`) |
| `player1`, `player2` | `{ id, name, country, countryName, seed, entry, ranking }`. Doubles teams also have `members[]` |
| `winner`, `winnerSide` | Winner's name and `1` or `2` |
| `score` | `{ sets: [{ p1, p2, tiebreak?, durationMinutes? }], setsWon: { p1, p2 }, summary, currentGame? }` from player1's point of view, e.g. `6-7(7-9) 7-6(7-2) 6-3` |
| `resultNote` | The source's one-line result, e.g. `(1) Alexander Zverev (GER) bt (9) Alex de Minaur (AUS) 6-4 6-4` |
| `durationMinutes`, `stats` | With **Include match stats**: match duration and `{ player1: {...}, player2: {...} }` with `aces`, `doubleFaults`, `firstServePercentage`, `firstServePointsWon`, `secondServePointsWon`, `breakPointsSaved`, `breakPointsConverted`, `firstReturnPointsWon`, `secondReturnPointsWon`, `winners`, `unforcedErrors`, `netPointsWon`, `servicePointsWon`, `returnPointsWon`, `totalPointsWon`, `serviceGamesWon`, `returnGamesWon`, `totalGamesWon` and more where published |
| `url`, `flashscoreUrl`, `scrapedAt` | Source page links and the scrape time |

**Ranking rows** (`type: "ranking"`)

| Field | Description |
|---|---|
| `tour`, `rankingsType` | `atp` or `wta`; `singles` or `doubles` |
| `rank`, `previousRank`, `movement` | Movement is positive when the player moved up |
| `player`, `playerId`, `country`, `countryName` | Player name and the source's player ID |
| `points`, `tournamentsPlayed` | Ranking points and the number of counted tournaments |
| `rankingDate`, `source` | The ranking week (YYYY-MM-DD) and the source list (`flashscore`, `wtatennis` or `espn`) |

**Status rows** (`type: "status"`, never charged) carry `level` (`info` or `error`) and a `message`, for example "No live matches right now", an off-season fallback notice or a source error.

### How to use

1. Open the Actor and pick **What to scrape**: results, fixtures, live, rankings or a combination.
2. Pick the **Tours**. ATP and WTA include the Grand Slams.
3. Optionally set **From date** / **To date** (YYYY-MM-DD, `yesterday`, `today`, `-3`, `+2`). Empty dates mean yesterday for results and today plus two days for fixtures.
4. Optionally narrow it down by **Tournament name** or **Player names**, and switch on **Include doubles** or **Include match stats**.
5. Click **Start**. Matches appear in the **Matches** view and rankings in the **Rankings** view; export them as JSON, CSV or Excel, or read them through the API.

Example input: last three days of ATP and Challenger results with stats for two players, plus the ATP top 100.

```json
{
  "mode": ["results", "rankings"],
  "tours": ["atp", "challenger"],
  "dateFrom": "-3",
  "dateTo": "yesterday",
  "players": ["Sinner", "Alexander Zverev"],
  "includeMatchStats": true,
  "rankingsType": "singles",
  "maxRankingRows": 100
}
```

Monitoring input for a schedule every 30 minutes (returns only results that are new since the previous run):

```json
{
  "mode": ["results"],
  "tours": ["atp", "wta"],
  "dateFrom": "-1",
  "dateTo": "today",
  "onlyNewResults": true,
  "stateStoreName": "tennis-results-monitor"
}
```

### Output example

A completed ATP match with stats (stats trimmed):

```json
{
  "type": "match",
  "matchId": "184954",
  "source": "espn",
  "tour": "atp",
  "gender": "men",
  "matchType": "singles",
  "tournament": {
    "id": "315-2026",
    "name": "Rolex Shanghai Masters",
    "category": "ATP Tour",
    "surface": "hard",
    "indoor": false,
    "location": "Shanghai, China PR",
    "startDate": "2026-10-05",
    "endDate": "2026-10-18"
  },
  "round": "Quarterfinal",
  "court": "Stadium Court",
  "startAt": "2026-10-15T10:10:00.000Z",
  "startTimeConfirmed": true,
  "status": "finished",
  "statusDetail": "Final",
  "player1": { "id": "2375", "name": "Alexander Zverev", "country": "GER", "countryName": "Germany", "seed": 1, "entry": null, "ranking": 2 },
  "player2": { "id": "2651", "name": "Alex de Minaur", "country": "AUS", "countryName": "Australia", "seed": 9, "entry": null, "ranking": 9 },
  "winner": "Alexander Zverev",
  "winnerSide": 1,
  "score": {
    "sets": [
      { "p1": 6, "p2": 4, "durationMinutes": 37 },
      { "p1": 6, "p2": 4, "durationMinutes": 41 }
    ],
    "setsWon": { "p1": 2, "p2": 0 },
    "summary": "6-4 6-4"
  },
  "resultNote": "(1) Alexander Zverev (GER) bt (9) Alex de Minaur (AUS) 6-4 6-4",
  "durationMinutes": 78,
  "stats": {
    "player1": {
      "aces": 10,
      "doubleFaults": 1,
      "firstServePercentage": 80,
      "firstServePointsWon": { "pct": 80, "won": 35, "total": 44 },
      "breakPointsSaved": { "pct": 100, "won": 5, "total": 5 },
      "breakPointsConverted": { "pct": 100, "won": 2, "total": 2 },
      "winners": 24,
      "unforcedErrors": 15
    },
    "player2": {
      "aces": 6,
      "doubleFaults": 2,
      "firstServePercentage": 63,
      "firstServePointsWon": { "pct": 81, "won": 25, "total": 31 }
    }
  },
  "url": "https://www.espn.com/tennis/scoreboard/tournament/_/eventId/315-2026/competitionType/1",
  "sourceIds": { "espn": "184954", "flashscore": "ERN0r0Qp" },
  "scrapedAt": "2026-10-16T12:56:49.062Z",
  "flashscoreUrl": "https://www.flashscore.com/match/tennis/zverev-alexander-dGbUhw9m/de-minaur-alex-EZgZ9Xfh/?mid=ERN0r0Qp"
}
```

A Challenger fixture (names are abbreviated as published for Challenger and ITF):

```json
{
  "type": "match",
  "matchId": "GI66xd7s",
  "source": "flashscore",
  "tour": "challenger",
  "tournament": { "id": "pKRztbdD", "name": "Olbia", "category": "ATP Challenger", "surface": "hard", "indoor": false, "location": "Olbia, Italy" },
  "startAt": "2026-10-16T13:00:00.000Z",
  "status": "scheduled",
  "player1": { "id": "pGBaKGOn", "name": "Fearnley J.", "countryName": "United Kingdom" },
  "player2": { "id": "hMR2isnm", "name": "Mrva M.", "countryName": "Czech Republic" },
  "url": "https://www.flashscore.com/match/tennis/fearnley-jacob-pGBaKGOn/mrva-maxim-hMR2isnm/?mid=GI66xd7s"
}
```

A ranking row:

```json
{
  "type": "ranking",
  "tour": "wta",
  "rankingsType": "singles",
  "rank": 3,
  "previousRank": 5,
  "movement": 2,
  "player": "Mirra Andreeva",
  "playerId": "331809",
  "country": "RUS",
  "points": 6371,
  "tournamentsPlayed": 20,
  "rankingDate": "2026-10-12",
  "source": "wtatennis"
}
```

### Pricing

Pay per event, no subscription:

| Event | Price |
|---|---|
| `match`: one match row (result, fixture or live) | $0.0025 ($2.50 per 1,000) |
| `ranking-row`: one ranking place | $0.0005 ($0.50 per 1,000) |

Worked examples:

- **Daily ATP + WTA singles results** (about 20 to 60 matches a day in season): about $0.05 to $0.15 a day, $1.50 to $4.50 a month.
- **A full week of everything** (ATP, WTA, Challenger and ITF, singles and doubles, about 1,900 matches in our test week): about $4.85.
- **Top 100 ATP and WTA singles rankings once a week**: 200 rows = $0.10 a week.
- **The default example run** (yesterday's ATP/WTA results plus the top 20 of both rankings, 21 matches + 40 ranking rows in our test): about $0.07.

Not charged: status rows, matches removed by your tour, date, player or tournament filters, results skipped by **Only new results**, and match stats or durations (they are part of the match row). **Max match rows** and the run's maximum charge setting cap your spend; the Actor stops cleanly when the limit is reached.

### Integrations and scheduling

- **Schedules:** run it every few minutes for live scores, every 30 to 60 minutes with **Only new results** for result feeds, or weekly on Monday for rankings.
- **Webhooks:** trigger your pipeline, a Slack message or a model retrain when a run succeeds.
- **API and SDKs:** start runs and read the dataset from any language; use `?view=overview` for the flat match table and `?view=rankings` for rankings.
- **Integrations:** send rows to Google Sheets, Airtable, BigQuery, S3, Zapier or Make with Apify integrations.
- **AI agents and MCP:** the Actor works as a tool through the Apify MCP server, so an assistant can answer "how did Swiatek do this week?" with fresh data.

### FAQ and limitations

**How fresh are live scores?** Each run is a snapshot taken at run time, including the current game score. Schedule it every 1 to 5 minutes for a live feed. Live mode can return zero matches when nothing is being played; you then get a free status row.

**Why do Challenger and ITF matches have abbreviated names and no round or seed?** Those tours come from Flashscore's data feed, which publishes names as "Surname I." and does not include rounds or seeds in its daily list. ATP and WTA matches use full names, rounds and seeds from ESPN.

**How far back can I go?** ATP and WTA results go back years (31 days per run). Challenger and ITF matches, match stats and the surface are available for the last 7 and next 7 days only, so schedule the Actor if you want to build a history.

**Which ranking list is used?** ATP rankings come from Flashscore's dated list (names and country codes checked against ESPN), WTA rankings from the official WTA rankings API. `rankingDate` is the ranking week. The per-match `player.ranking` is ESPN's current list (top 150), which can differ by a few places from the dated official list while a tournament is running.

**Are odds or head-to-head records included?** Odds are not included because they are not freely available in these sources. For head-to-head records, filter results by both players' names over the dates you need.

**Are times local?** No. All dates and times are UTC, and the date filters use UTC days.

**What happens when a source is down?** ESPN days that fail are filled from Flashscore; rankings fall back to another list; anything still missing is explained in a free status row. If a source starts blocking requests, the Actor stops asking it for the rest of the run instead of hammering it.

**Responsible use.** The Actor collects factual sports data only: names, dates, scores, statistics and ranking numbers from publicly available, unauthenticated feeds. It does not collect logos, photos, flags, editorial articles or any data behind a login, and it keeps request rates low. It is not affiliated with or endorsed by the ATP, WTA, ITF, ESPN or Flashscore. You are responsible for using the data in line with the sources' terms and the law that applies to you, for example when you republish data commercially. Use the data for analysis and entertainment; it is not betting advice.

# Actor input Schema

## `mode` (type: `array`):

One or more of: results (completed matches with scores), fixtures (scheduled matches with both players known), live (matches in progress right now) and rankings (ATP/WTA ranking lists). Each match row is one charged event; each ranking row is one cheaper event.

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

atp = ATP Tour and Grand Slams (men). wta = WTA Tour, WTA 125 and Grand Slams (women). challenger = ATP Challenger Tour. itf = ITF World Tennis Tour (M15-M25, W15-W100). Challenger and ITF matches are available for the last 7 and next 7 days. Rankings are available for atp and wta.

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

First day (UTC) for results and fixtures. Accepts YYYY-MM-DD, today, yesterday, tomorrow or an offset in days such as -3 or +2. Empty: results default to yesterday, fixtures to today. ATP/WTA results go back years; the range can be up to 31 days per run.

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

Last day (UTC), same formats. Empty: results end yesterday (or today when From date is set), fixtures end two days after the start. Longer ranges mean more requests and more match rows.

## `fallbackToLatestResults` (type: `boolean`):

If no completed matches exist in the date range (off-season, rest days), return the most recent completed match day instead of nothing. It only kicks in when the range itself is empty, not when your player or tournament filters remove everything.

## `includeDoubles` (type: `boolean`):

Also return doubles and Grand Slam mixed doubles matches. Off: singles only. Doubles roughly add 40% more match rows (and cost).

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

Keep only matches whose tournament name or location contains one of these texts (case-insensitive), e.g. Wimbledon, Shanghai, Monastir. Empty: all tournaments.

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

Keep only matches with one of these players (and only these players in rankings). Full or partial names work, e.g. Sinner, Iga Swiatek, Alcaraz. Accents and case are ignored, and Jannik Sinner also matches the short form Sinner J.

## `includeMatchStats` (type: `boolean`):

Adds per-player statistics (aces, double faults, first-serve %, points won on first and second serve, break points saved and converted, winners, unforced errors, net points, service and return games) plus match and set durations for completed and live matches of the last 7 days. Makes 2 extra requests per match, so runs are slower. Not charged extra.

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

Upper limit of match rows per run (after filters). Each match row is one charged event, so this caps your cost. Matches are returned in start-time order.

## `rankingsType` (type: `string`):

Singles, doubles or both. Used only when rankings is selected.

## `maxRankingRows` (type: `integer`):

How many places to return per ranking list (top N), e.g. 100 for the top 100. Each row is one ranking-row event. With a player filter, the top N is searched and only matching players are returned.

## `onlyNewResults` (type: `boolean`):

Remember delivered completed matches in a named key-value store and skip them on the next runs, so a scheduled run returns (and charges) only results that are new since the last run. Fixtures, live matches and rankings are always returned. Tip: use From date -1 so each run covers yesterday and today.

## `stateStoreName` (type: `string`):

Name of the key-value store that keeps already delivered result IDs for Only new results. Use a different name per schedule if you run several independent monitors.

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

Optional. The sources are public JSON feeds that normally work without a proxy; Apify Proxy (datacenter) helps if a feed starts rate-limiting.

## `maxConcurrency` (type: `integer`):

Parallel requests (mostly for match stats). Each source is additionally rate-limited per host, so higher values only help with stats on large date ranges.

## Actor input object example

```json
{
  "mode": [
    "results",
    "rankings"
  ],
  "tours": [
    "atp",
    "wta"
  ],
  "fallbackToLatestResults": true,
  "includeDoubles": false,
  "includeMatchStats": false,
  "maxMatches": 1000,
  "rankingsType": "singles",
  "maxRankingRows": 20,
  "onlyNewResults": false,
  "stateStoreName": "tennis-scores-state",
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 4
}
```

# Actor output Schema

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

No description

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

No description

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

No description

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

No description

# 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 = {
    "mode": [
        "results",
        "rankings"
    ],
    "tours": [
        "atp",
        "wta"
    ],
    "maxRankingRows": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("frank_conifer_af4/tennis-scores-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 = {
    "mode": [
        "results",
        "rankings",
    ],
    "tours": [
        "atp",
        "wta",
    ],
    "maxRankingRows": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("frank_conifer_af4/tennis-scores-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 '{
  "mode": [
    "results",
    "rankings"
  ],
  "tours": [
    "atp",
    "wta"
  ],
  "maxRankingRows": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call frank_conifer_af4/tennis-scores-stats-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,frank_conifer_af4/tennis-scores-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/9eJ5ESoJVT8IqvycT/builds/23yG17ZAy7CUwdeAg/openapi.json
