# TennisExplorer Scraper — Odds, Line Movement, Players, H2H (`memo23/tennisexplorer-scraper`) Actor

Scrape tennisexplorer.com — fixtures & results with odds, match details with per-bookmaker odds history (opening price, every move, 4 markets), player profiles, head-to-head records, ATP/WTA rankings incl. history. Pick a mode or paste any URL. JSON, CSV or Excel out.

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

## Pricing

from $5.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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## TennisExplorer Scraper — Fixtures, Results, Odds & Line Movement, Players, H2H, Rankings

Turn tennisexplorer.com into clean, structured tennis data. One actor covers the whole site: upcoming fixtures with odds, finished results with scores, deep match records with **per-bookmaker odds history across four markets**, full player profiles, head-to-head records, and ATP/WTA rankings — current or any historical week. Built for betting models, tennis analytics, and sports media that need more than final scores.

#### How it works

![How the TennisExplorer Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-tennisexplorer.png)

#### ✨ Why use this scraper?

- **Line movement nobody else sells** — match details capture every bookmaker's opening price, every timestamped odds change with its delta, the current price and trend, across Home/Away, Over/Under, Asian Handicap and Correct Score markets.
- **Seven modes in one actor** — fixtures, results, match details, player profiles, head-to-head, rankings and player search. No need to buy and wire up five different scrapers.
- **Paste any URL** — every tennisexplorer.com page type is auto-classified: match links, player pages, `/mutual/` head-to-heads, ranking tables, day listings. Mix them freely in one run.
- **Date ranges, both tours, singles and doubles** — scrape a whole week of results in one run, filtered to ATP/WTA, singles/doubles, or everything.
- **Historical depth** — weekly ranking snapshots going back years, per-season win/loss balances by surface, career titles and injury history per player.
- **Clean, model-ready rows** — ISO dates, numeric odds, tiebreak points split from games, one billed row per match/player/ranking entry. No padding rows, ever.

#### 🎯 Use cases

| Who | What they do with it |
|---|---|
| **Betting modelers & syndicates** | Backtest markets with opening→closing line movement per bookmaker; track steam moves; join odds to results by `matchId`. |
| **Tennis analytics teams** | Build win-probability and surface-adjusted models from per-season balances, H2H records and set-level scores. |
| **Sports media & content sites** | Auto-generate match previews (H2H, form, odds) and recaps (scores, movers in the rankings). |
| **Fantasy & prediction apps** | Daily fixtures with odds as the slate; results feed for scoring; player bios for profiles. |
| **Data products & researchers** | Historical ranking snapshots and results archives as time series, exported to JSON/CSV/Excel. |

#### 📥 Supported inputs

| Input | Example |
|---|---|
| Fixtures / day listings | `https://www.tennisexplorer.com/matches/`, `/next/?year=2026&month=08&day=14` |
| Results by day | `https://www.tennisexplorer.com/results/?type=atp-single&year=2026&month=08&day=11` |
| Match detail URLs or bare ids | `https://www.tennisexplorer.com/match-detail/?id=3290771` or `3290771` |
| Player profiles (slug or URL) | `djokovic` or `https://www.tennisexplorer.com/player/djokovic/` |
| Head-to-head pairs | `djokovic vs alcaraz-5ab70`, or `https://www.tennisexplorer.com/mutual/djokovic/alcaraz-5ab70/` |
| Rankings (with history) | `https://www.tennisexplorer.com/ranking/atp-men/?page=2&date=2026-07-27` |
| Player search | any name fragment, e.g. `alcaraz` |

**Not supported:** pages behind a TennisExplorer login, live-score push updates (each run is a snapshot), and non-tennisexplorer.com domains.

#### 🔄 How a run works

1. **Choose** a mode (or paste URLs — URLs win when both are present).
2. The actor **expands** your input into page fetches: date ranges become day pages, ranking runs walk `?page=N`, ids become detail URLs.
3. Pages are fetched **in parallel** with browser-grade TLS (impit) and a three-stack fallback race for resilience.
4. Each page is **parsed and normalised**: paired match rows, odds tables with nested history, player boxes, ranking tables.
5. Rows are **pushed to the dataset** — one row per match, player, head-to-head pair, or ranking entry — up to your `maxItems`.

#### ⚙️ Input parameters

