# Tennis Scraper: Results, Odds, Rankings, Players (`zerolag/tennis-scraper`) Actor

Tennis results, fixtures, tournaments, player history, head-to-head and rankings from public TennisExplorer pages. ATP, WTA, Challenger and ITF matches with set scores and average pre-match odds. No proxy, no login.

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

## Pricing

from $2.80 / 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 for ATP, WTA, Challenger and ITF matches. It reads public TennisExplorer pages and gives you results by date, upcoming matches, whole tournaments, player history, head-to-head records, rankings and the tournament calendar as clean rows.

- **Price:** $4.00 per 1,000 results on the Free plan, $2.80 on Business. Platform usage is included.
- **Input:** days, a tournament, a player, two players or a ranking date.
- **Output:** players, winner, set scores, average bookmaker odds, round, surface and start time.
- **Measured:** 1,143 French Open matches from nine seasons in 10 seconds on Apify.

Found a bug, need a field, or want a new Actor? Open an issue on the **Issues** tab. If it works well for you, a short review helps others find it.

### How to use it

1. Pick a job in **What to get**.
2. Fill in the section for that job. Everything else can stay empty.
3. Set **Maximum results** and click **Start**.
4. Export the results as JSON, CSV or Excel.

| What to get | You enter | You get |
| --- | --- | --- |
| Match results by date | Days, or nothing for the latest day with matches | Finished matches with score, odds, round and surface |
| Upcoming matches by date | Days, or nothing for today and tomorrow | Scheduled matches with odds and earlier wins against each other |
| All matches of a tournament | A name or link, and years | The draw as matches, with qualifying if you ask |
| Match history of a player | A name or link | Career matches with surface, round, score and odds |
| Head-to-head of two players | Two names, such as `Carlos Alcaraz vs Jannik Sinner` | Every match between them |
| Rankings | ATP or WTA, a type and a date | Rank, points, move and country |
| Tournament calendar | Years | Start date, surface, prize money, draw size and singles winner |

Use **Tours**, **Formats** and **Surfaces** to narrow the rows. Singles is the default format. In the tournament mode **Tours** picks the men's or women's draw.

Example input:

```json
{
    "mode": "tournament",
    "tournaments": ["Wimbledon"],
    "yearFrom": 2023,
    "yearTo": 2024,
    "includeQualifying": true,
    "maxItems": 1000
}
```

On 5 November 2026 this input saved 956 Wimbledon matches in 10 seconds.

### What data you get

Match rows have the same fields in every mode. A value the source does not show is `null`.

| Field | What it holds |
| --- | --- |
| `matchId`, `matchUrl` | Match ID and link |
| `status`, `resultType` | `finished`, `in progress` or `scheduled`, then `completed`, `retired` or `walkover` |
| `date`, `startTime`, `startTimeUtc` | Date and time in Central European time, and the start in UTC |
| `tour`, `gender`, `format` | ATP, WTA, Challenger, ITF or UTR, then men, women or mixed, then singles, doubles or mixed |
| `tournament`, `tournamentUrl`, `tournamentCountry`, `tournamentPrizeMoney` | Tournament details |
| `surface`, `round` | `hard`, `clay`, `grass` or `indoors`, then a code such as `R16`, `QF` or `Q-1R` |
| `player1`, `player2` and their `Id`, `Url`, `Seed` fields | Players or doubles teams |
| `winner`, `winnerSide`, `score`, `sets` | Result, with each set and tie-break |
| `player1SetsWon`, `player2SetsWon` | Finished sets won |
| `oddsPlayer1`, `oddsPlayer2` | Average decimal odds across bookmakers |
| `h2hPlayer1`, `h2hPlayer2` | Earlier wins against each other, for upcoming matches |
| `queriedPlayerWon` | Whether the player you asked for won |
| `query`, `sourceUrl`, `retrievedAt` | Where the row came from |

Rankings give `rankingDate`, `rank`, `rankMove`, `player`, `country` and `points`. The calendar gives `startDate`, `level`, `prizeMoney`, `singlesDraw`, `doublesDraw` and `singlesWinner`.

One real row:

```json
{
    "recordType": "match",
    "matchId": "3362391",
    "matchUrl": "https://www.tennisexplorer.com/match-detail/?id=3362391",
    "status": "finished",
    "resultType": "completed",
    "date": "2026-10-29",
    "startTime": "09:05",
    "startTimeUtc": "2026-10-29T08:05:00.000Z",
    "tour": "WTA",
    "gender": "women",
    "format": "singles",
    "tournament": "Tokyo",
    "tournamentUrl": "https://www.tennisexplorer.com/tokyo/2026/wta-women/",
    "tournamentCountry": "Japan",
    "tournamentPrizeMoney": "1,206,446 $",
    "surface": "hard",
    "round": "R16",
    "player1": "Rybakina E.",
    "player1Id": "rybakina",
    "player1Url": "https://www.tennisexplorer.com/player/rybakina/",
    "player1Seed": "1",
    "player2": "Putintseva Y.",
    "player2Id": "putintseva",
    "player2Url": "https://www.tennisexplorer.com/player/putintseva/",
    "player2Seed": null,
    "winner": "Rybakina E.",
    "winnerSide": 1,
    "score": "6-2, 6-3",
    "player1SetsWon": 2,
    "player2SetsWon": 0,
    "sets": [
        {
            "player1": 6,
            "player2": 2,
            "tiebreak": null
        },
        {
            "player1": 6,
            "player2": 3,
            "tiebreak": null
        }
    ],
    "oddsPlayer1": 1.13,
    "oddsPlayer2": 5.75,
    "h2hPlayer1": null,
    "h2hPlayer2": null,
    "queriedPlayerWon": null,
    "query": "2026-10-29",
    "sourceUrl": "https://www.tennisexplorer.com/results/?type=all&year=2026&month=10&day=29",
    "retrievedAt": "2026-11-05T22:50:29.953Z"
}
```

