# Tennis Odds Scraper | Results & Closing Odds, $3/1K (`aurelix/tennis-odds-scraper`) Actor

Extract tennis matches with opening and closing odds per bookmaker: ATP, WTA, Challenger and ITF results and upcoming matches, scores by set, round, surface, rankings, implied probabilities, stable match IDs and a new-since-last-run feed. $3.00 per 1,000 matches.

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

## Pricing

from $2.50 / 1,000 matches

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 is Tennis Odds Scraper?

**Get tennis results and upcoming matches with opening and closing odds from 10-15 bookmakers, scores, round, surface and implied probabilities for $3.00 per 1,000 matches, no login.**

For bettors, modellers and odds analysts. ATP, WTA, Challenger and ITF singles and doubles, stable match IDs across runs, and a free only-new mode for hourly or daily feeds.

[![Tennis Odds Scraper: example output from a real run](https://api.apify.com/v2/key-value-stores/twkCCtTWzsi8fVbx5/records/tennis-odds-scraper-card-v1.png)](https://apify.com/aurelix/tennis-odds-scraper)

[![Tennis Odds Scraper: 75-second demo video](https://api.apify.com/v2/key-value-stores/twkCCtTWzsi8fVbx5/records/tennis-odds-scraper-demo-thumb-v1.png)](https://api.apify.com/v2/key-value-stores/twkCCtTWzsi8fVbx5/records/tennis-odds-scraper-demo-v1.mp4)

**[▶ Watch the 75-second demo](https://api.apify.com/v2/key-value-stores/twkCCtTWzsi8fVbx5/records/tennis-odds-scraper-demo-v1.mp4)**

| | |
|---|---|
| **Source** | TennisExplorer public match lists and match pages, 400-700 matches a day |
| **Modes** | Finished results (days back or a date range) or upcoming matches |
| **Filters** | Tour level, singles/doubles, tournaments, players, only priced matches |
| **Output** | 47 fields per match; JSON, CSV, Excel, API |
| **Automate** | Only new since last run, Slack/webhook digest, schedules, MCP |
| **Price** | $3.00 per 1,000 matches ($2.50 per 1,000 matches on paid Apify plans) |
| **Login needed** | No |

### Daily monitor: only what's new, every morning

Set it once, get only the new matches every morning. With **Only new since last run** on, the first run returns everything that matches; each later run skips matches that earlier runs already returned, so you pay only for new ones.

```json
{
  "mode": "results",
  "daysBack": 1,
  "categories": [
    "atp",
    "wta"
  ],
  "matchType": "singles",
  "onlyWithOdds": true,
  "incremental": true,
  "stateKey": "atp-wta-results"
}
```

1. Paste this input into the **Input** tab (JSON view) and click **Save as a new task**; it appears under **Saved tasks**.
2. Open the task from **Saved tasks** and click **Schedule**.
3. Choose **Daily**, pick the hour, and save.

**Cost example:** ATP and WTA singles, about 60 newly finished matches a day ≈ $5.55/month on the free plan ($4.65 on paid plans): one run a day for 30 days, start fee included. The first run returns everything that matches, so it costs more.

Add a Slack incoming-webhook URL in `slackWebhookUrl` (or any URL in `webhookUrl`) to get a short digest of each run's new matches; nothing is sent when a run finds nothing.

### Why Tennis Odds Scraper users pick this one

- **Odds per bookmaker.** Opening and closing prices from 10-15 bookmakers, each with its time.
- **Results joined to odds.** Score by set, winner, retirements and walkovers in the same row.
- **62% under the leading Actor.** $3.00 per 1,000 matches against $8.
- **Market probabilities.** Implied win chances with the bookmaker margin removed.
- **A feed, not a dump.** Free only-new mode with a Slack or webhook digest.

### Get started in 3 steps

1. Pick 'Results' (finished matches: days back or a date range) or 'Upcoming' (today plus N days); match pages with per-bookmaker opening and closing odds are on by default.
2. Optionally narrow it to ATP/WTA/Challenger/ITF levels, singles or doubles, tournaments or players, and turn on 'Only new since last run' for a feed.
3. Click Start, then download JSON, CSV or Excel, or schedule it hourly or daily and send new matches to Slack, Google Sheets, Zapier, Make or n8n.

**[▶ Try it with the example input](https://apify.com/aurelix/tennis-odds-scraper)**

The form opens with an example input already filled in.

### How much does it cost to scrape tennis odds?

Pay per event: you pay only for what the run returns, plus a small start fee. Bronze, Silver, Gold and higher Apify plans pay less per result.

| Matches | Free plan | Paid plans (Bronze, Silver, Gold) |
|---|---|---|
| 100 | $0.31 | $0.26 |
| 1,000 | $3.01 | $2.51 |
| 10,000 | $30.01 | $25.01 |

Totals include the $0.005 start fee per run, rounded to the cent. Apify's free plan includes $5 of credit a month, enough for about 1,600 matches.

Matches outside your filters, already returned with 'Only new since last run', or beyond 'Max matches' are not charged.

#### Billing events

| Event | Free plan | Paid plans (Bronze, Silver, Gold) |
|---|---|---|
| `match` | $3.00 per 1,000 matches | $2.50 per 1,000 matches |
| `apify-actor-start` | $0.005 per run | $0.005 per run |

### Tennis Odds Scraper vs alternatives

| | **This Actor** | crawlstone/tennis-scraper | extractify-labs/flashscore-tennis-matches |
|---|---|---|---|
| **Price per 1,000 matches, free plan** | $3 | $8 | $1 |
| **Price per 1,000, paid plans** | $2.50 | $6 to $7.36 | $1 |
| **Start fee per run** | $0.005 | none | $0.00005 |
| **Bookmaker odds** | 10-15 bookmakers per match, opening and closing with times | One odds pair (SofaScore), live matches | None |
| **Implied probabilities, margin removed** | Yes | No | No |
| **Results by date range** | Yes, up to 92 days per run | By date (tournaments mode) | Recent days (day offsets) |
| **Round, surface, rankings** | Yes (match page) | Yes (tournament/player modes) | Yes |
| **Only new since last run + Slack/webhook** | Yes, free | No | No |
| **Live scores, point-by-point** | No | Yes | Live game score |

Prices checked regularly on each Actor's Store page.

### Benchmarks: how we compare

| Run | Matches returned | With per-bookmaker odds | Run time |
|---|---|---|---|
| Prefilled form: results, 20 matches | 20 | 19 | 11 s |
| Upcoming matches, 10 matches | 10 | 10 | 8 s |
| Results, 3 days, match pages on, 1,000 cap | 1,000 (1,003 page loads, 0 failed) | 925 | 326 s |
| Upcoming ATP + WTA, only new, first run | 48 | not recorded | 24 s |
| Same input, run again | 0 new (48 skipped, not charged) | - | 3 s |
| Worst empty run: 92-day range, player that matches nothing | 0 | - | 114 s |
| Results, 2 days, all levels, match pages off (local) | 1,175, every match_id unique | 0 (1,041 with a price from the day list) | not timed |

The first six rows are single runs on the Apify cloud at 256 MB memory. The last row is one local run used to check duplicates across two full days of all levels with match pages switched off. Each row is one run, so treat times as a guide, not a guarantee.

### What data you get

**Tennis Odds Scraper** turns TennisExplorer's public daily match lists and match pages into one clean row per match: ATP, WTA, Challenger, ITF and UTR singles and doubles, with tournament, surface, round, players with seeds and rankings, start time in UTC, the result set by set (retirements and walkovers marked), and the Home/Away odds of every listed bookmaker with the opening and closing price and when each was set, plus averages, best prices and implied win probabilities with the bookmaker margin removed. Pull finished results for any date range, or upcoming matches with the latest prices, and turn on 'Only new since last run' for an hourly or daily feed with a Slack or webhook digest.

- match_id (stable across runs), date and start_time (UTC), status, tournament, category, match_type, round, surface
- Both players: short and full name, seed, singles ranking; winner, sets, score (e.g. 7-6(3) 6-4), set_scores
- Average opening and closing odds, best closing odds, implied_prob_p1 / p2 (margin removed), overround
- bookmaker_odds: opening and closing price per bookmaker with the time each was set

| player1 | player2 | score | closing_avg_p1 | closing_avg_p2 | implied_prob_p1 | tournament | round | surface | bookmakers_count |
|---|---|---|---|---|---|---|---|---|---|
| Fils A. | Machac T. | 6-3 7-6(4) | 1.553 | 2.427 | 0.6098 | Paris | 2. round | indoors | 15 |
| Struff J. | Tirante T. | 6-7(5) 6-3 7-6(3) | 1.42 | 2.838 | 0.6665 | Paris | 2. round | indoors | 15 |
| Alcaraz C. | Michelsen A. | 7-6(4) 6-3 | 1.167 | 5.023 | 0.8115 | Paris | 2. round | indoors | 15 |
| Berrettini M. | Humbert U. | 7-6(3) 7-5 | 2.033 | 1.769 | 0.4653 | Paris | 2. round | indoors | 15 |
| Borges N. | Khachanov K. | 6-3 6-4 | 2.696 | 1.457 | 0.3508 | Paris | 2. round | indoors | 15 |
| Fritz T. | Molcan A. | 6-7(6) 7-5 7-6(5) | 1.229 | 4.151 | 0.7716 | Paris | 2. round | indoors | 15 |

One full match record, exactly as a real run returned it (long text and lists shortened here):

```json
{
  "match_id": "te-3368391",
  "source_match_id": "3368391",
  "match_url": "https://www.tennisexplorer.com/match-detail/?id=3368391",
  "title": "Fils A. vs Machac T.",
  "date": "2026-11-04",
  "start_time": "2026-11-04T20:20:00Z",
  "tournament": "Paris",
  "tournament_url": "https://www.tennisexplorer.com/paris/2026/atp-men/",
  "tournament_country": "FR",
  "category": "atp",
  "gender": "men",
  "match_type": "singles",
  "round": "2. round",
  "surface": "indoors",
  "status": "finished",
  "player1": "Fils A.",
  "player1_full_name": "Fils Arthur",
  "player1_url": "https://www.tennisexplorer.com/player/fils/",
  "player1_seed": "6",
  "player1_ranking": 7,
  "player2": "Machac T.",
  "player2_full_name": "Machac Tomas",
  "player2_url": "https://www.tennisexplorer.com/player/machac/",
  "player2_seed": null,
  "player2_ranking": 69,
  "winner": "player1",
  "winner_name": "Fils A.",
  "sets_p1": 2,
  "sets_p2": 0,
  "score": "6-3 7-6(4)",
  "set_scores": [
    {
      "p1": 6,
      "p2": 3,
      "tiebreak": null
    },
    {
      "p1": 7,
      "p2": 6,
      "tiebreak": 4
    }
  ],
  "odds_p1": 1.55,
  "odds_p2": 2.43,
  "implied_prob_p1": 0.6098,
  "implied_prob_p2": 0.3902,
  "overround": 0.0559,
  "opening_avg_p1": 1.429,
  "opening_avg_p2": 2.74,
  "closing_avg_p1": 1.553,
  "closing_avg_p2": 2.427,
  "closing_best_p1": 1.59,
  "closing_best_p2": 2.5,
  "bookmakers_count": 15,
  "bookmaker_odds": [
    {
      "bookmaker": "10Bet",
      "opening_p1": 1.42,
      "opening_p2": 2.7,
      "opening_at": "2026-11-02T18:37:00Z",
      "closing_p1": 1.53,
      "closing_p2": 2.4,
      "closing_at": "2026-11-04T14:23:00Z",
      "changes": 20
    },
    {
      "bookmaker": "1xBet",
      "opening_p1": 1.43,
      "opening_p2": 2.92,
      "opening_at": "2026-11-02T18:26:00Z",
      "closing_p1": 1.59,
      "closing_p2": 2.4,
      "closing_at": "2026-11-04T20:07:00Z",
      "changes": 30
    },
    {
      "bookmaker": "bet-at-home",
      "opening_p1": 1.4,
      "opening_p2": 2.85,
      "opening_at": "2026-11-02T18:38:00Z",
      "closing_p1": 1.55,
      "closing_p2": 2.45,
      "closing_at": "2026-11-04T19:52:00Z",
      "changes": 30
    },
    "... [12 more]"
  ],
  "odds_source": "bookmakers",
  "source_url": "https://www.tennisexplorer.com/match-detail/?id=3368391",
  "scraped_at": "2026-11-05T00:02:04Z"
}
```

### Use cases

- Bettors and tipsters measuring closing-line value: compare the price you took with the closing average
- Modellers backtesting Elo or serve/return models against market-implied probabilities
- Odds and trading analysts tracking line movement from opening to close across 10-15 bookmakers
- Media, apps and newsletters publishing daily results and tomorrow's order of play with prices

### Use-case recipes

#### Closing-line value check for bettors

For bettors and tipsters who want to compare the price they took with the market close. Run Results mode for yesterday and today at ATP and WTA level, singles, priced matches only. Join your bet log on player names or match_id, then compare your price with closing_avg_p1 / closing_avg_p2 and closing_best_p1 / closing_best_p2; the per-bookmaker closing price is in bookmaker_odds.

```json
{
  "mode": "results",
  "daysBack": 1,
  "categories": [
    "atp",
    "wta"
  ],
  "matchType": "singles",
  "onlyWithOdds": true,
  "maxResults": 200
}
```

#### Tomorrow's order of play with prices

For newsletters, apps and tipping sites. Run Upcoming mode for today and tomorrow, priced matches only, with 'Only new since last run' on and schedule it every few hours. Each run returns only newly scheduled matches; publish start_time (UTC), tournament, round, player1 and player2 with odds_p1, odds_p2 and implied_prob_p1. Add a Slack webhook to get a digest of the top 10.

```json
{
  "mode": "upcoming",
  "daysAhead": 1,
  "categories": [
    "atp",
    "wta",
    "challenger"
  ],
  "onlyWithOdds": true,
  "incremental": true,
  "maxResults": 150
}
```

#### Backtest a model against the market

For modellers testing Elo or serve/return models. Pull a month of finished ATP singles with odds, then compare your model's win chance with implied_prob_p1 (closing averages, margin removed) and score it on winner. Raise maxResults once the sample looks right, and run one range of up to 92 days per run for older periods. Rankings are the players' current ones, not as of the match, so do not use them as model inputs for old matches.

```json
{
  "mode": "results",
  "daysBack": 30,
  "categories": [
    "atp"
  ],
  "matchType": "singles",
  "onlyWithOdds": true,
  "maxResults": 300
}
```

#### Player watchlist feed without match pages

For fans, agents and analysts following a few players. List them in players (surname matching), turn match pages off for a fast run that reads one day list per day, turn on 'Only new since last run' and schedule it hourly. Each new result arrives with score, set_scores, winner_name and the day-list price in odds_p1 / odds_p2; set slackWebhookUrl or webhookUrl to forward it.

```json
{
  "mode": "results",
  "daysBack": 1,
  "players": [
    "Jannik Sinner",
    "Swiatek"
  ],
  "bookmakerOdds": false,
  "incremental": true,
  "maxResults": 50
}
```

### Input

| Field | Type | What it does | Default |
|---|---|---|---|
| `mode` (What to get) | string | 'results' = finished matches with scores and closing odds for past days; 'upcoming' = matches not started yet with the... | "results" |
| `maxResults` (Max matches) | integer | Stop after this many matches. | 500 |
| `daysBack` (Results: days back) | integer | Results mode: finished matches from today (UTC) and the N days before. | 1 |
| `daysAhead` (Upcoming: days ahead) | integer | Upcoming mode: matches scheduled today (UTC) and on the next N days. | 1 |
| `categories` (Tours and levels) | array | Keep only these levels. |  |
| `matchType` (Singles or doubles) | string | Example: singles. | "all" |

All 16 fields, with examples: see the **Input** tab.

### Switching from crawlstone/tennis-scraper

Already running `crawlstone/tennis-scraper`? It is built around live matches and SofaScore IDs; this Actor is built around finished and upcoming matches with bookmaker odds. Map its input fields like this:

| `crawlstone/tennis-scraper` input | This Actor's input | Notes |
|---|---|---|
| `mode` | `mode` | Theirs has liveMatches, tournaments, point-by-point, players and rankings modes. Ours has results (finished matches) and upcoming. There is no live mode: matches in progress are skipped until they finish. |
| `maxMatches` | `maxResults` | Theirs caps live matches only and 0 means all. Ours caps every run, from 1 to 50,000, default 500. |
| `matchType` | `matchType` | Theirs is a list; ours is one value: all, singles or doubles. |
| `tournamentLevel` | `categories` | Levels: atp, wta, challenger, itf-men, itf-women, utr, other. Empty = all. |
| `gender` | `categories` | Pick the men's levels (atp, challenger, itf-men) or the women's (wta, itf-women). Each row also carries a gender field. |
| `date` | `daysBack`, or `dateFrom` + `dateTo` | Theirs finds tournaments on one date. Ours returns the matches of today and N days back, or of a date range of up to 92 days. |
| `tournamentId` | `tournaments` | Words from the tournament name, e.g. Paris, not numeric IDs. |
| `playerId` | `players` | Player names, matched on the surname. You get that player's matches, not a player profile. |
| `matchId` (point-by-point) | no equivalent | No point-by-point or live scores. |
| `ranking` | no equivalent | No ranking tables. Each match row carries both players' current singles ranking from the match page. |

- Match IDs change: ours are TennisExplorer match numbers (te-3368391), the same in every run and mode, so joins between upcoming and results runs use match_id.
- One row per match holds the odds of every listed bookmaker in bookmaker_odds (opening and closing price with times), plus averages, best prices and implied probabilities. The comparison table above lists one odds pair for theirs.
- Pricing model: a flat price per match returned plus a small start fee per run; matches outside your filters, beyond Max matches, or already returned in only-new mode are not charged.
- Need live scores or point-by-point? Keep theirs for that and use this Actor for results, upcoming matches and odds.

### Use with AI agents (Apify MCP server, Claude, ChatGPT, n8n AI agent)

Tennis Odds Scraper is a ready-made tool for AI agents. Through the Apify MCP server an agent reads this Actor's input schema, fills in the input from your request, starts the run and reads the matches back. You pay the same per-result price as a run started by hand, from your own Apify account.

#### Example prompts

- "Get yesterday's finished ATP and WTA singles matches with the closing odds of every bookmaker and the implied win probabilities, as a table"
- "List today's and tomorrow's ATP and Challenger matches that bookmakers price, with start time in UTC, round, surface and the best odds for each player"
- "Pull every finished ATP singles match of the last 30 days with opening and closing average odds and tell me how often the closing favourite won"

#### Claude (claude.ai and Claude Desktop)

Open **Settings > Connectors**, choose **Add custom connector**, paste `https://mcp.apify.com/?tools=aurelix/tennis-odds-scraper` and sign in to Apify when asked. Then ask Claude one of the prompts above.

#### Claude Code, Cursor, VS Code and other MCP clients

```bash
claude mcp add --transport http apify "https://mcp.apify.com/?tools=aurelix/tennis-odds-scraper"
```

Clients configured with JSON take the same URL:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=aurelix/tennis-odds-scraper"
    }
  }
}
```

A client without Apify sign-in sends the header `Authorization: Bearer <YOUR_APIFY_TOKEN>`; to run the server locally instead, use `npx -y @apify/actors-mcp-server --actors aurelix/tennis-odds-scraper` with `APIFY_TOKEN` set.

#### ChatGPT

Where your ChatGPT plan offers custom connectors (developer mode), add `https://mcp.apify.com/?tools=aurelix/tennis-odds-scraper` as a connector, sign in to Apify, and ask for the data in plain words.

#### n8n AI agent

Give an **AI Agent** node an **MCP Client Tool** with the endpoint `https://mcp.apify.com/?tools=aurelix/tennis-odds-scraper` and a Bearer credential holding your Apify token. The agent can then call Tennis Odds Scraper whenever a workflow step needs matches; for a fixed, non-agent workflow use Apify's n8n node with **Run Actor** instead.

### Use it from AI agents (MCP), the API or no-code

#### Copy this to your AI assistant

```text
Use the Apify Actor aurelix/tennis-odds-scraper (Tennis Odds Scraper) through Apify's MCP server or API. Main input: mode (What to get); maxResults (Max matches). Example input: {"mode": "results", "dateFrom": "2026-10-01", "dateTo": "2026-10-31", "categories": ["atp", "wta"], "matchType": "singles", "onlyWithOdds": true, "maxResults": 5000}. Each result is one match with fields such as match_id, source_match_id, match_url, title, date, start_time, tournament, tournament_url. It costs $3.00 per 1,000 matches on Apify's free plan plus a $0.005 start fee per run, and needs no login.
```

MCP server URL for this Actor: `https://mcp.apify.com/?tools=aurelix/tennis-odds-scraper` (setup for each client in the section above).

#### API

Start a run and read the dataset with Apify's REST API or the Python and JavaScript clients:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("aurelix/tennis-odds-scraper").call(run_input={'mode': 'results', 'dateFrom': '2026-10-01', 'dateTo': '2026-10-31', 'categories': ['atp', 'wta'], 'matchType': 'singles', 'onlyWithOdds': True, 'maxResults': 5000})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

#### No-code: schedule it in 3 clicks

Send the results to Google Sheets, Zapier, Make, n8n, Slack or any webhook URL.

1. Fill in the input, then click **Save as a new task**.
2. In the task, click **Schedule** and pick daily or weekly (Apify Schedules).
3. Turn on **Only new since last run** so each run returns only what is new, and paste a Slack or webhook URL into **Slack webhook URL** / **Webhook URL** to get a digest of the new items (count plus the top 10) after every run.

Prefer n8n or Make? Ask in the **Issues** tab and we will share a ready-made workflow for it.

### FAQ

#### Can I scrape tennis odds without login?

Yes. It never logs in, so you need no TennisExplorer account, password or cookies; only a free Apify account to run it.

#### Is there a tennis odds API?

You can use this Actor as one. Start it with Apify's REST API or the Python and JavaScript clients and read the results as JSON (see the code above), or call it from an AI agent through MCP.

#### How much does it cost to scrape tennis odds?

About $3.01 for 1,000 matches on Apify's free plan and $2.51 on paid plans, start fee included. The free $5 monthly credit covers about 1,600 matches.

#### Can I export tennis odds data to Excel or Google Sheets?

Yes. Download the results as Excel, CSV, JSON, XML or HTML from the run's **Storage** tab, or send them to Google Sheets with Apify's Google Sheets integration or the API.

#### Is it legal to scrape tennis odds?

It reads only public data any logged-out visitor can see: TennisExplorer's public match lists and match pages, on paths its robots.txt allows, at a polite pace with a declared bot user-agent. It never logs in, never solves CAPTCHAs and never visits bookmaker websites. Match results and odds are facts about public sporting events; no personal data beyond players' public names, seeds and rankings. To have a record removed, open an issue on this Actor's Issues tab; if TennisExplorer formally asks us to stop, this Actor will stop. You are responsible for using the data lawfully, incl. gambling and data-reuse laws where you are; this is not legal advice.

#### Which bookmakers are included?

Those TennisExplorer lists for the match, typically 10 to 15 at ATP/WTA and Challenger level (bet365, Pinnacle, Betfair, William Hill, Unibet, bwin and more). Many ITF and UTR matches have few or none; 'Only matches with odds' skips those.

#### What exactly are opening and closing odds?

Per bookmaker, opening is the oldest price in its change history on the match page and closing its last price before the start (current price for upcoming matches), each with its UTC time.

#### How do I get a live-ish feed?

Turn on 'Only new since last run' and schedule Results mode (days back 1) hourly: each run returns only matches finished since the last one. Add a Slack or webhook URL for a digest.

#### What happens if TennisExplorer slows down or a day does not load?

After 6 match pages in a row fail, the run stops opening match pages and returns the remaining matches from the day list only: score, players and the day-list price, without round, surface, rankings or per-bookmaker odds. After 3 day lists in a row fail, the run stops. Matches already read are kept, the status message says how many came without a match page and which days did not load, and you pay only for matches written to the dataset.

#### What am I charged for a run that returns nothing?

Only the small per-run start fee. Matches are charged one by one as they are written; a run with no matching matches, or a repeat run in only-new mode with nothing new, writes nothing and pays no match charges.

#### How does 'Only new since last run' decide what is new?

It keeps a memory of returned matches per mode and filter set (or per 'State key' if you name one). In Results mode each finished match is returned once. In Upcoming mode each scheduled match is returned once, so later odds moves on that match are not sent again; run Results mode later for the closing odds. Skipped matches are not charged, and matches unseen for 120 days drop out of the memory.

#### Why are round, surface, full names or rankings empty on some rows?

They come from the match page. They are empty when 'Bookmaker odds' (match pages) is switched off, or when that match page did not load. Rankings are singles rankings only, and they are the players' current rankings, not as of the match date.

#### What do odds_p1, closing_avg_p1 and odds_source mean?

odds_p1 / odds_p2 are the prices shown on the day list. closing_avg and opening_avg are the means of each bookmaker's last and first price; closing_best is the highest closing price across bookmakers. implied_prob_p1 / p2 come from the closing averages when the match page was read, otherwise from the day-list prices, with the margin (overround) removed. odds_source is 'bookmakers', 'list', or empty when no price was shown.

#### How far back can one run go?

Results mode reads today and up to 92 days back, or a date range of at most 92 days. A longer range is refused with a clear message; split it into several runs. A 'To date' in the future is cut to today.

#### Which day does a match belong to, and are times UTC?

Yes. Pages are read with the site's public time-zone setting at UTC, so date is the UTC day of the list and start_time is UTC. Odds history times are converted from the site's Prague time to UTC.

#### How does the players filter match names?

On the last word of what you type (the surname), ignoring case and accents, against both players. 'Jannik Sinner' and 'Sinner' both match any player named Sinner on the list.

#### How many matches does a day have, and how long does a run take?

A busy day lists 400 to 700 matches across all levels, about 60-100 of them at ATP/WTA level. With match pages on, a run reads about 3 pages per second: 1,000 matches took 326 seconds in our test run. With match pages off it reads one page per day.

### Limits (stated honestly)

- One source: TennisExplorer. Matches, bookmakers and odds are as it shows them; Home/Away (match winner) odds only, no handicaps, totals or in-play odds.
- No live scores; matches in progress are skipped until they finish.
- Round, surface, full names and rankings need match pages (on by default); rankings are current, not as of the match date.
- UTC days; a date range covers at most 92 days per run; about 3 match pages per second (1,000 matches in 5-8 minutes).

### Works great with

- **Then: [Google Trends by Region](https://apify.com/aurelix/google-trends-scraper)**: put player full names or tournament names into its `keywords` input to compare search interest around a match

### Related Aurelix tools

Public Aurelix Actors, grouped by the data they collect. All are pay-per-result and need no login.

#### Jobs and hiring data

- **[Greenhouse Jobs Scraper](https://apify.com/aurelix/greenhouse-jobs-scraper)**: Live jobs from a company's Greenhouse, Lever, Ashby or SmartRecruiters board, with salary and Canadian province tags.
- **[LinkedIn Jobs Country Filter Scraper](https://apify.com/aurelix/linkedin-jobs)**: Public LinkedIn job postings by keyword and country: title, company, location, seniority, job type, salary text, description.
- **[Multi Job Board Scraper](https://apify.com/aurelix/multi-job-board-scraper)**: LinkedIn, about 50 company career sites, Remote OK and We Work Remotely jobs in one deduplicated list.

#### E-commerce and product data

- **[AliExpress Scraper API](https://apify.com/aurelix/aliexpress-product-search)**: AliExpress search results by keyword: price, discount, rating, units sold, ship-from country and sponsored flag.
- **[Amazon New Releases & Best Sellers](https://apify.com/aurelix/amazon-best-sellers)**: Amazon Best Sellers and New Releases top 100 on 10 marketplaces: rank, ASIN, price, rating, rank change.
- **[Shopify Product Export Scraper](https://apify.com/aurelix/shopify-products-scraper)**: Every public product of any Shopify store with variants, SKUs, prices, barcodes, stock counts and collections.

#### Leads and company data

- **[Funded Companies Hiring](https://apify.com/aurelix/funded-companies-hiring)**: Companies that just raised money (SEC Form D), with their open roles and business contacts, as a new-only monitor.
- **[Google Maps Phone Scraper](https://apify.com/aurelix/google-maps-scraper)**: Local business lists from Google Maps: phones, websites, ratings, hours, coordinates; optional website emails and socials.
- **[Recently Funded Companies API](https://apify.com/aurelix/recently-funded-companies)**: US companies that just filed an SEC Form D: amount raised, industry, state, executives, confirmed website.
- **[Tech Stack API](https://apify.com/aurelix/tech-stack-detector)**: CMS, e-commerce platform, analytics, ad pixels, chat, CDN and email provider for each domain you enter.

#### Ad intelligence

- **[Competitor Watch](https://apify.com/aurelix/competitor-watch)**: One report per competitor with only what is new: Meta and TikTok ads, page changes, Shopify products and prices, jobs.
- **[Facebook Ad Library Search & API](https://apify.com/aurelix/facebook-ad-library-scraper)**: Facebook and Instagram business ads from the Meta Ad Library: copy, creatives, landing pages, dates, optional EU reach.

#### Canada and government data

- **[Canada Building Permits](https://apify.com/aurelix/canada-building-permits-scraper)**: Building permits and development applications from 7 Canadian cities: address, type, status, dates, value, contractor company.

#### AI, content and SEO

- **[Google Trends by Region](https://apify.com/aurelix/google-trends-scraper)**: Google Trends interest over time, by region and rising queries for any number of keywords, plus trending searches.
- **[Google Autocomplete API](https://apify.com/aurelix/google-autocomplete-scraper)**: Expands seed keywords into hundreds of real Google and YouTube autocomplete phrases, tagged by search intent.
- **[PDF to Markdown OCR & Table Extraction](https://apify.com/aurelix/pdf-to-markdown)**: Turns linked PDF and Word files into Markdown per page, with tables as JSON rows, RAG chunks and OCR.
- **[Broken Link Crawler & SEO Audit](https://apify.com/aurelix/seo-audit-crawler)**: Audits a whole site: one row per page with SEO checks and broken links, plus a ranked issues summary.
- **[Website to Markdown API](https://apify.com/aurelix/website-to-markdown)**: Crawls a site and returns one row of clean main-content Markdown per page, with title, headings and links.

### Recent changes

- **0.1**: Faster long ranges: the next day lists are fetched ahead while the current day is processed, within the same polite request rate.

The full history is on the **Changelog** tab.

### Tested daily

An automated health check runs this Actor every day on a fixed input and checks that at least 8 matches come back with `match_id`, `match_url`, `date`, `tournament`, `player1`, `player2` filled in; a failed check alerts the maintainer straight away.

### Your feedback

Missing a field, found a bug or need another filter? Open an issue on the **Issues** tab; we aim to answer within a day. If this Actor saved you time, a short review helps other people find it.

# Changelog

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

# Actor input Schema

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

'results' = finished matches with scores and closing odds for past days; 'upcoming' = matches not started yet with the latest odds. Example: results.

## `daysBack` (type: `integer`):

Results mode: finished matches from today (UTC) and the N days before. Example: 1 = yesterday and today. Ignored when 'From date' is set.

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

Results mode: first day of a date range, YYYY-MM-DD (UTC). Example: 2026-10-01. A range covers at most 92 days per run; run it again for older months.

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

Results mode: last day of the range, YYYY-MM-DD (UTC). Example: 2026-10-31. Empty = today.

## `daysAhead` (type: `integer`):

Upcoming mode: matches scheduled today (UTC) and on the next N days. Example: 1 = today and tomorrow. Draws are usually published 1-2 days ahead.

## `categories` (type: `array`):

Keep only these levels. Example: \['atp', 'wta']. Empty = all.

## `matchType` (type: `string`):

Example: singles. Default: both.

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

Keep matches whose tournament name contains any of these words. Example: \['Paris', 'Wimbledon']. Empty = all.

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

Keep matches with any of these players, matched on the surname (last word). Example: \['Jannik Sinner', 'Swiatek']. Empty = all.

## `onlyWithOdds` (type: `boolean`):

Skip matches no bookmaker priced (common in ITF and UTR events). Example: true.

## `bookmakerOdds` (type: `boolean`):

Open each match page for the round, surface, full names, rankings and the Home/Away odds of every listed bookmaker (opening, closing and time of each, about 10-15 bookmakers). Example: false for a faster list with average odds only.

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

Stop after this many matches. Example: 500. A busy day has 400 to 700 matches across all levels (about 60-100 at ATP/WTA level).

## `incremental` (type: `boolean`):

Return only matches not returned by a previous run with the same mode and filters: newly finished matches in results mode, newly scheduled ones in upcoming mode. Seen matches are skipped and not charged. Schedule it hourly or daily for a feed.

## `slackWebhookUrl` (type: `string`):

Post a short digest (count + top 10) to this Slack incoming webhook after each run that found something. Example: https://hooks.slack.com/services/T000/B000/XXXX

## `webhookUrl` (type: `string`):

POST the same digest as JSON to this URL (Zapier, Make, n8n). Example: https://hooks.zapier.com/hooks/catch/123/abc

## `stateKey` (type: `string`):

Name for the 'new since last run' memory, e.g. 'atp-results'. Leave empty to derive it from the mode and filters.

## Actor input object example

```json
{
  "mode": "results",
  "daysBack": 1,
  "daysAhead": 1,
  "categories": [],
  "matchType": "all",
  "tournaments": [],
  "players": [],
  "onlyWithOdds": false,
  "bookmakerOdds": true,
  "maxResults": 20,
  "incremental": false
}
```

# Actor output Schema

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

All tennis matches found in this run, one item per match, with scores and odds.

# 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",
    "maxResults": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("aurelix/tennis-odds-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",
    "maxResults": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("aurelix/tennis-odds-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",
  "maxResults": 20
}' |
apify call aurelix/tennis-odds-scraper --silent --output-dataset

```

## MCP server setup

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