| Field | Type | Default | Notes |
|---|---|---|---|
| `mode` | select | `fixtures` | `fixtures` · `results` · `matchDetails` · `players` · `h2h` · `rankings` · `searchPlayers` |
| `startUrls` | array | `[]` | Any tennisexplorer.com URLs; overrides `mode` when present |
| `tourType` | select | `all` | `all` · `atp-single` · `wta-single` · `atp-double` · `wta-double` |
| `dateFrom` / `dateTo` | string | today / same day | `YYYY-MM-DD`; inclusive range, capped at 62 days |
| `matchIds` | array | `[]` | Numeric ids or `/match-detail/` URLs |
| `includeOddsHistory` | boolean | `true` | Full timestamped movement per bookmaker on matchDetails rows |
| `includeRecentForm` | boolean | `true` | Last matches + mutual meetings on matchDetails rows |
| `playerSlugs` | array | `[]` | Slugs or full player URLs |
| `h2hPairs` | array | `[]` | `a vs b`, `a,b` or `/mutual/` URLs |
| `searchQuery` | string | — | For `searchPlayers` |
| `rankingTour` | select | `atp-men` | `atp-men` · `wta-women` |
| `rankingDate` | string | latest | Historical Monday snapshot, `YYYY-MM-DD` |
| `maxItems` | integer | `200` | Hard cap on billed rows — your spend guard |
| `maxConcurrency` | integer | `10` | Parallel page fetches |
| `proxy` | object | none | Site works over a direct connection; configure only if you want one |

#### 📊 Output overview

Every mode writes one row per entity to the default dataset. Match rows (fixtures/results) carry both sides with players, seeds, per-set scores (tiebreaks split out), the two odds, the winner and a `matchId` you can feed straight into `matchDetails`. Match-detail rows add both players' bios, the structured final score, surface win/loss comparison, recent form, and the odds block: for each bookmaker and each of the four markets, the current price, trend, opening price and (optionally) the full timestamped history. Player rows carry the bio, current/highest rankings, per-season and per-surface balances, titles and injuries. Ranking rows are one player each, stamped with the snapshot date. Every row includes `sourceUrl` and `scrapedAt`.

#### 📦 Output sample

A trimmed, real match-detail row (Montreal quarterfinal, scraped 2026-08-12):

```json
{
    "type": "matchDetail",
    "matchId": "3290771",
    "matchUrl": "https://www.tennisexplorer.com/match-detail/?id=3290771",
    "title": "Jodar - Fils",
    "date": "2026-08-11",
    "time": "20:20",
    "tournament": "Montreal",
    "round": "quarterfinal",
    "surface": "hard",
    "tour": "atp",
    "home": { "name": "Jodar Rafael", "slug": "jodar", "ranking": 15, "birthdate": "2006-09-17", "plays": "right" },
    "away": { "name": "Fils Arthur", "slug": "fils", "ranking": 24, "birthdate": "2004-06-12", "plays": "right" },
    "result": {
        "raw": "2 : 0(7-65, 6-3)",
        "setsHome": 2,
        "setsAway": 0,
        "sets": [
            { "home": 7, "away": 6, "awayTiebreak": 5 },
            { "home": 6, "away": 3 }
        ]
    },
    "surfaceComparison": {
        "hard": { "home": { "wins": 21, "losses": 9 }, "away": { "wins": 30, "losses": 15 } },
        "clay": { "home": { "wins": 19, "losses": 4 }, "away": { "wins": 9, "losses": 2 } }
    },
    "odds": {
        "homeAway": [
            {
                "bookmaker": "10Bet",
                "line": null,
                "side1": {
                    "odds": 1.65,
                    "trend": "down",
                    "opening": 1.9,
                    "history": [
                        { "at": "11.08. 20:04", "odds": 1.65, "delta": -0.02 },
                        { "at": "09.08. 08:37", "odds": 1.9, "delta": null }
                    ]
                },
                "side2": { "odds": 2.2, "trend": "up", "opening": 1.85 }
            }
        ],
        "overUnder": [{ "bookmaker": "10Bet", "line": "2.5", "side1": { "odds": 2.2 }, "side2": { "odds": 1.61 } }],
        "asianHandicap": [],
        "correctScore": []
    },
    "recentForm": {
        "home": [
            {
                "tournament": "Montreal",
                "date": "2026-08-11",
                "matchup": "Jodar - Fils",
                "outcome": "win",
                "score": "2:0",
                "matchUrl": "https://www.tennisexplorer.com/match-detail/?id=3290771"
            }
        ]
    },
    "sourceUrl": "https://www.tennisexplorer.com/match-detail/?id=3290771",
    "scrapedAt": "2026-08-12T21:31:04.660Z"
}
```

(`overUnder` had 92 rows, `asianHandicap` 125 and `correctScore` 40 on this match — trimmed here. Arrays are `[]`, never missing.)

