# Tennis Point-by-Point & Odds Timeline (`humin93/flashscore-tennis-pbp`) Actor

Point-level tennis match data (every point, server, break/set/match points) plus timestamped bookmaker odds snapshots for ATP, WTA, Challenger and ITF matches. Data tool for analytics and research - not a results scraper.

- **URL**: https://apify.com/humin93/flashscore-tennis-pbp.md
- **Developed by:** [hyeonsu kim](https://apify.com/humin93) (community)
- **Categories:**
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 match point-by-points

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?

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

## Tennis Point-by-Point & Odds Timeline

A **Flashscore scraper** for **tennis point by point data** and **tennis odds history**: every single point of a match, who served it, whether it was a break/set/match point — plus timestamped bookmaker odds snapshots that scheduled runs turn into an odds movement timeline. Covers **ATP, WTA data**, Challenger and ITF matches, delivered as clean JSON you can treat as a **tennis analytics API**: run it on demand or on a schedule and read the dataset from your code.

Most tennis scrapers stop at schedules, final scores and H2H tables. This actor collects what those don't:

1. **Point-by-point progression** — the full set → game → point sequence: server of every game, the exact score path (`0:15, 0:30, 30:30, 40:30, ...`), break/set/match points flagged per point, and tiebreaks expanded point-by-point with mini-break markers.
2. **Odds snapshots** — every listed bookmaker's pre-match odds (current + opening value) for moneyline, totals, handicap, correct-score and odd/even markets, stamped with the collection time.

This is a **data tool** for analytics, modeling, research and journalism. It reports publicly displayed sports records and publicly displayed odds numbers — it does not place bets, link to betting sites, or provide betting advice.

### Sample output

One dataset record per match. This is a real (shortened) record — Comesana vs. Cobolli, US Open first round:

```json
{
  "matchId": "xYnEsNM6",
  "mode": "both",
  "capturedAt": "2026-09-02T13:54:49.778Z",
  "tournament": { "name": "US Open, hard", "category": "ATP - SINGLES", "tour": "atp", "country": "USA" },
  "round": "1/64-finals",
  "status": "finished",
  "startTimestamp": 1788275400,
  "home": { "name": "Francisco Comesana", "country": "Argentina", "ranking": { "tour": "ATP", "position": 119 } },
  "away": { "name": "Flavio Cobolli", "country": "Italy", "ranking": { "tour": "ATP", "position": 6 } },

  "pointByPoint": {
    "totalGames": 46, "totalPoints": 271, "breaks": 12,
    "sets": [
      {
        "set": 1,
        "games": [
          {
            "gameNumber": 1, "server": "home", "winner": "home",
            "isBreak": false, "scoreAfter": { "home": 1, "away": 0 },
            "breakPointCount": 4,
            "points": [
              { "home": "0",  "away": "40", "breakPoint": true,  "setPoint": false, "matchPoint": false },
              { "home": "40", "away": "A",  "breakPoint": true,  "setPoint": false, "matchPoint": false }
            ]
          }
        ],
        "tiebreak": {
          "finalScore": { "home": 7, "away": 9 },
          "points": [
            { "pointNumber": 11, "scoreAfter": { "home": 5, "away": 6 }, "server": "away",
              "winner": "away", "isMiniBreak": false, "setPoint": true, "markerFor": "away" }
          ]
        }
      }
    ]
  },
  "pbpAvailable": true,

  "odds": {
    "capturedAt": "2026-09-02T13:54:51.892Z",
    "bookmakerCount": 4, "marketCount": 34,
    "markets": [
      {
        "bookmaker": "DraftKings", "betType": "HOME_AWAY", "betScope": "FULL_TIME",
        "outcomes": [
          { "side": "home", "odds": 4.27, "openingOdds": 4.33, "active": true },
          { "side": "away", "odds": 1.21, "openingOdds": 1.21, "active": true }
        ]
      }
    ]
  },
  "oddsAvailable": true
}
```

(Arrays shortened here; real records contain every set, every game, every point, and all ~30+ markets across bookmakers: `HOME_AWAY`, `OVER_UNDER`, `ASIAN_HANDICAP`, `CORRECT_SCORE`, `ODD_OR_EVEN`, each also per `FIRST_SET`/`SECOND_SET` scope where offered.)

#### Field guide

| Field | Meaning |
|---|---|
| `matchId` | Flashscore's 8-character match ID — the stable join key across repeated runs |
| `capturedAt` | When this record was collected (ISO 8601) — the timeline axis for scheduled runs |
| `home` / `away` | Player name, country, and current ATP/WTA ranking position |
| `pointByPoint.sets[].games[]` | Per game: server, winner, break flag, score after, and the point-by-point array |
| `points[].home/away` | The score *after* that point, from the server's perspective conventions of the scoreboard |
| `points[].breakPoint/setPoint/matchPoint` | Pressure-point flags on the individual point |
| `sets[].tiebreak` | Tiebreak expanded point-by-point with server, mini-break and set/match-point markers |
| `pbpAvailable` | `false` when the source has no point feed for the match (section is `null`, not charged) |
| `odds.markets[]` | One entry per bookmaker × bet type × scope, with current `odds` and `openingOdds` per outcome |
| `oddsAvailable` | `false` when no bookmaker odds are listed (section is `null`, not charged) |

### Modes

| Mode | What it collects | Typical use |
|---|---|---|
| `point_by_point` | Full point sequences | Post-match analytics, serve/pressure modeling |
| `odds_snapshot` | All bookmakers' current + opening odds, timestamped | Odds movement timelines via scheduled runs |
| `both` (default) | Both sections in one record | One-stop match archive |

### Targeting matches

- **Explicit match IDs** — pass Flashscore match IDs (the 8-character ID in match URLs) in `matchIds`. Metadata (round, rankings, full names) is read from each match page.
- **Auto-discovery by day** — leave `matchIds` empty and set `dayOffset` (0 = today, -1 = yesterday, range ±7). Filter by tour (`atp`, `wta`, `challenger`, `itf`, `other`), singles/doubles, and status (`finished`, `live`, `scheduled`, `all`).

### Recipe: building an odds timeline

Odds pages expose only the *current* and *opening* value of each market — the path between them is not published anywhere. But if you run this actor on a schedule, each run stamps a fresh snapshot, and the snapshots line up into a proper time series per match. This is the actor's core recurring use case.

**Step 1 — create the task input.** Snapshot tomorrow's schedule (auto-discovery):

```json
{
  "mode": "odds_snapshot",
  "dayOffset": 1,
  "statusFilter": "scheduled",
  "tours": ["atp", "wta"],
  "includeDoubles": false,
  "maxMatches": 50
}
```

Or pin specific matches you want a dense timeline for (e.g. a final):

```json
{
  "mode": "odds_snapshot",
  "matchIds": ["xYnEsNM6", "AbCdEf12"]
}
```

**Step 2 — schedule it.** In Apify Console: **Schedules → Create new**, attach this actor (or a saved Task with the input above), and set a cron like `0 */4 * * *` (every 4 hours). Every 2–6 hours is a sensible cadence for pre-match markets; the `dayOffset: 1` window means each match is captured repeatedly from roughly a day out until it starts.

**Step 3 — join the snapshots.** All runs can push to the same named dataset (set one on the Task), or you read runs' default datasets via the Apify API. Group records by `matchId`, sort by `odds.capturedAt`, and each market's `odds` values form the movement curve — with `openingOdds` as the anchor point at the start.

Each scheduled run charges only per match that actually returns an odds snapshot (see pricing), so an off-day with no matching matches costs almost nothing.

### Pricing (pay-per-event)

You are charged only for data actually delivered:

| Event | Charged when |
|---|---|
| `match-pbp` | Per match with point-by-point data returned |
| `match-odds` | Per match with an odds snapshot returned |
| `actor-start` | Once per run (very small flat fee) |

Matches without point-by-point coverage or without bookmaker odds are pushed with that section set to `null` and are **not charged** for that section. No data, no charge.

### FAQ

**Is this legal?** The actor reads only publicly displayed pages — sports records (scores, point sequences, rankings) and publicly displayed odds numbers, the same data any visitor sees without logging in. Player names are public match participants; no personal data beyond that is collected. If the source ever answers with bot-protection responses, the run stops and reports instead of attempting any bypass.

**Am I charged when a match has no data?** No. `match-pbp` and `match-odds` are charged only when that section is actually delivered. A match with `pbpAvailable: false` or `oddsAvailable: false` costs nothing for the missing section, and a failed run charges nothing beyond the tiny `actor-start` fee.

**Is this a betting tool?** No. It is a data collection tool for research, modeling and journalism. It does not place bets, does not link to betting operators, and provides no betting advice. Odds are reported as published numbers, like any other public sports statistic.

**Where do I get help?** Open an issue on the actor's **Issues tab** in Apify Console — include the `matchId` and your input if it's about a specific match. Feature requests are welcome.

### Limitations

- Point-by-point coverage depends on the source; lower-tier ITF and some doubles matches may have no PBP feed (delivered as `pbpAvailable: false`, not charged).
- Odds are pre-match markets; the bookmaker portfolio varies by the configured region (`geoIpCode`, default US).
- Historical odds before the actor's first snapshot of a match cannot be reconstructed — start the schedule before the matches you care about.

### Input reference

See the Input tab. Key fields: `mode`, `matchIds`, `dayOffset`, `tours`, `includeDoubles`, `statusFilter`, `maxMatches`, plus politeness controls (`requestDelayMs`, default 600 ms with jitter; `maxRetries`) and odds region (`geoIpCode`, `geoIpSubdivisionCode`).

Not affiliated with Flashscore or any bookmaker.

# Actor input Schema

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

What to collect per match. <b>point\_by\_point</b>: full set/game/point sequences (server, score progression, break/set/match points). <b>odds\_snapshot</b>: timestamped snapshot of all bookmakers' pre-match odds (current + opening values) - schedule repeated runs to build an odds movement timeline. <b>both</b>: one record per match with both sections.

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

Specific Flashscore match IDs (the 8-character code in match URLs, e.g. <code>.../match/tennis/.../?mid=xYnEsNM6</code> → <code>xYnEsNM6</code>). When provided, day discovery is skipped and metadata (tournament, round, rankings) is read from each match page.

## `dayOffset` (type: `integer`):

Used when no match IDs are given: collect matches from this day. <code>0</code> = today, <code>-1</code> = yesterday, <code>1</code> = tomorrow. Range -7 to 7.

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

Which tennis tours to include when auto-discovering matches.

## `includeDoubles` (type: `boolean`):

Include doubles matches in auto-discovery (point-by-point coverage is rarer for doubles).

## `statusFilter` (type: `string`):

Only process matches in this state. <b>finished</b> gives complete point-by-point data; <b>live</b>/<b>scheduled</b> are most useful for odds snapshots.

## `maxMatches` (type: `integer`):

Maximum number of matches to process in this run.

## `geoIpCode` (type: `string`):

The odds endpoint requires a region and returns that region's bookmaker portfolio (this mirrors what the website sends). Default <code>US</code>.

## `geoIpSubdivisionCode` (type: `string`):

Region subdivision code accompanying the country code, e.g. <code>USNY</code> for US / New York.

## `requestDelayMs` (type: `integer`):

Polite delay between consecutive HTTP requests (plus random jitter). Keep at 500ms or higher.

## `maxRetries` (type: `integer`):

Retries with exponential backoff on 429/5xx/network errors. 403 responses are never retried or bypassed.

## Actor input object example

```json
{
  "mode": "both",
  "matchIds": [],
  "dayOffset": 0,
  "tours": [
    "atp",
    "wta",
    "challenger"
  ],
  "includeDoubles": false,
  "statusFilter": "all",
  "maxMatches": 20,
  "geoIpCode": "US",
  "geoIpSubdivisionCode": "USNY",
  "requestDelayMs": 600,
  "maxRetries": 3
}
```

# Actor output Schema

## `matches` (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 = {
    "matchIds": [],
    "tours": [
        "atp",
        "wta",
        "challenger"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("humin93/flashscore-tennis-pbp").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 = {
    "matchIds": [],
    "tours": [
        "atp",
        "wta",
        "challenger",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("humin93/flashscore-tennis-pbp").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 '{
  "matchIds": [],
  "tours": [
    "atp",
    "wta",
    "challenger"
  ]
}' |
apify call humin93/flashscore-tennis-pbp --silent --output-dataset

```

## MCP server setup

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

```

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/ZopLjI6m7U8cqzfbr/builds/VmltrWEHSXxiZi5GR/openapi.json
