# Tennis Odds Scraper — Pinnacle, Kambi — $1.50/1K until 11-24 (`architekture/tennis-odds-scraper`) Actor

Tennis odds scraper. Launch price $1.50/1K until 2026-11-24, then $1.50/1K (return price). Pre-match ATP, WTA, Challenger and ITF odds from Pinnacle and Kambi, optional Betfair Exchange back/lay, joined to Flashscore match IDs. Watch mode delivers only moved prices. Pay per result.

- **URL**: https://apify.com/architekture/tennis-odds-scraper.md
- **Developed by:** [architek.earth](https://apify.com/architekture) (community)
- **Categories:** Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 delivered price rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Tennis Odds Scraper — Pinnacle, Kambi, Betfair Prices

**Recorded run: 44 odds rows from 2 books in 11.6 seconds** (2026-10-25, build 0.1.1, run `c5LkVY8t0TD7OlI5Z`). A bounded canary sample, not a full-catalogue benchmark.

architek.earth — an independent data studio. Pay-per-result scrapers with recorded runs, published canaries and stable schemas, built for people and AI agents.

**What's new**

- **2026-10-25** — Betfair Exchange rows move to $3.00 per 1,000 to cover UK residential access; Pinnacle and Kambi stay $1.50. Existing $1.50 Betfair pricing applies during any Apify notice period.
- **2026-10-25** — Kambi's occasional HTTP 410 refusal of one proxy address is now retried on a new address before the book is reported as blocked.
- **2026-10-25** — Launch: $1.50 per 1,000 odds rows; it stays $1.50 after 2026-11-24 (the return price equals the launch price).

Tennis odds from Pinnacle, Kambi (Unibet) and, when you turn it on, Betfair Exchange: give it tours, markets and a time window and get one row per outcome price, joined to the Flashscore match ID that tennis-data-scraper uses.

| Source | Market | From | Standout fields |
|---|---|---|---|
| Pinnacle, Kambi (Unibet SE), Betfair Exchange | ATP, WTA, Challenger, ITF pre-match | $1.50 / 1,000 odds rows | `priceDecimal` + `line`, `matchId` join, `previousPriceDecimal` in watch mode |

### ✅ Why choose this scraper

- **Sharp and soft book in one schema.** Pinnacle (with `limitUsd`, its maximum stake) and Kambi rows share the same fields: `market`, `line`, `outcome`, `priceDecimal`, `priceAmerican`.
- **Exchange back and lay.** With `includeBetfair`, Betfair rows carry `priceDecimal` (best back), `layDecimal`, `backSize` and `laySize` in GBP.
- **Joined to tennis-data-scraper.** `matchId` is the Flashscore match ID; `joinConfidence` says how it was matched (exact or fuzzy) or `none`.
- **Line-move watch mode.** `incrementalMode` delivers and charges only prices that moved since the last delivered price, with `previousPriceDecimal`, `openingPriceDecimal` and `changeType`.
- **Few requests.** Pinnacle's whole tennis board is 3 requests and Kambi's is 1, whatever the number of matches; Betfair adds 2 searches plus 1 request per 10 markets. 20 matches per book gave 328 rows in 11.2 seconds (run `renucu8yIaHVBbYeJ`).

#### Tested head-to-head (2026-10-25)

| | This actor | The field (15 priced incumbents) |
|---|---|---|
| Runs that did not succeed | 0% of 2 recorded canary runs (2026-10-25) | median 0%, range 0–10% (30-day store stats) |
| Rows per run | 44 odds rows per Pinnacle + Kambi canary run; 30 on the watch canary's first run | not published |
| Price | $1.50 / 1,000 odds rows | median $3.00 / 1,000 |

### ⚠️ Worth knowing up front

- Pre-match prices from Pinnacle (guest odds API), Kambi (the Unibet Sweden board) and Betfair Exchange only. Bovada, DraftKings, FanDuel and bet365 are not in this version; Kambi rows are Unibet Sweden's prices, other Kambi operators may differ.
- Betfair refuses datacenter and non-UK traffic (HTTP 403 geo page), so it is read through a UK residential proxy and is off by default. The Betfair surcharge is $3.00 per 1,000 rows (the previous $1.50 applies during any Apify notice period); Pinnacle and Kambi remain $1.50. Anonymous Betfair prices are the delayed public feed (`isDelayed: true`).
- `matchId` is null when the Flashscore day feed does not list the match or two candidates fit; ITF and next-day matches join least often. Kambi lists only main lines; Pinnacle alternate lines need `includeAlternateLines`.
- Watch mode compares your runs, not ticks: a price that moved and moved back between two runs is not delivered, and a moved handicap or total line is a new outcome (`changeType: "new"`). A book that is blocked or unreachable is skipped with a free status row and the run ends `PARTIAL`; the other books are still delivered.
- Invalid input, a book with no prices in your filters (`NO_ODDS`), a block (`BLOCKED`, `GEO_BLOCKED`), a source error, a requested match ID no book prices, the run timeout and your spending limit each end with a free status row (`"recordType": "status"`) that says what happened.

### ⚡ Try the example

This actor has no free preview; the example below delivered 328 odds rows for $0.49 in validation (20 matches per book).

Open the actor's input page, switch to JSON, paste this and start the run:

```json
{
  "books": ["pinnacle", "kambi"],
  "tours": ["ATP", "WTA"],
  "maxMatchesPerBook": 20
}
```

Lower `maxMatchesPerBook` or narrow `markets` to spend less; add `"includeBetfair": true` for exchange prices.

### ⚙️ Input

| Parameter | Type | Default | Description |
|---|---|---|---|
| `books` | array | `["pinnacle", "kambi"]` | Books read on datacenter proxies: `pinnacle`, `kambi`. Empty = Betfair only. |
| `includeBetfair` | boolean | `false` | Also read Betfair Exchange (UK residential proxy); rows are charged as `odds-betfair`. |
| `tours` | array | `["ATP", "WTA", "Challenger"]` | `ATP`, `WTA`, `Challenger`, `ITF`, `Other` (UTR, exhibitions, unlabelled). |
| `markets` | array | all 9 | `match-winner`, `set-1-winner`, `set-handicap`, `total-sets`, `games-handicap`, `games-total`, `set-1-games-handicap`, `set-1-games-total`, `set-betting`. |
| `includeAlternateLines` | boolean | `false` | Pinnacle alternate handicap and total lines (about 3x more rows). |
| `includeDoubles` | boolean | `true` | Keep doubles matches. |
| `matchIds` | array | `[]` | Only these Flashscore match IDs (8 letters or digits, e.g. `6B33aysH`). |
| `playerNames` | array | `[]` | Only matches with one of these players, matched by surname. |
| `hoursAhead` | integer | `48` | Matches starting within this many hours (1 to 168). |
| `includeLive` | boolean | `false` | Also keep prices of matches already in play. |
| `maxMatchesPerBook` | integer | `100` | At most this many matches per book, earliest first (1 to 500). |
| `maxItems` | integer | `5000` | Stop after this many delivered rows (1 to 50,000). |
| `incrementalMode` | boolean | `false` | Watch mode: only prices that moved since the last delivered price for the `stateKey`, plus new outcomes. |
| `stateKey` | string | — | Any name, reused on every run. Required for `incrementalMode`; also resumes a stopped run without re-charging. |
| `feedback` | string | — | Optional note for the maintainers. It is logged and read by a human, and it never changes output. |

#### Input recipes

Paste one into the input's JSON view. The matching example tasks exist but are not public yet (the studio account is at Apify's limit of 100 published tasks), so they are not linked here.