#### 🗂 Key output fields

**Match rows (`type: "match"` — fixtures & results)**

| Field | Meaning |
|---|---|
| `matchId`, `matchUrl` | Feed these into `matchDetails` for the deep record |
| `status` | `scheduled` · `live` · `finished` |
| `tournament`, `tour`, `discipline` | e.g. `Montreal`, `atp`, `singles` |
| `home` / `away` | `players[]` (name, url, slug — two entries for doubles), `seed`, `setsWon`, `setScores[]` (`{games, tiebreak?}`), `odds`, `h2hWins` (fixtures) |
| `winner` | `home` · `away` · `null` |
| `date`, `time` | Day (mode runs) and local match time |

**Match-detail rows (`type: "matchDetail"`)** — adds `round`, `surface`, per-player bios (`ranking`, `birthdate`, `heightCm`, `weightKg`, `plays`, `turnedPro`), structured `result.sets`, `surfaceComparison`, `recentForm` (home/away/mutual), and `odds` with `homeAway` / `overUnder` / `asianHandicap` / `correctScore` — each row per bookmaker with `line`, `side1`/`side2` = `{odds, trend, opening, history[]}`.

**Player rows (`type: "player"`)** — `name`, `country`, `heightCm`, `weightKg`, `age`, `birthdate`, `plays`, `rankSingles`/`rankDoubles` (`{current, highest}`), `careerBalance` + `seasonBalance[]` (wins/losses total and per surface), `titles[]`, `titleCounts[]`, `injuries[]`, `recentTournaments[]`.

**H2H rows (`type: "h2h"`)** — `players[]`, official `record` (e.g. `5 : 6`), bio `comparison`, `mutualMatches[]` with year, tournament, surface, round, winner/loser and set scores.

**Ranking rows (`type: "rankingEntry"`)** — `tour`, `snapshotDate`, `rank`, `move`, `player`, `country`, `points`.

**Search rows (`type: "playerSearchResult"`)** — `query`, `tour`, `rank`, `player` (with the `slug` you need for the other modes).

#### ❓ FAQ

**How do I get odds history for a match I found in fixtures?**
Take the `matchId` from the fixtures row and run `matchDetails` with it. Chaining fixtures → matchDetails each morning gives you a daily line-movement archive.

**What do odds-history timestamps look like?**
As the site prints them: `"11.08. 20:04"` (day.month. hour:minute, no year). The `opening` price is extracted for you, so open→close deltas don't require parsing the history at all.

**Can I get last year's rankings?**
Yes — set `rankingDate` to any Monday (e.g. `2025-06-02`). TennisExplorer keeps weekly snapshots going back years; the row's `snapshotDate` confirms which week you got.

**Does it cover doubles and Challenger/ITF events?**
Doubles: yes — use `tourType: atp-double`/`wta-double`; each side then carries two players. Coverage mirrors the site, which includes Challengers, ITF and exhibition events alongside the main tours.

**Do I need a proxy?**
No. The site is served fine over a direct connection, which keeps your cost at compute + results only. If you prefer a proxy anyway, configure one in the input and it's honoured.

**Why did a run return fewer rows than maxItems?**
`maxItems` is a cap, not a target — a quiet Tuesday simply has fewer matches. Failed pages never produce placeholder rows; gaps are reported in the run's status message instead.

#### 💬 Support

Found a bug or missing a field? Open an issue on the actor's **Issues tab** in Apify Console — issues are answered within 1–2 business days. Feature requests are welcome: if TennisExplorer shows it, it can usually be added.

#### 🛠 Additional services

Need a custom pipeline (scheduled line-movement archive, merged multi-source tennis feed, direct-to-database delivery) or a scraper for another sports/odds site? Contact me through the actor page — custom builds and SLAs available.

#### 🔎 Explore more scrapers

