Tennis Intelligence — Match Previews, Odds & Player Analytics avatar

Tennis Intelligence — Match Previews, Odds & Player Analytics

Pricing

Pay per usage

Go to Apify Store
Tennis Intelligence — Match Previews, Odds & Player Analytics

Tennis Intelligence — Match Previews, Odds & Player Analytics

Tennis betting & analytics data from SofaScore and Tennis Abstract: match previews that combine pre-match odds with player form, head-to-head and surface splits, plus live matches, schedules, player match history and rankings. No login or API key required.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

Sergei A.

Sergei A.

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

26 minutes ago

Last modified

Categories

Share

Tennis betting and analytics data in one record: pre-match odds, player form, head-to-head and surface splits for each match — plus live matches, schedules and full player match history. Start from match IDs, a tournament, a date or a player; no login or API key is required. Sources: SofaScore (matches, live, odds) and Tennis Abstract (form, Elo, splits).

What this Actor covers — and what it does not

  • Odds and match intelligence in a single record. Every match preview combines the SofaScore pre-match odds feed (moneyline, set winner, totals, with fractional and decimal values and last movement) with Tennis Abstract analytics for both players: recent form, surface win rates and the head-to-head record. One match ID in, one decision-ready record out.
  • The only multi-signal tennis preview on the Store. Incumbent tennis scrapers sell either SofaScore live/odds data or Tennis Abstract history alone. No current actor merges the two sources into a single per-match preview, which is the format betting and modeling teams actually consume.
  • Join-ready identifiers. Every record carries the SofaScore match ID, tournament and unique-tournament IDs, player IDs and current rankings, plus each player's Tennis Abstract slug and Elo, so previews join cleanly with external odds and rankings feeds.
  • Four modes under one input. matchPreview for per-match previews, liveMatches for the current live slate, schedule for upcoming matches on a date, and playerHistory for a player's recent results with dominance ratio, ace rate and double-fault rate per match.
  • Politeness and reliability built in. Requests rotate browser headers, respect a sliding-window rate limit and a per-run delay, and retry with backoff; blocked pages are re-fetched through the Apify proxy pool with browser TLS impersonation, and missing pages are logged and skipped instead of killing the run.
  • Public pages only. No account, cookies or API keys for either source. Unavailable values stay null rather than being invented.

Who it's for

  • Sports betting analysts and odds engineers — pull per-match previews that combine pre-match odds with form and head-to-head context for pricing and model features.
  • Tennis stat and modeling teams — collect player form, surface splits, Elo and head-to-head records with stable SofaScore and Tennis Abstract identifiers.
  • LLM and dataset builders — source structured tennis match and player-history pages for betting-knowledge and ranking datasets.

Quick start

One exact match preview

{
"mode": "matchPreview",
"matchIds": [
16737476
],
"maxMatches": 1
}

Preview the next matches of a tournament

{
"mode": "matchPreview",
"tournamentId": 194944,
"maxMatches": 10
}

All live tennis matches right now

{
"mode": "liveMatches",
"maxMatches": 20
}

Upcoming matches on a date

{
"mode": "schedule",
"date": "2026-08-16",
"maxMatches": 50
}

One player's full match history

{
"mode": "playerHistory",
"player": "NovakDjokovic",
"maxMatches": 30
}

Output

The dataset contains one matchPreview record per match ID (the core product: odds + form + head-to-head), liveMatch records for currently running matches, scheduledMatch records for upcoming matches on a date, and playerHistory records with a player's full match history. recordType identifies the shape.

fieldtypemeaning
recordTypestringRecord discriminator: matchPreview, liveMatch, scheduledMatch or playerHistory.
scrapedAtstringISO-8601 timestamp of when the record was collected.
matchIdintegerSofaScore event ID.
startTimestringISO-8601 start time of the match.
statusstringMatch status: notstarted, inprogress, finished, etc.
tournamentobjectTournament metadata: name, category (ATP/WTA/Challenger/ITF), tournament and unique tournament IDs.
surfacestringCourt surface, e.g. Hardcourt outdoor, Red clay, Grass.
roundstringRound name, e.g. Quarterfinal.
homePlayerobjectPlayer metadata: name, country, SofaScore ID, ranking, Tennis Abstract slug and Elo (when available).
awayPlayerobjectAway player metadata (same shape as homePlayer).
scoreobjectSet-by-set score for live/finished matches.
oddsarrayPre-match odds markets from SofaScore: moneyline, set winner, totals, with fractional and decimal values and last change direction.
playerFormarrayAggregated recent form per player (from Tennis Abstract recent results): last N results, surface win rates, and head-to-head record between the two players.
headToHeadobjectAggregated head-to-head record between the two players (home/away wins).
playerobjectPlayer history record: player identity and total rows.
matchesarrayMatch history rows for playerHistory mode.