### How much does it cost?

You pay for each saved row. Rows that only explain an empty result are free. Each run also has a start charge of $0.00005.

| Plan | Price per 1,000 results |
| --- | --- |
| Free | $4.00 |
| Starter | $3.60 |
| Scale | $3.20 |
| Business | $2.80 |

Example: a daily run that saves 50 matches is 1,500 results a month. That costs $6.00 on the Free plan and $4.20 on Business.

### How to get yesterday's results every morning

Set **First day** and **Last day** to `1 day`, which counts back from today, then add an Apify Schedule.

```json
{
    "mode": "results",
    "dateFrom": "1 day",
    "dateTo": "1 day",
    "tours": ["atp", "wta"],
    "maxItems": 300
}
```

### How to check a player on one surface

```json
{
    "mode": "player",
    "players": ["Iga Swiatek"],
    "surfaces": ["clay"],
    "yearFrom": 2022,
    "maxItems": 300
}
```

Count the rows where `queriedPlayerWon` is true for the win record. `oddsPlayer1` and `oddsPlayer2` show how the market saw each match.

### Limits

- **No live scores.** The source leaves most matches blank until they end, so a match in play usually reads as `scheduled`.
- **`retired` and `walkover` are worked out from the score.** The source lists no label. A partial score on a match that started in the last 6 hours reads as `in progress`.
- **Round and surface need extra pages.** Day pages do not show them, so the run reads each tournament page once. Turn off **Add round, surface and prize money** for a faster run.
- **Men's ITF matches have no round or surface.** The source lists them under one heading, such as `Futures 2026`, with no tournament page. In date modes UTR events have no round or surface either.
- **Names differ between pages:** `Djokovic` on tournament pages, `Djokovic N.` elsewhere. Join rows on `player1Id` and `player2Id`.
- **Player and head-to-head rows have no start time.** The source shows only the date there.
- **Odds are averages,** not the odds of one bookmaker. In a sample of French Open draws, odds appear by 2008.
- **Pace:** about one page per second, on purpose. A long date range takes minutes.
- **Player bios and injuries are left out.**

### API and integrations

Start runs and read results through the [Apify API](https://docs.apify.com/api/v2) or the JavaScript and Python clients. Use Schedules for repeat runs and webhooks to send the data on when a run ends.

### FAQ

**Do I need a proxy or a TennisExplorer account?**
No. The Actor reads public pages with a plain request, and the test runs on Apify used no proxy.

**Why is there a row with `recordType` set to `notice`?**
It explains why part of your input gave nothing, for example a player name that was not found. These rows are free.

**How far back does the data go?**
Calendars go back to 1995 and ranking lists to 1998. In a sample of French Open draws, the 1995 draw has every match with round and score. Start times appear by 2005 and odds by 2008.

**Which time zone are the dates in?**
`date` and `startTime` are Central European time, as the source shows them. `startTimeUtc` is the same moment in UTC.

**Why did a run stop before the end of my date range?**
It reached **Maximum results** or your spending limit. The status message says which.

**Can I use this data in a commercial product?**
Check first. The terms of use of TennisExplorer limit commercial use, scraping and copying large parts of its database. You are responsible for how you use the data.

*This Actor is an independent tool with no connection to TennisExplorer or Livesport. All trademarks belong to their owners.*

# Actor input Schema

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

Pick one job. Each mode reads only the fields in its own section below.

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

For match results and upcoming matches. Pick a date or a length: <code>7 days</code> counts back from today for results and forward for upcoming matches. With no date, results come from the latest day with matches, and upcoming matches start today.

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

With no date, results run up to today and upcoming matches cover two days.

## `includeRoundAndSurface` (type: `boolean`):

Day pages do not show these, so the run reads each tournament page once. Turn it off for a faster run with fewer fields.

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

For the tournament mode: names such as <code>Wimbledon</code>, or a link such as <code>https://www.tennisexplorer.com/wimbledon/2024/atp-men/</code>. For results, upcoming matches and player history: keep only tournaments whose name contains one of these words.

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

For the tournament mode with a tournament name. Adds the qualifying draw.

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

For the player mode: names such as <code>Jannik Sinner</code>, or a link such as <code>https://www.tennisexplorer.com/player/sinner-8b8e8/</code>.

## `matchups` (type: `array`):

For the head-to-head mode. One pair per line: <code>Carlos Alcaraz vs Jannik Sinner</code>.

## `yearFrom` (type: `integer`):

For tournaments, players, head-to-head and the calendar. Tournaments and the calendar default to this year. Players and head-to-head default to the whole career.

## `yearTo` (type: `integer`):

Same as the first year when you want one season.

## `rankingType` (type: `string`):

For the rankings mode.

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

For the rankings mode. Leave empty for the latest list. Lists go back to 1998.

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

Keep only these tours. Leave empty for all. In the rankings mode use ATP or WTA to choose the list.

## `formats` (type: `array`):

Which matches to keep. Leave empty for all formats. Mixed doubles appear in results, upcoming matches and player history.

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

Keep only these surfaces. Leave empty for all.

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

The run stops after saving this many results. You pay only for saved results.

## Actor input object example

```json
{
  "mode": "results",
  "includeRoundAndSurface": true,
  "tournaments": [],
  "includeQualifying": false,
  "players": [],
  "matchups": [],
  "rankingType": "singles",
  "tours": [],
  "formats": [
    "singles"
  ],
  "surfaces": [],
  "maxItems": 30
}
```

# Actor output Schema

## `results` (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",
    "maxItems": 30
};

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

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

```

## MCP server setup

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