More actors by the same developer: [memo23 on Apify Store](https://apify.com/memo23) — flight prices, job boards, review platforms, real-estate portals and more, all pay-per-result.

#### 🤖 For AI Agents & LLM Apps

This actor is MCP-friendly: modes map cleanly to tool calls (`fixtures` → "what's on today", `matchDetails` → "odds history for match X", `h2h` → "how do these two compare"). Outputs are stable, typed JSON with no HTML fragments, safe to feed straight into an LLM context or a function-calling loop. Use `searchPlayers` first to resolve names → slugs, then chain `players`/`h2h`. Keep `maxItems` low per call to control cost; every row is self-contained.

***

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by TennisExplorer (tennisexplorer.com) or any of its operators, nor by the ATP, WTA, ITF or any tournament, nor by any bookmaker whose odds appear in the data. All trademarks mentioned are the property of their respective owners.

The scraper accesses only publicly available pages — no authenticated endpoints, no paywalled content, and no live-score push feeds. Odds data is informational; nothing in this actor's output constitutes betting advice. Users are responsible for ensuring their use complies with TennisExplorer's Terms of Service, applicable data-protection law (GDPR, CCPA, etc.), local gambling regulations, and any contractual obligations of their own organisation.

***

### SEO Keywords

tennisexplorer scraper, tennis explorer api, tennis odds scraper, tennis line movement data, opening closing odds tennis, tennis results scraper, tennis fixtures api, atp rankings scraper, wta rankings history, tennis head to head data, tennis player stats scraper, tennis betting data, odds history per bookmaker, tennis match details api, scrape tennisexplorer, tennis data for betting models, tennis backtesting dataset, apify tennis scraper, sofascore alternative tennis, flashscore alternative tennis, oddsportal alternative

# Actor input Schema

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

What this run scrapes. `fixtures` = upcoming matches with odds (today by default), `results` = finished matches with scores and closing odds (yesterday by default), `matchDetails` = deep per-match records including per-bookmaker odds history, `players` = full player profiles, `h2h` = head-to-head records, `rankings` = ATP/WTA ranking tables (current or historical), `searchPlayers` = find players and their slugs by name.

## `startUrls` (type: `array`):

Any tennisexplorer.com URLs: `/matches/`, `/next/`, `/results/`, `/match-detail/?id=…`, `/player/{slug}/`, `/mutual/{a}/{b}/`, `/ranking/…` or `/list-players/…`. Each URL is auto-classified and routed to the right parser. Leave empty to use Mode instead.

## `tourType` (type: `string`):

Which tournaments to include: both tours, or a single tour, singles or doubles.

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

First day to scrape. Defaults: fixtures = today, results = yesterday. Fixtures for past dates are automatically served from the results pages.

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

Last day, inclusive. Defaults to Date from (a single day). Ranges are capped at 62 days per run.

## `matchIds` (type: `array`):

Numeric match ids (e.g. `3290771`) or full `/match-detail/?id=…` URLs. Get ids from fixtures/results rows (`matchId` field) or from any TennisExplorer match link.

## `includeOddsHistory` (type: `boolean`):

Keep the full per-bookmaker odds movement — every price change with timestamp plus the opening price — on matchDetails rows, across all four markets (Home/Away, Over/Under, Asian Handicap, Correct Score). Turn off for lighter rows with current + opening odds only.

## `includeRecentForm` (type: `boolean`):

Keep the recent-matches blocks (each player's last matches and their mutual meetings) on matchDetails rows.

## `playerSlugs` (type: `array`):

For the players mode: slugs like `djokovic` or full `/player/…` URLs. Find slugs with the searchPlayers mode.

## `h2hPairs` (type: `array`):

For the h2h mode. One pair per line: `djokovic vs alcaraz-5ab70`, `djokovic, alcaraz-5ab70` or a full `/mutual/…/…/` URL.

## `searchQuery` (type: `string`):

For the searchPlayers mode: part of a player's name, case-insensitive (e.g. `alcaraz`). Returns matching players with their slugs and current rank for both tours.

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

Which ranking to walk in rankings mode.

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

Historical weekly snapshot to read (Mondays). Leave empty for the latest ranking. TennisExplorer keeps weekly snapshots going back years.

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

Global cap on dataset rows for the run. You are billed per row, so this is your spend guard.

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

Parallel page fetches. 10 is a good default; raise it for large matchDetails batches.

## `proxy` (type: `object`):

tennisexplorer.com is served fine over a direct connection, so no proxy is used unless you configure one here.

## Actor input object example

```json
{
  "mode": "fixtures",
  "startUrls": [],
  "tourType": "all",
  "matchIds": [],
  "includeOddsHistory": true,
  "includeRecentForm": true,
  "playerSlugs": [],
  "h2hPairs": [],
  "rankingTour": "atp-men",
  "maxItems": 200,
  "maxConcurrency": 10
}
```

# Actor output Schema

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

All scraped rows in the default dataset. Row shape depends on the run mode; every row carries a `type` discriminator plus `sourceUrl` and `scrapedAt`.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/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 = {}

# Run the Actor and wait for it to finish
run = client.actor("memo23/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 '{}' |
apify call memo23/tennisexplorer-scraper --silent --output-dataset

```

## MCP server setup

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