Live example from a bounded run:

{
"recordType": "matchPreview",
"scrapedAt": "2026-08-16T10:39:43.037276+00:00",
"matchId": 16737476,
"startTime": "2026-08-16T15:00:00+00:00",
"status": "notstarted",
"tournament": {
"name": "Cincinnati, USA",
"category": "ATP",
"tournamentId": 194944,
"uniqueTournamentId": 2373
},
"surface": "Hardcourt outdoor",
"round": "Round of 64",
"homePlayer": {
"name": "Marco Trungelliti",
"country": "Argentina",
"playerId": 38517,
"ranking": 82
},
"awayPlayer": {
"name": "Daniil Medvedev",
"country": "Russia",
"playerId": 163504,
"ranking": 5
},
"score": {
"homeSets": null,
"awaySets": null,
"sets": []
},
"odds": [
{
"market": "Full time",
"group": "Home/Away",
"period": "Match",
"suspended": false,
"choices": [
{
"name": "1",
"fractional": "9/2",
"decimal": 5.5,
"change": -1
},
{
"name": "2",
"fractional": "7/50",
"decimal": 1.14,
"change": 1
}
]
},
{
"market": "First set winner",
"group": "Home/Away",
"period": "1st set",
"suspended": false,
"choices": [
{
"name": "1",
"fractional": "3/1",
"decimal": 4.0,
"change": -1
},
{
"name": "2",
"fractional": "1/4",
"decimal": 1.25,
"change": 1
}
]
},
{
"market": "Total games won",
"group": "Total sets/games",
"period": "Extra time",
"suspended": false,
"choices": [
{
"name": "Over",
"fractional": "10/11",
"decimal": 1.91,
"change": 1
},
{
"name": "Under",
"fractional": "4/5",
"decimal": 1.8,
"change": -1
}
]
}
],
"playerForm": [
{
"player": "Marco Trungelliti",
"playerId": 38517,
"recent": [
{
"date": "13-Aug-2026",
"tournament": "Cincinnati",
"surface": "Hard",
"result": "W",
"opponent": "Hamad Medjedovic",
"score": "7-5 7-6(2)"
},
{
"date": "13-Aug-2026",
"tournament": "Cincinnati",
"surface": "Hard",
"result": "W",
"opponent": "Kei Nishikori",
"score": "4-6 7-5 6-3"
},
{
"date": "13-Aug-2026",
"tournament": "Cincinnati",
"surface": "Hard",
"result": "W",
"opponent": "Sebastian Ofner",
"score": "4-6 6-4 7-6(4)"
},
{
"date": "02-Aug-2026",
"tournament": "Montreal",
"surface": "Hard",
"result": "L",
"opponent": "Nicolas Mejia",
"score": "7-6(5) 3-6 6-1"
},
{
"date": "20-Jul-2026",
"tournament": "Kitzbuhel",
"surface": "Clay",
"result": "L",
"opponent": "Yannick Hanfmann",
"score": "6-4 7-6(2)"
},
{
"date": "20-Jul-2026",
"tournament": "Kitzbuhel",
"surface": "Clay",
"result": "W",
"opponent": "Juan Manuel Cerundolo",
"score": "6-2 6-1"
},
{
"date": "13-Jul-2026",
"tournament": "Umag",
"surface": "Clay",
"result": "L",
"opponent": "Alejandro Davidovich Fokina",
"score": "6-4 7-5"
},
{
"date": "13-Jul-2026",
"tournament": "Umag",
"surface": "Clay",
"result": "W",
"opponent": "Kyrian Jacquet",
"score": "6-2 6-3"
},
{
"date": "29-Jun-2026",
"tournament": "Wimbledon",
"surface": "Grass",
"result": "L",
"opponent": "Martin Damm",
"score": "7-6(5) 6-7(5) 7-6(2) 7-6(5)"
},
{
"date": "22-Jun-2026",
"tournament": "Eastbourne",
"surface": "Grass",
"result": "L",
"opponent": "Jack Pinnington Jones",
"score": "5-7 6-3 7-5"
}
],
"surfaceSplits": {
"hard": 0.25,
"clay": 0.467,
"grass": 0.0
},
"elo": 1636
},
{
"player": "Daniil Medvedev",
"playerId": 163504,
"recent": [
{
"date": "02-Aug-2026",
"tournament": "Montreal",
"surface": "Hard",
"result": "L",
"opponent": "Botic Van De Zandschulp",
"score": "6-3 7-6(5)"
},
{
"date": "29-Jun-2026",
"tournament": "Wimbledon",
"surface": "Grass",
"result": "L",
"opponent": "Jan Lennard Struff",
"score": "7-6(4) 7-6(5) 7-5"
},
{
"date": "29-Jun-2026",
"tournament": "Wimbledon",
"surface": "Grass",
"result": "W",
"opponent": "Daniel Merida Aguilar",
"score": "3-6 6-3 7-5 6-2"
},
{
"date": "29-Jun-2026",
"tournament": "Wimbledon",
"surface": "Grass",
"result": "W",
"opponent": "Marin Cilic",
"score": "6-1 6-2 6-4"
},
{
"date": "15-Jun-2026",
"tournament": "Halle",
"surface": "Grass",
"result": "L",
"opponent": "Daniel Altmaier",
"score": "6-4 6-7(6) 6-4"
},
{
"date": "15-Jun-2026",
"tournament": "Halle",
"surface": "Grass",
"result": "W",
"opponent": "Terence Atmane",
"score": "6-4 6-4"
},
{
"date": "15-Jun-2026",
"tournament": "Halle",
"surface": "Grass",
"result": "W",
"opponent": "Tomas Martin Etcheverry",
"score": "6-3 6-4"
},
{
"date": "08-Jun-2026",
"tournament": "s Hertogenbosch",
"surface": "Grass",
"result": "L",
"opponent": "Kamil Majchrzak",
"score": "7-6(4) 6-1"
},
{
"date": "08-Jun-2026",
"tournament": "s Hertogenbosch",
"surface": "Grass",
"result": "W",
"opponent": "Marin Cilic",
"score": "6-2 3-6 6-1"
},
{
"date": "08-Jun-2026",
"tournament": "s Hertogenbosch",
"surface": "Grass",
"result": "W",
"opponent": "Thijs Boogaard",
"score": "6-3 4-6 7-6(6)"
}
],
"surfaceSplits": {
"hard": 0.735,
"clay": 0.6,
"grass": 0.667
},
"elo": 1971
}
],
"headToHead": {
"home": 0,
"away": 0,
"matches": []
}
}