- ATP and WTA, Pinnacle and Kambi: `{"books": ["pinnacle", "kambi"], "tours": ["ATP", "WTA"], "maxMatchesPerBook": 20}`
- Match winner only, every tour incl. ITF: `{"tours": ["ATP", "WTA", "Challenger", "ITF"], "markets": ["match-winner"], "maxMatchesPerBook": 30}`
- Betfair Exchange back and lay: `{"books": [], "includeBetfair": true, "maxMatchesPerBook": 20}`
- Line-move watch (schedule it hourly): `{"incrementalMode": true, "stateKey": "my-line-watch", "maxMatchesPerBook": 20}`
- Pinnacle with alternate lines: `{"books": ["pinnacle"], "includeAlternateLines": true, "maxMatchesPerBook": 10}`
- Challenger and ITF, next 24 hours: `{"tours": ["Challenger", "ITF"], "hoursAhead": 24, "maxMatchesPerBook": 20}`

### 🧱 Stable fields

These top-level fields keep the same name and the same kind of value in every release. Fields are only ever added, never renamed or removed, so an n8n, Make, Zapier or spreadsheet pipeline can bind to them.

| Field | Type | Meaning |
|---|---|---|
| `book` | string | `pinnacle`, `kambi` or `betfair`. |
| `bookEventId` | string | The book's own match ID. |
| `bookMarketId` | string, nullable | The book's market ID (Betfair marketId, Kambi bet offer ID, Pinnacle matchup and market key). |
| `matchId` | string, nullable | Flashscore match ID (tennis-data-scraper's `matchId`); null when not joined. |
| `joinConfidence` | string | `exact` (both players, start within 15 minutes), `fuzzy` (both players, start within 3 hours) or `none`. |
| `tour` | string | `ATP`, `WTA`, `Challenger`, `ITF` or `Other`. |
| `tournament` | string, nullable | The book's tournament label. |
| `isDoubles` | boolean | Doubles match. |
| `homePlayer`, `awayPlayer` | string | The book's player (or pair) names, first-listed first. |
| `startTime` | string | Scheduled start, ISO 8601 UTC. |
| `isLive` | boolean | The match had started when the price was read (only with `includeLive`). |
| `market` | string | One of the 9 market names above. |
| `line` | number, nullable | Handicap or total for this outcome; null on winner markets. |
| `outcome` | string | `home`, `away`, `over`, `under`, or a set score such as `home 2-0`. |
| `outcomeName` | string | The book's label for the outcome. |
| `priceDecimal` | number | Decimal odds (Betfair: best back price). |
| `priceAmerican` | number, nullable | American odds. |
| `layDecimal`, `backSize`, `laySize` | number, nullable | Betfair best lay price and GBP sizes; null for other books. |
| `limitUsd` | number, nullable | Pinnacle's maximum risk stake for the market. |
| `isDelayed` | boolean, nullable | Betfair: anonymous exchange prices are delayed. |
| `openingPriceDecimal`, `previousPriceDecimal` | number, nullable | Watch mode: first price seen and last price delivered for this outcome. |
| `changeType` | string, nullable | Watch mode: `new` or `changed`. |
| `sourceUpdatedAt` | string, nullable | When the book says the price last changed (Kambi). |
| `observedAt` | string | When this run read the price. |
| `recordType` | string | `"status"` on free status rows; `"odds"` on data rows. |

Other fields are extras, not part of the contract. To have one pinned, say so in `feedback`.

### 📦 Output example

Abridged real rows from runs `renucu8yIaHVBbYeJ` (Pinnacle) and `tXsSNOwaSf0NX8odB` (Betfair) on 2026-10-25; values are a historical snapshot.

```json
{
  "recordType": "odds",
  "book": "pinnacle",
  "bookEventId": "1639210922",
  "bookMarketId": "1639210922:s;0;m",
  "matchId": "8YfBpTmn",
  "joinConfidence": "exact",
  "tour": "WTA",
  "tournament": "WTA Tokyo - Qualifiers",
  "homePlayer": "Tereza Valentova",
  "awayPlayer": "Shuai Zhang",
  "startTime": "2026-10-25T06:00:00Z",
  "market": "match-winner",
  "line": null,
  "outcome": "home",
  "priceDecimal": 1.4608,
  "priceAmerican": -217,
  "limitUsd": 1750,
  "observedAt": "2026-10-25T05:35:14.301752+00:00"
}
```

```json
{
  "recordType": "odds",
  "book": "betfair",
  "bookEventId": "36232943",
  "bookMarketId": "1.264429545",
  "matchId": "IoR2Mwe5",
  "joinConfidence": "exact",
  "tour": "Challenger",
  "tournament": "Seoul Challenger 2026",
  "homePlayer": "Christian Langmo",
  "awayPlayer": "Yusuke Takahashi",
  "startTime": "2026-10-25T05:40:00Z",
  "market": "match-winner",
  "outcome": "home",
  "priceDecimal": 1.57,
  "layDecimal": 1.65,
  "backSize": 15.59,
  "laySize": 178.26,
  "isDelayed": true
}
```

A free status row (run `p2eVwC0T1EM7pubmR`, a match ID no book prices) looks like this:

```json
{
  "recordType": "status",
  "inputRef": "ZZZZZZZZ",
  "status": "NO_ODDS",
  "message": "No odds for this Flashscore match ID at the selected books, tours, markets and hours"
}
```

### 🧠 Advanced usage

**Hourly line-move feed.** Schedule the actor hourly with `"incrementalMode": true` and a `stateKey`. The first run delivers every price (`changeType: "new"`); later runs deliver only moved prices with `previousPriceDecimal`. Check `RUN_SUMMARY.status` (`COMPLETE`, `PARTIAL`, `LIMITED`, `BUDGET_LIMIT`) and `RUN_SUMMARY.unchanged` for the prices that did not move.

**Join to match data.** Run tennis-data-scraper for the same day and join on `matchId`; rows with `joinConfidence: "none"` have no Flashscore match.

**Sharp versus soft.** Filter `market == "match-winner"` and compare Pinnacle's `priceDecimal` with Kambi's for the same `matchId`, or with the Betfair back/lay spread.

**Resume after a stop.** A run stopped by the timeout or your spending limit is resumed by the next run with the same `stateKey`: finished books are not read again and outcomes already delivered are skipped.

### 🔌 Integrations

Results land in a dataset you can export as JSON, CSV or Excel. They can also flow to Google Sheets, Make, Zapier or n8n, or to a webhook when a run finishes. The Apify Scheduler runs it on a timetable. An hourly schedule with watch mode turns it into a line-move feed. Copy-paste code examples for this actor are not published yet; the Python snippet under "For AI agents" works as is.

#### n8n and Make

The architek-earth/examples repository has no n8n workflow or Make blueprint for this actor yet; use Apify's own n8n and Make modules with the actor ID `architekture/tennis-odds-scraper`.

### 💸 Pricing

Pay per result. Platform and proxy usage are included. Status rows and failed requests are free. A stopped run resumed with the same `stateKey` skips the outcomes it already delivered, so they are not charged again; in watch mode unchanged prices are never charged.

| Event | Price | What it covers |
|---|---|---|
| `odds` | $1.50 / 1,000 odds rows | One delivered Pinnacle or Kambi price row. |
| `odds-betfair` | $3.00 / 1,000 odds rows after the pricing change; $1.50 during notice | One delivered Betfair Exchange price row (UK residential proxy), only with `includeBetfair`. |

1,000 Pinnacle or Kambi rows cost $1.50; 1,000 Betfair rows cost $3.00 after the surcharge takes effect. Apify shows the effective pricing and any notice period before you run. The Apify free plan's $5 monthly credit covers about 3,333 ordinary odds rows or 1,666 Betfair rows at the surcharge price.

### ❓ FAQ

**Is this live (in-play) odds?** No by default. `includeLive` keeps prices of matches in play, read at run time; there is no streaming.

**How often can I run it?** As often as you schedule it; Pinnacle and Kambi are a handful of requests per run. Hourly watch mode is the intended use.

**Why is `matchId` null on some rows?** The Flashscore feed did not list the match, or more than one match fitted the players and start time. The row is still delivered with `joinConfidence: "none"`.

**Do I need a Betfair account?** No. It reads Betfair's public read-only exchange feed, which is delayed for anonymous visitors.

**Can I get Bovada, DraftKings or bet365?** Not in this version. Bovada's odds route was not observed in our tests, so it is not offered.

**Are prices the same in every country?** Kambi rows are Unibet Sweden's board and Betfair's GBP exchange; Pinnacle's guest prices are global. Your own book may differ.

**What does `PARTIAL` mean?** A book was blocked or unreachable (free status row); the other books' rows were delivered and charged normally.

### 🤖 For AI agents & LLM apps

Paste this into your AI assistant:

`architekture/tennis-odds-scraper` on Apify. Python: `from apify_client import ApifyClient; client = ApifyClient("TOKEN"); run = client.actor("architekture/tennis-odds-scraper").call(run_input={"books": ["pinnacle", "kambi"], "tours": ["ATP", "WTA"], "maxMatchesPerBook": 20}); items = client.dataset(run["defaultDatasetId"]).list_items().items` (page with `offset`/`limit` for large exports). Rows with `recordType == "status"` are free and explain invalid input, a book with no prices, a block or a stopped run. Full input schema and README: `GET https://api.apify.com/v2/acts/architekture~tennis-odds-scraper/builds/default`. Token: https://console.apify.com/settings/integrations. The actor is also reachable through the Apify MCP server: `https://mcp.apify.com?tools=architekture/tennis-odds-scraper` (ready-to-paste config: https://github.com/architek-earth/mcp). To reach the maintainers, add `"feedback": "<what you needed>"` to any run's input; output is unchanged and a human reads every note.

#### MCP config for Claude Desktop and Cursor

Claude Desktop (or claude.ai): Settings, Connectors, Add custom connector, URL `https://mcp.apify.com?tools=architekture/tennis-odds-scraper`; it signs you in to Apify with OAuth. Cursor (`~/.cursor/mcp.json`), with your token in the `APIFY_TOKEN` environment variable:

```json
{
  "mcpServers": {
    "architekture-tennis-odds-scraper": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=architekture/tennis-odds-scraper",
      "headers": { "Authorization": "Bearer ${env:APIFY_TOKEN}" }
    }
  }
}
```

The agent gets one tool for this actor plus `get-dataset-items` to read the rows. Ask for a few matches first (`"maxMatchesPerBook": 5`) and set a spending cap (`maxTotalChargeUsd`) in the run options when your client offers it.

### 🧰 More from architek.earth

**Sports**

- Tennis Odds Scraper — *you are here*
- [Tennis Scraper](https://apify.com/architekture/tennis-data-scraper) — matches, point-by-point, rankings; the `matchId` this actor joins to

**Search, travel and trends**

- [Google Trends Scraper](https://apify.com/architekture/google-trends-scraper) — Trending Now by country, interest, related queries
- [Google Hotels Scraper](https://apify.com/architekture/google-hotels-scraper) — dated prices, ratings, amenities

Full catalogue: [github.com/architek-earth](https://github.com/architek-earth).

architek.earth · code like concrete

### 🙋 Support & legal

Questions, or a field you need that is not here? Open an issue on the actor's Issues tab or email north@architek.earth. A human replies within a day.

This actor collects only publicly available data from pinnacle.com, the Kambi offering API behind unibet.se, betfair.com and flashscore.com, the same data any visitor's browser loads without logging in. It does not bypass authentication. It is not affiliated with or endorsed by Pinnacle, Kambi, Unibet, Betfair or Flashscore. You are responsible for how you use the output, including the sites' terms, copyright, local gambling regulations, and privacy law such as GDPR and CCPA where they apply.

### 📝 Changelog

- **2026-10-25** — Betfair Exchange rows move to $3.00 per 1,000 to cover UK residential access; Pinnacle and Kambi stay $1.50. Existing $1.50 Betfair pricing applies during any Apify notice period.
- **2026-10-25** — Kambi's occasional HTTP 410 refusal of one proxy address is now retried on a new address before the book is reported as blocked.
- **2026-10-25** — Launch: $1.50 per 1,000 odds rows; it stays $1.50 after 2026-11-24 (the return price equals the launch price).

# Actor input Schema

## `books` (type: `array`):

Sportsbooks read on datacenter proxies: pinnacle (moneylines, set and games handicaps and totals, set 1 lines) and kambi (Unibet Sweden: match odds, games handicap, total games). Leave empty to read only Betfair.

## `includeBetfair` (type: `boolean`):

Also read Betfair Exchange best back and lay prices with available size (match odds, set betting, set 1 winner). Betfair refuses datacenter and non-UK traffic, so this book alone runs through a UK residential proxy and its rows are charged as odds-betfair. Anonymous exchange prices are a delayed feed.

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

Which tours to keep, from the book's tournament label (or the joined Flashscore tournament). Other = exhibitions, UTR and anything unlabelled.

## `markets` (type: `array`):

Markets to keep. Not every book prices every market: Pinnacle has all but set-betting, Kambi has match-winner, games-handicap and games-total, Betfair has match-winner, set-1-winner and set-betting.

## `includeAlternateLines` (type: `boolean`):

Also deliver Pinnacle's alternate handicap and total lines (about 3x more rows). Off = main lines only.

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

Keep doubles matches (players are pairs, e.g. "Arends S./Johnson L.").

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

Optional: only these matches, by Flashscore match ID (the matchId of tennis-data-scraper, e.g. 6B33aysH). A match no selected book prices gets a free NO_ODDS status row.

## `playerNames` (type: `array`):

Optional: only matches with one of these players, matched by surname (e.g. "Jannik Sinner" or "Sinner").

## `hoursAhead` (type: `integer`):

Keep matches starting within this many hours (1 to 168).

## `includeLive` (type: `boolean`):

Also keep prices of matches already in play (isLive true). Off = pre-match only.

## `maxMatchesPerBook` (type: `integer`):

At most this many matches per book, earliest start first (1 to 500).

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

Stop after delivering this many price rows (1 to 50,000).

## `incrementalMode` (type: `boolean`):

Deliver and charge only prices that moved since the last delivered price for the same stateKey, plus outcomes not seen before. Unchanged prices are free and not written. Needs a stateKey.

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

Any name you choose, reused on every run of the same inputs. Required for incrementalMode; also lets a stopped run resume without re-charging.

## `feedback` (type: `string`):

Optional note for the maintainers. It is logged and read by a human, and it never changes output.

## Actor input object example

```json
{
  "books": [
    "pinnacle",
    "kambi"
  ],
  "includeBetfair": false,
  "tours": [
    "ATP",
    "WTA",
    "Challenger"
  ],
  "markets": [
    "match-winner",
    "set-1-winner",
    "set-handicap",
    "total-sets",
    "games-handicap",
    "games-total",
    "set-1-games-handicap",
    "set-1-games-total",
    "set-betting"
  ],
  "includeAlternateLines": false,
  "includeDoubles": true,
  "matchIds": [],
  "playerNames": [],
  "hoursAhead": 48,
  "includeLive": false,
  "maxMatchesPerBook": 10,
  "maxItems": 5000,
  "incrementalMode": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `summary` (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 = {
    "books": [
        "pinnacle",
        "kambi"
    ],
    "maxMatchesPerBook": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("architekture/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 = {
    "books": [
        "pinnacle",
        "kambi",
    ],
    "maxMatchesPerBook": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("architekture/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 '{
  "books": [
    "pinnacle",
    "kambi"
  ],
  "maxMatchesPerBook": 10
}' |
apify call architekture/tennis-odds-scraper --silent --output-dataset

```

## MCP server setup

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