# Tennis Scraper: Odds History & Player Results (`tagadanar/tennis-history-odds`) Actor

Export tennis player-season results and bookmaker opening and last quoted match-winner odds as flat rows. ATP and WTA singles. $1 per 1,000 rows + $0.001/run, usage included. Paste TennisExplorer players or match links.

- **URL**: https://apify.com/tagadanar/tennis-history-odds.md
- **Developed by:** [Tagada Data](https://apify.com/tagadanar) (community)
- **Categories:** Automation, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 match result or bookmaker odds rows

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: Odds History & Player Results

Export a tennis player's season results and each bookmaker's opening and last quoted match-winner odds as flat rows. Build a player-history dataset, compare bookmakers or prepare a backtest without copying tables by hand.

This actor reads public TennisExplorer pages. It covers **singles season results and the Home/Away market**. Odds history means the opening and last published quote, where available. It does not export every intermediate price change.

**$1 per 1,000 delivered rows + $0.001 per run**, with usage included. No login or API key for the source.

#### Start with a player and season

```json
{
  "players": ["djokovic"],
  "seasons": ["2024"],
  "resultType": "both",
  "maxResults": 20
}
```

The prefilled example uses an archived season so it also works when no tennis is live. A `result` row contains a match; an `odds` row contains one bookmaker's prices for both players in that match. With `both`, a match can therefore produce several billed rows. `maxResults` is the combined row cap.

For season results without bookmaker detail pages:

```json
{
  "players": ["djokovic", "swiatek"],
  "seasons": ["2023", "2024"],
  "resultType": "results",
  "maxResults": 200
}
```

For a specific match and bookmaker:

```json
{
  "matchIds": ["https://www.tennisexplorer.com/match-detail/?id=3337719"],
  "resultType": "odds",
  "bookmakers": ["bet365"],
  "maxResults": 10
}
```

#### Inputs

| Field | What it changes |
| --- | --- |
| `players` | TennisExplorer player slugs or `/player/` links. Get the slug from the player's page. Optional when `matchIds` is supplied. |
| `seasons` | Years for each player. Omitted means the current UTC year. The form example selects 2024. URL query parameters do not override this field. |
| `matchIds` | Numeric TennisExplorer IDs or `/match-detail/?id=` links, instead of or alongside players. IDs from other tennis sites do not work. |
| `resultType` | `results`, `odds` or `both` (default). |
| `bookmakers` | Optional exact names, case insensitive. Applies to odds rows. Leave empty for all available bookmakers. |
| `maxResults` | Combined dataset row cap, default 100 through the API and 20 in the form example. Maximum 5,000. |

Limits per run: 20 players, 10 seasons, 50 player-season pages and 200 unique match detail pages. If a limit or the run clock cuts collection short, the status says what stopped and the run summary identifies skipped sources. Split larger archives into more runs. Season tables contain all available results on one page; a page number does not unlock more results.

An odds-only run with a bookmaker filter stops after 10 consecutive matches with no matching bookmaker rows. `NO_YIELD_LIMIT` names that stop; it does not claim that later matches have no prices.

#### Run time

The launch test collecting 100 Pinnacle rows took about **3 minutes 35 seconds** at the default 512 MiB. It read two season pages and 103 match pages. Requests run one at a time with no fixed delay after a successful response. Reading and parsing each match page dominates this narrow-bookmaker case; a 100-row cap can require many fewer matches when all bookmakers are included. Source response time and the selected history affect duration.

Results-only collection is much shorter in these checks: 250 Djokovic and Swiatek result rows from four season pages took **3.7 seconds**. Use `results` when you need season outcomes without bookmaker detail rows.

#### Output

All fields are scalar values, ready for CSV, Excel or JSON export. `matchId` joins result and odds rows. Repeated matches and bookmaker rows are deduplicated within a run. When requested players share a match, the result is delivered once; `requestedPlayer` identifies the first player source that supplied it.

| Row type | Fields |
| --- | --- |
| `result` | Match ID and URL, requested player and season, players and slugs, tournament, round, surface, source date text, score, and the season table's unattributed H/A odds when present. |
| `odds` | Match ID and URL, players, tournament, score, bookmaker, market, both last quoted prices, both opening prices and their published time labels. |

An excerpt from the public match used in the example:

```json
{
  "type": "odds",
  "matchId": "3337719",
  "bookmaker": "bet365",
  "market": "Home/Away",
  "homePlayer": "Djokovic Novak",
  "awayPlayer": "Yunchaokete Bu",
  "homeOpeningOdds": "1.33",
  "awayOpeningOdds": "3.40",
  "homeOdds": "1.25",
  "awayOdds": "4.00",
  "homeOpeningTimeRaw": "30.09. 15:19",
  "homeLastChangeTimeRaw": "02.10. 09:39"
}
```

Prices remain strings in the source's format. Missing opening prices and times remain `null`; the current price is never invented as the opening price. `homeListedOdds` and `awayListedOdds` on a season result are the table's H/A columns, with no bookmaker attribution.

`homeDeactivated` and `awayDeactivated` preserve the source's disabled-quote flags. Those historical prices remain in the export and are not represented as available offers.

`matchDateRaw` and quote times preserve the site's labels. Quote labels omit a year and timezone, so they are not converted into timestamps. A season can include matches played in December of the preceding year. Player names and score order follow the source, and superscript tiebreak points appear in parentheses. Do not infer the winner from row order. Detail-page names can be longer than season-table names.

#### Pricing

| Apify plan | Per 1,000 delivered rows |
| --- | ---: |
| Free | $1.00 |
| Bronze | $0.90 |
| Silver | $0.80 |
| Gold | $0.70 |

The `result` event charges once per delivered result or bookmaker row. An odds row includes both players' prices. The `actor-start` event charges **$0.001 once per run**, including invalid, unavailable and zero-result runs. Usage is included; there is no extra detail surcharge. Skipped and unavailable records have no result charge.

At the Free rate, 20 rows cost $0.021 including the start, and 1,000 rows cost $1.001. Set `maxResults` and the run's charge limit to control spend. Different actors bill different units: compare a bookmaker row with a bookmaker row, rather than a complete nested match record.

#### Coverage and run status

- Historical coverage depends on TennisExplorer. Singles may include qualifying, lower-level and exhibition matches that appear on the player's season page. Doubles, live-score polling, ranking history, point-by-point data, other odds markets and full odds movement are outside this version.
- **Last quoted is not guaranteed pre-match closing odds.** A published quote can be stale or recorded after a match started. This actor does not label it as an independently verified closing line.
- Bookmaker availability differs by match and year. No result is padded with a fabricated bookmaker or price.
- All-invalid input fails `BAD_INPUT`. Missing players and matches fail `NOT_FOUND` when nothing is delivered. An unavailable season without a result container fails with `SEASON_UNRESOLVED` in the status detail. A bookmaker filter matching no delivered rows fails `FILTERED_OUT`.
- An unrecognized page, refusal, unresolved request or exhausted clock with nothing delivered fails by name. A successful zero run requires the origin's explicit no-results proof and charges only the start event. Real empty archive cases have not been platform-verified at launch.
- A partial run can succeed with delivered rows. Check its status and the `RUN_SUMMARY` record for caps, failures and skipped sources. Repeated runs return fresh snapshots; this version has no persistent only-new mode.

Use the Dataset tab to export rows or retrieve them with the Apify API. Schedule repeated snapshots when you want to compare changes over time. Public odds are source observations, not betting advice or a guarantee of an executable price.

For daily live/finished/scheduled scores and set statistics, use [Tennis Live Scores](https://apify.com/tagadanar/tennis-live-scores). This actor serves player-season research and bookmaker-price comparisons.

Independent tool, not affiliated with TennisExplorer, its data providers or bookmakers. For support, open an issue with the run ID and input, without credentials.

# Changelog

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

# Actor input Schema

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

TennisExplorer player slugs or /player/ URLs, e.g. djokovic. Singles only. Optional if Match IDs are supplied. URL query parameters are replaced by Seasons.

## `seasons` (type: `array`):

Years to collect for each player, e.g. 2023 and 2024. If omitted, the current UTC year. Up to 10 years and 50 player-season pages per run.

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

TennisExplorer numeric match IDs or /match-detail/?id= URLs. Optional alternative to Players; can be combined. Up to 200 detail pages across the run.

## `resultType` (type: `string`):

Results: one row per singles match. Odds: one row per bookmaker for Home/Away, with both players opening and last quoted odds. Both: these two row types. No intermediate price changes or other markets.

## `bookmakers` (type: `array`):

Exact bookmaker names, case insensitive, e.g. Pinnacle or bet365. Leave empty for all. Applies only to odds rows. A filter matching no rows fails FILTERED_OUT.

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

Total dataset row cap across all sources and both row types. Each delivered row costs the same. Raising this does not bypass the 200-detail-page safety cap.

## Actor input object example

```json
{
  "players": [
    "djokovic"
  ],
  "seasons": [
    "2024"
  ],
  "resultType": "both",
  "maxResults": 20
}
```

# Actor output Schema

## `dataset` (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 = {
    "players": [
        "djokovic"
    ],
    "seasons": [
        "2024"
    ],
    "maxResults": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("tagadanar/tennis-history-odds").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": ["djokovic"],
    "seasons": ["2024"],
    "maxResults": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("tagadanar/tennis-history-odds").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": [
    "djokovic"
  ],
  "seasons": [
    "2024"
  ],
  "maxResults": 20
}' |
apify call tagadanar/tennis-history-odds --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tagadanar/tennis-history-odds"
        }
    }
}
```

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/VgQ35s0VSBAa7C2e2/builds/Y0jXIYzHb8uEPUbfb/openapi.json