Pricing

Tennis Intelligence runs under Apify's pay-per-event model: you pay per delivered dataset record, plus the platform usage your run generates. No subscription, no data limits.

A typical run costs very little on the platform. Measured on a real run (20 matchPreview records, ~3 minutes): $0.04 total platform usage, of which ~$0.037 is compute units and ~$0.0006 is residential proxy traffic. That works out to roughly $0.002 per record of platform cost before the per-record event price.

You control spend directly with maxMatches (cap on delivered records, also clamped to your run's pay-per-event budget) and by trimming includeOdds / includeHistory. The per-record price is shown on this Actor's Pricing tab.

Usage patterns

  • Start from a match ID — Grab match IDs from SofaScore, or first run schedule / liveMatches and reuse the matchId values from the output dataset as matchIds input for a matchPreview run.
  • Bound the first run — Use a small maxMatches; it caps the delivered records and is also clamped to the run's pay-per-event budget. maxRequestsPerMinute and delaySeconds control how fast the Actor hits the sources.
  • Trim the fat — Set includeOdds: false to skip the odds feed (one request per match saved) or includeHistory: false to skip the Tennis Abstract form/splits blocks (two to three requests per match saved). The metadata-only record stays valid.
  • Handle rate limiting — SofaScore blocks plain datacenter IPs, so on platform runs the Actor routes through the Apify proxy pool (auto group with an automatic RESIDENTIAL fallback) and impersonates a browser TLS fingerprint; on local runs it uses direct egress. Lower maxRequestsPerMinute or add delaySeconds for very large runs; the Actor retries 429s with backoff automatically.
  • Estimate the cost of a run — A typical matchPreview run costs about $0.002 in platform usage per delivered record (compute units + residential proxy traffic), so a 100-record run lands near $0.20 before the per-record event price. maxMatches is your hard spend cap.

Input reference

fieldtypedefaultwhat it does
modematchPreview / liveMatches / schedule / playerHistory"matchPreview"Which data operation to run.
matchIdsarraySofaScore match IDs for matchPreview mode (from schedule/live runs or the SofaScore site). Each produces one aggregated match preview record. Optional: if omitted, the Actor previews the next scheduled matches of the configured tournament.
tournamentIdintegerSofaScore tournament ID to pull upcoming matches from when matchIds is empty in matchPreview mode. Optional.
datestringCalendar date for schedule mode (upcoming matches on that date). Defaults to today (UTC).
playerstringTennis Abstract player slug for playerHistory mode, e.g. NovakDjokovic, IgaSwiatek, JannikSinner (name without spaces).
maxMatchesinteger20Cap on the number of records produced per run (per mode: match previews, live matches, scheduled matches, or history rows). Default 20; set 0 for unlimited.
maxFormMatchesinteger10How many recent matches per player to include in the form block of a match preview. Default 10.
includeOddsbooleantrueFetch the SofaScore odds feed (moneyline, set winner, totals) for each previewed or live match. One extra request per match.
includeHistorybooleantrueFetch Tennis Abstract recent-results for each player in a match preview (form + surface splits). One or two extra requests per match.
maxRequestsPerMinuteinteger120Hard rate limit across all requests. Higher values speed up large runs but raise the risk of rate-limiting. Default 120 (each blocked SofaScore request is retried once via the proxy pool with browser TLS impersonation).
delaySecondsnumber0.5Politeness delay between HTTP requests.
proxyConfigurationobjectApify Proxy. On platform runs this defaults to on: SofaScore blocks plain datacenter egress, so the Actor routes through the Apify proxy pool (auto group, with a residential fallback) and uses browser TLS impersonation to deliver data. Local runs are direct unless enabled here. On accounts where the auto group only covers datacenter IPs, the Actor automatically retries through the RESIDENTIAL group.

FAQ

Where do the odds come from?

The SofaScore odds feed for each match (provider 1): typically full-time moneyline, first-set winner and total-games markets, with fractional and decimal values and the last change direction. SofaScore is a single book-implied source, not a multi-book comparison.

What exactly is in a match preview?

Match and tournament metadata, both players' names, countries, IDs and current rankings, the pre-match odds block, and per player: last N results (date, tournament, surface, result, opponent, score), surface win rates (hard/clay/grass) and Elo, plus the aggregated head-to-head record between the two players.

Why is head-to-head sometimes empty?

Tennis Abstract stores head-to-head aggregates per opponent; if the two players have never met (or the pair is absent from the table), headToHead stays null. Recent-results rows for upcoming matches (no score yet) are skipped, not reported as losses.

Do I need a SofaScore or Tennis Abstract account or API key?

No. Both sources are public pages/endpoints and the Actor reads them directly over HTTP. SofaScore and Tennis Abstract are unofficial, analytics-focused sources.

Is this allowed? Terms of service and legality

SofaScore and Tennis Abstract are unofficial sources and neither offers a licensed API for this data. The Actor reads public pages/endpoints at a polite rate; you are responsible for complying with the sources' terms and with any laws that apply to your use of the data, including any commercial redistribution. This is analytics/odds reference data, not a guarantee of outcomes.

Why does a run sometimes deliver fewer records than I asked for?

Records are only delivered for matches that exist and resolve. A match with no odds posted yet or no Tennis Abstract history keeps odds/headToHead null but is still delivered as a valid record; matches whose tournament feed fails or is blocked are skipped with a log warning. On very large runs the pay-per-event budget and maxMatches also cap the total.

Why do some fields come back null or empty?

A source may omit the field (e.g. odds not yet posted for a qualifier), or a request may be blocked or missing. Unavailable values stay null instead of being invented, and failed pages are logged and skipped rather than failing the run. On Apify platform runs the Actor routes through the proxy pool with browser TLS impersonation so SofaScore does not block it; on a local run your own IP applies.

Integration

JavaScript

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('varnie/tennis-intelligence').call({"mode": "matchPreview", "matchIds": [16737476], "maxMatches": 1});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Python

from apify_client import ApifyClient
client = ApifyClient('YOUR_TOKEN')
run = client.actor('varnie/tennis-intelligence').call(run_input={'mode': 'matchPreview', 'matchIds': [16737476], 'maxMatches': 1})
items = client.dataset(run['defaultDatasetId']).list_items().items

CLI

$apify call varnie/tennis-intelligence --input '{"mode": "matchPreview", "matchIds": [16737476], "maxMatches": 1}'

REST

curl -X POST "https://api.apify.com/v2/acts/varnie~tennis-intelligence/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H 'Content-Type: application/json' -d '{"mode": "matchPreview", "matchIds": [16737476], "maxMatches": 1}'

Support

support@hyperbach.com

This page is generated from readme.toml, the Actor schemas, and a live sample. Edit the sources and run python gen_readme.py; do not hand-edit README.md.