# Tennis Scraper — Matches, Odds History, Players, Rankings, H2H (`teodor_banea/tennisexplorer-scraper`) Actor

Scrape TennisExplorer: ATP/WTA/ITF/Challenger results and fixtures by date range with average and per-bookmaker odds (opening odds, movement history, O/U, handicap, correct score), player profiles with match history, rankings and head-to-head. No login, no browser.

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

## Pricing

from $3.00 / 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.

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, Odds History, Players, Rankings, H2H

### What does Tennis Scraper do?

Tennis Scraper extracts tennis data from [TennisExplorer](https://www.tennisexplorer.com): **match results and fixtures for any date range** (ATP, WTA, Challenger, ITF, UTR — singles and doubles), **bookmaker odds with opening odds and timestamped movement history** from up to 15 bookmakers (match winner, Over/Under, Asian handicap, correct score), **player profiles with their match history**, **ATP/WTA rankings** (current or year-end) and **head-to-head records**. It reads the site's server-rendered pages — no login, no browser, no proxy needed.

### Why use this Actor?

- **Odds depth nobody else sells**: per-bookmaker quotes with opening odds and every price change (time + odds), plus the site's average odds on every match row.
- **Date ranges, not single days**: results for past days, live/finished matches for today, fixtures for future days — up to 92 days per run.
- **Clean, typed rows**: score text *and* set-by-set integer columns, ISO UTC start times, round and surface from the tournament page, `status` (finished / retired / walkover / live / scheduled), seeds, prize money.
- **Five data sets in one Actor**: matches, odds, players, rankings, head-to-head — one dataset with a `type` column and a table view per type.
- **Honest output**: a field the site does not show is `null`, never guessed (round is only filled when the tournament page lists that exact match).

### How much does it cost?

You pay **$3.00 per 1,000 rows** (every row of any type is one result). Platform compute is tiny — pages are plain HTML.

| Run | Rows | Cost |
|---|---|---|
| One day of ATP singles results (incl. Challenger/ITF) | ~70–230 match rows | $0.21–0.69 |
| One week of ATP + WTA singles | ~1,500–2,500 | $4.50–7.50 |
| One match with full bookmaker odds (`includeOdds`) | 1 match + ~250 odds rows | ~$0.75 |
| Sinner, 2026 season (profile + matches) | 53 | $0.16 |
| ATP top 100 | 100 | $0.30 |
| One head-to-head (Zverev v De Minaur) | 14 | $0.04 |

Odds multiply rows: a top-level match has 200–300 bookmaker lines. Use **Maximum results** and the **tournament filter** to keep odds runs to what you need.

### Input

| Field | Mode | Description | Example |
|---|---|---|---|
| `mode` | all | `matches`, `players`, `rankings` or `headToHead` | `matches` |
| `dateFrom` / `dateTo` | matches | Day range (YYYY-MM-DD, London calendar day); max 92 days unless `maxResults` is set | `2026-10-10` |
| `tours` | matches | `atp-single`, `wta-single`, `atp-double`, `wta-double`, `all` | `["atp-single"]` |
| `includeOdds` | matches | Fetch each match's detail page for per-bookmaker odds | `true` |
| `tournamentFilter` | matches | Keep tournaments whose name contains this text | `Shanghai` |
| `players` | players | Slug, profile URL or name | `["sinner-8b8e8", "Iga Swiatek"]` |
| `years` | players | Seasons for the match list, or `all` | `["2026"]` |
| `includePlayerMatches` | players | Add the player's matches | `true` |
| `rankingTour` / `rankingYear` / `maxRank` | rankings | ATP or WTA, optional past season (year-end), depth (0 = all) | `atp-men`, `2025`, `100` |
| `pairs` | headToHead | `"slugA vs slugB"` or a `/mutual/a/b/` URL | `["zverev-6f768 vs de-minaur"]` |
| `maxResults` | all | Stop after this many rows (0 = no limit). The form prefills 100 so a first run costs about $0.30. | `100` |
| `maxConcurrency` / `maxRequestsPerMinute` | all | Politeness (defaults 3 and 60) | |

```json
{
    "mode": "matches",
    "dateFrom": "2026-10-10",
    "dateTo": "2026-10-10",
    "tours": ["atp-single"],
    "includeOdds": true,
    "tournamentFilter": "shanghai",
    "maxResults": 100
}
```

### Output

One dataset; the `type` column tells the row kind, and each type always carries the same fields. Views in the Console: **Matches**, **Odds**, **Players**, **Rankings**, **Head-to-head**.

#### `match`

```json
{
    "type": "match",
    "matchId": "3344135",
    "url": "https://www.tennisexplorer.com/match-detail/?id=3344135",
    "source": "results:2026-10-10",
    "tour": "atp-single",
    "gender": "men",
    "tournament": "Shanghai",
    "tournamentUrl": "https://www.tennisexplorer.com/shanghai/2026/atp-men/",
    "tournamentCountry": "China",
    "tournamentCountryCode": "cn",
    "tournamentYear": 2026,
    "tournamentPrizeMoney": 9415725,
    "tournamentPrizeCurrency": "USD",
    "round": "2R",
    "roundName": "2. round",
    "surface": "hard",
    "startTime": "2026-10-10T12:50:00.000Z",
    "hasStartTime": true,
    "status": "finished",
    "homePlayer": "Vacherot V.",
    "homePlayerSlug": "vacherot",
    "homeSeed": 17,
    "homeCountry": null,
    "awayPlayer": "Baez S.",
    "awayPlayerSlug": "baez-a8fb1",
    "awaySeed": null,
    "awayCountry": null,
    "winner": "home",
    "homeSets": 2,
    "awaySets": 0,
    "score": "6-4 7-6(5)",
    "sets": ["6-4", "7-6(5)"],
    "set1Home": 6, "set1Away": 4, "set2Home": 7, "set2Away": 6,
    "set3Home": null, "set3Away": null, "set4Home": null, "set4Away": null, "set5Home": null, "set5Away": null,
    "homeOdds": 1.31,
    "awayOdds": 3.5,
    "h2hBefore": null,
    "scrapedAt": "2026-10-15T20:35:23.673Z",
    "runId": "aBcD1234efGh5678"
}
```

#### `odds`

```json
{
    "type": "odds",
    "matchId": "3344135",
    "url": "https://www.tennisexplorer.com/match-detail/?id=3344135",
    "homePlayer": "Vacherot V.",
    "awayPlayer": "Baez S.",
    "startTime": "2026-10-10T12:50:00.000Z",
    "tournament": "Shanghai",
    "bookmaker": "10Bet",
    "market": "matchWinner",
    "line": null,
    "lineUnit": null,
    "homeOdds": 1.3,
    "awayOdds": 3.4,
    "overOdds": null,
    "underOdds": null,
    "odds": null,
    "homeOpeningOdds": 1.29,
    "awayOpeningOdds": 3.5,
    "homeOddsHistory": [{ "time": "2026-10-10T13:35:00.000Z", "odds": 1.3 }, { "time": "2026-10-10T03:50:00.000Z", "odds": 1.29 }],
    "awayOddsHistory": [{ "time": "2026-10-10T12:36:00.000Z", "odds": 3.4 }, { "time": "2026-10-10T12:21:00.000Z", "odds": 3.5 }],
    "oddsUpdatedAt": "2026-10-10T13:35:00.000Z",
    "isAverage": false,
    "isActive": true,
    "scrapedAt": "2026-10-15T20:35:27.519Z",
    "runId": "aBcD1234efGh5678"
}
```

For Over/Under rows the prices are in `overOdds`/`underOdds` and the line in `line` + `lineUnit` (`"22.5"`, `"games"`); for correct score the price is in `odds` and the score in `line` (`"2-0"`). Opening odds and history always follow the page's two price columns: home/over/score first, away/under second. An odds row is unique on `matchId` + `bookmaker` + `market` + `line` + `lineUnit` (handicap -1.5 exists in both sets and games). Some bookmakers (bet365, Betfair, Betsson, Unibet) publish only the current price, so their opening odds and history are null. When `maxResults` stops the run mid-match, the last match's bookmaker table is cut at the cap.

#### `player`

```json
{
    "type": "player",
    "playerSlug": "sinner-8b8e8",
    "url": "https://www.tennisexplorer.com/player/sinner-8b8e8/",
    "name": "Sinner Jannik",
    "country": "Italy",
    "sex": "man",
    "birthDate": "2001-08-16",
    "age": 25,
    "heightCm": 188,
    "weightKg": 76,
    "plays": "right",
    "currentRank": 1,
    "highestRank": 1,
    "highestRankDate": null,
    "currentDoublesRank": null,
    "highestDoublesRank": 34,
    "careerWins": 465,
    "careerLosses": 138,
    "winsBySurface": { "hard": 183, "clay": 119, "grass": 40, "indoors": 111, "notSet": 12 },
    "lossesBySurface": { "hard": 51, "clay": 46, "grass": 13, "indoors": 27, "notSet": 1 },
    "titles": 38,
    "titlesList": [{ "year": 2026, "tournament": "Wimbledon", "tournamentUrl": "https://www.tennisexplorer.com/wimbledon/2026/atp-men/", "surface": "grass", "level": "main", "prizeMoney": 74292561, "prizeCurrency": "EUR" }],
    "nextMatch": null,
    "injuries": [{ "dateFrom": "2025-10-05", "dateTo": "2025-10-15", "tournament": "Shanghai", "tournamentUrl": "https://www.tennisexplorer.com/shanghai/2025/atp-men/", "reason": "retired", "scoreAtRetirement": "7-6(3) 5-7 2-3" }],
    "yearsAvailable": [2026, 2025, 2024],
    "photoUrl": "https://www.tennisexplorer.com/res/img/player/OK7tW3bR-0zrsJ4N3.jpeg",
    "scrapedAt": "2026-10-15T20:39:41.347Z",
    "runId": "aBcD1234efGh5678"
}
```

#### `ranking`

```json
{
    "type": "ranking",
    "tour": "atp-men",
    "rankingDate": "2026-10-05",
    "year": 2026,
    "rank": 31,
    "movement": -1,
    "name": "Etcheverry Tomas Martin",
    "playerSlug": "etcheverry-7101a",
    "url": "https://www.tennisexplorer.com/player/etcheverry-7101a/",
    "country": "Argentina",
    "points": 1590,
    "scrapedAt": "2026-10-15T20:40:08.157Z",
    "runId": "aBcD1234efGh5678"
}
```

#### `h2h`

```json
{
    "type": "h2h",
    "url": "https://www.tennisexplorer.com/mutual/zverev-6f768/de-minaur/",
    "playerA": "Zverev Alexander",
    "playerASlug": "zverev-6f768",
    "playerB": "De Minaur Alex",
    "playerBSlug": "de-minaur",
    "winsA": 9,
    "winsB": 4,
    "meetings": 13,
    "winsBySurfaceA": { "hard": 5, "clay": 2, "grass": 0, "indoors": 1, "notSet": 1 },
    "winsBySurfaceB": { "hard": 2, "clay": 0, "grass": 0, "indoors": 2, "notSet": 0 },
    "lastMeeting": null,
    "lastMeetingYear": 2026,
    "includesExhibitions": true,
    "scrapedAt": "2026-10-15T20:40:20.995Z",
    "runId": "aBcD1234efGh5678"
}
```

Each meeting is also a `match` row (`source: "h2h:zverev-6f768-de-minaur"`).

### Tips and tricks

- **Round and surface come from tournament pages.** Day lists do not show them, so the Actor fetches each tournament page once per run (cached) and fills `round` only when that page lists the exact match id. Qualifying matches are looked up on the tournament's qualification page. Aggregated "Futures" lists have no tournament page — their rows keep `round`/`surface` null unless you turn on odds (the match-detail header has both).
- **Times are UTC.** The site prints local times (London with daylight saving); the Actor converts them. Day boundaries follow the site's London calendar day.
- **Head-to-head includes exhibitions and team events** (Laver Cup, United Cup …) — the site counts them, so `winsA`/`winsB` do too (`includesExhibitions: true`). Meetings carry the season only, no date or match id.
- **Retirements and walkovers** are detected from the score: a decided match with an unfinished last set is `retired`; a decided match with no games is a `walkover`.
- **Players by name**: a name with a space ("Jannik Sinner") is searched and the top-ranked match is used; ambiguous names are recorded in the run's `ERRORS` record with the candidates. Slugs (`sinner-8b8e8`) are exact.
- **Odds runs are big**: ~600 KB per match page and 200–300 rows per top match. Combine `includeOdds` with `tournamentFilter` and `maxResults`.
- **No proxy needed.** Keep the default pace (60 pages/minute, spread evenly) — the site is a shared resource.

### Integrations

Export the dataset as JSON, CSV or Excel, or fetch it by API (`/v2/datasets/{id}/items?view=matches`). Connect the Actor to Google Sheets, Zapier, Make or a webhook from the **Integrations** tab, or schedule a daily run (e.g. `dateFrom: "yesterday"`) to keep a results table up to date.

### Disclaimer

Data belongs to TennisExplorer / Livesport s.r.o. Check the site's terms before reusing it commercially.

# Changelog

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

# Actor input Schema

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

Choose the data set. Matches: results and fixtures for a date range (optionally with bookmaker odds). Players: profile plus match history. Rankings: ATP or WTA ranking table. Head-to-head: all meetings between two players.

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

Enter the first day to scrape (YYYY-MM-DD, London calendar day as the site lists it). Past days give results, today gives live and finished matches, future days give fixtures. Empty = today.

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

Enter the last day to scrape (inclusive). One run covers up to 92 days unless you set Maximum results. Empty = same as Date from.

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

Select the lists to scrape. ATP and WTA singles include Challengers, ITF and UTR events. "All matches" is one combined list of every tour.

## `includeOdds` (type: `boolean`):

Fetch each match's detail page and add one odds row per bookmaker and market line: current and opening odds, timestamped movement history, Over/Under, Asian handicap and correct score. About 200–300 extra rows per ATP/WTA match — set Maximum results to control cost.

## `tournamentFilter` (type: `string`):

Keep only matches whose tournament name contains this text (case-insensitive), e.g. "Shanghai" or "challenger". Leave empty for every tournament.

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

Add players as a TennisExplorer slug (sinner-8b8e8), a profile URL, or a name with a space (Jannik Sinner — the top-ranked match is used). A lower-case word without spaces is read as a slug.

## `years` (type: `array`):

Enter the seasons to list matches for (e.g. 2026, 2025), or "all" for every season the player has. Empty = current season.

## `includePlayerMatches` (type: `boolean`):

Add one row per match the player played in the chosen seasons (singles, doubles and mixed). Turn off to get only the profile rows.

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

Select the ranking table.

## `rankingYear` (type: `integer`):

Enter a past season to get its year-end ranking. Leave empty for the current ranking.

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

Limit the ranking to the top N players. Set to 0 for the whole table (about 2,000–3,000 players).

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

Add pairs as "slugA vs slugB" (zverev-6f768 vs de-minaur) or paste a https://www.tennisexplorer.com/mutual/… URL. The site's head-to-head includes exhibitions and team events.

## `maxResults` (type: `integer`):

Stop once this many rows (of any type) are delivered. Set to 0 for no limit. With a limit you may also scrape date ranges longer than 92 days.

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

Set how many pages load in parallel. The default is polite and fast enough.

## `maxRequestsPerMinute` (type: `integer`):

Set the page budget per minute. Requests are spread evenly across the minute.

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

Not needed — TennisExplorer answers datacenter IPs normally. Enable Apify Proxy only if your runs start failing.

## Actor input object example

```json
{
  "mode": "matches",
  "dateFrom": "2026-10-10",
  "dateTo": "2026-10-12",
  "tours": [
    "atp-single",
    "wta-single"
  ],
  "includeOdds": false,
  "tournamentFilter": "Shanghai",
  "players": [
    "sinner-8b8e8"
  ],
  "years": [
    "2026",
    "2025"
  ],
  "includePlayerMatches": true,
  "rankingTour": "atp-men",
  "rankingYear": 2025,
  "maxRank": 50,
  "pairs": [
    "zverev-6f768 vs de-minaur"
  ],
  "maxResults": 100,
  "maxConcurrency": 3,
  "maxRequestsPerMinute": 60,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every row of the run (filter by the "type" column, or open the per-type views in the Console).

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

Match rows: players, score and set columns, status, average odds, round, surface, tournament.

## `odds` (type: `string`):

One row per bookmaker and market line with current and opening odds and the movement history (only when odds are on).

## `resultsCsv` (type: `string`):

The same rows as a CSV download.

## `errors` (type: `string`):

Pages or inputs that could not be scraped (not found, ambiguous player names with candidates, failed pages). Only present when something went wrong.

# 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 = {
    "players": [
        "sinner-8b8e8"
    ],
    "maxRank": 50,
    "pairs": [
        "zverev-6f768 vs de-minaur"
    ],
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("teodor_banea/tennisexplorer-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 = {
    "players": ["sinner-8b8e8"],
    "maxRank": 50,
    "pairs": ["zverev-6f768 vs de-minaur"],
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("teodor_banea/tennisexplorer-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 '{
  "players": [
    "sinner-8b8e8"
  ],
  "maxRank": 50,
  "pairs": [
    "zverev-6f768 vs de-minaur"
  ],
  "maxResults": 100
}' |
apify call teodor_banea/tennisexplorer-scraper --silent --output-dataset

```

## MCP server setup

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