Tennis Odds Scraper — Pinnacle, Kambi — $1.50/1K until 11-24
Pricing
from $1.50 / 1,000 delivered price rows
Tennis Odds Scraper — Pinnacle, Kambi — $1.50/1K until 11-24
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.
Pricing
from $1.50 / 1,000 delivered price rows
Rating
0.0
(0)
Developer
architek.earth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
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 carrypriceDecimal(best back),layDecimal,backSizeandlaySizein GBP. - Joined to tennis-data-scraper.
matchIdis the Flashscore match ID;joinConfidencesays how it was matched (exact or fuzzy) ornone. - Line-move watch mode.
incrementalModedelivers and charges only prices that moved since the last delivered price, withpreviousPriceDecimal,openingPriceDecimalandchangeType. - 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). matchIdis 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 needincludeAlternateLines.- 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 endsPARTIAL; 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:
{"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.
{"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"}
{"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:
{"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:
{"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 — matches, point-by-point, rankings; the
matchIdthis actor joins to
Search, travel and trends
- Google Trends Scraper — Trending Now by country, interest, related queries
- Google Hotels Scraper — dated prices, ratings, amenities
Full catalogue: 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).