The data behind a tennis prediction model.
Win probabilities come from history done right: rankings as they stood on the day, results split by surface, form windows and serve stats. Every input a tennis model needs, with no look-ahead built in.
- Rankings as of any week
- Surface and level splits
- No odds, by design
{ "player_name": "Jannik Sinner", "filters": { "surface": "Clay" }, "career_summary": { "matches_played": 150, "matches_won": 108, "matches_lost": 42, "win_percentage": 72, "titles_count": 5 }, "serving_stats": { "total_aces": 441, "first_serve_in_pct": 59.3, "first_serve_points_won_pct": 71.3, "break_points_saved_pct": 60.7 }}Seamless Integration with our MCP Server
Give Claude, Cursor, and ChatGPT the tennis archive. One command writes the MCP config, agents call match, ranking and head-to-head tools with real player IDs instead of guessing, and your key stays on your machine.
MCPInstall the Cito MCP serverModel inputs, ready to query
The features that move tennis win probabilities, each one available without scraping or reconciling sources.
Point-in-time rankings
Ask for the list as of any date, so a backtest only sees what was known before the match.
Surface splits
Win-loss and serve stats filtered by Hard, Clay, Grass or Carpet, and by level or year range.
Head-to-head
Every meeting between two players with surface, level and best-of splits.
Form windows
Recent results and win rate per player, and the full match log to build your own windows.
Serve and return stats
Aces, first-serve %, points won on serve, break points: from 1991 for the men, 2016 for the women.
Live state
Set, game and server during play for in-match models, over REST or WebSocket.
What data do you need to predict tennis matches?
Most tennis models start from a rating (Elo or a ranking), then adjust for surface, recent form, head-to-head and serve and return strength. Each input must be measured as of the match date, or the model learns from the future.
| Model input | Endpoint | Note |
|---|---|---|
| Results to build Elo | /tennis/matches?tour=ATP&year=2015 | Page through seasons; order by match_date, then round |
| Ranking on the day | /tennis/rankings?tour=ATP&date=2019-06-03 | Any published week; points are null where the tour did not publish them |
| One player's ranking path | /tennis/rankings/history/{player_id} | Week-by-week rank and points |
| Surface win rate | /tennis/players/{player_id}/stats?surface=Clay | Also filters by level and year range |
| Head-to-head | /tennis/h2h?player1_id=&player2_id= | Surface, level, format, tiebreak and deciding-set splits |
| Recent form | /tennis/players/{player_id}/form | Or build windows from /players/{player_id}/matches |
| Serve and return | /tennis/matches/{match_id}/stats | Per-match box score |
How do I build tennis Elo ratings from the API?
Read results in date order and update both players after every match. The example below rates every ATP match from 2015 onward with a simple K-factor; surface-specific Elo is the same loop keyed by surface.
import requests
from collections import defaultdict
API = "https://api.citoapi.com/api/v1/tennis/matches"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
ROUND_ORDER = {"Q1": 0, "Q2": 1, "Q3": 2, "R128": 3, "R64": 4, "R32": 5, "R16": 6, "QF": 7, "SF": 8, "F": 9}
def season(year):
page = 1
while True:
r = requests.get(API, headers=HEADERS, params={"tour": "ATP", "year": year, "page": page, "page_size": 100}).json()
yield from r["items"]
if not r["has_next"]:
return
page += 1
elo = defaultdict(lambda: 1500.0)
K = 32
for year in range(2015, 2027):
matches = sorted(season(year), key=lambda m: (m["match_date"], ROUND_ORDER.get(m["round"], 5)))
for m in matches:
w, l = m["winner_id"], m["loser_id"]
expected = 1 / (1 + 10 ** ((elo[l] - elo[w]) / 400))
elo[w] += K * (1 - expected)
elo[l] -= K * (1 - expected)
top = sorted(elo.items(), key=lambda kv: -kv[1])[:10]
print(top)Before 2004 many matches carry their tournament's start date (date_is_tournament_start: true), which is why the sort falls back to round order inside a tournament.
How do I avoid look-ahead bias in a tennis backtest?
Use inputs as they stood before the match. Request the ranking list dated on or before the match date rather than today's ranking, compute form from matches strictly earlier than the one you predict, and use career stats filtered by year_to rather than totals that include the future.
Does the API include betting odds?
No. We do not sell bookmaker odds. Models that price matches pair this data with a licensed odds feed: the odds tell you the market's price, and this data tells you what the price should be.
Endpoints you can ship with
Start on players: profiles, filtered career stats, form and the full match log.
Live scores
Every match in progress with the set scores, the game score, who is serving, and live match stats. Open one match for its full state.
- GET
/api/v1/tennis/matches/liveMatches in progress: sets, game score, server, and live stats.
- GET
/api/v1/tennis/matches/live/{match_id}One live match: every set, the current game, and both players' stats.
- GET
/api/v1/tennis/matches/{match_id}/statusScheduled, in progress, or finished, with the result once decided.
- GET
/api/v1/tennis/scheduleThe day's order of play across tours.
- GET
/api/v1/tennis/matches/upcomingScheduled matches, soonest first.
Results
Finished matches with the score of every set, the winner and loser, and the serve and return box score behind them.
- GET
/api/v1/tennis/matches/recentFinished matches, newest first, with set scores.
- GET
/api/v1/tennis/matches/completedEvery match completed on a given day.
- GET
/api/v1/tennis/matches/{match_id}One match: tournament, round, surface, score, and box score.
- GET
/api/v1/tennis/matches/{match_id}/statsAces, double faults, first-serve %, points won, break points.
- GET
/api/v1/tennis/matches?player_id=&year=&surface=The archive, filtered by player, opponent, year, surface, level or round.
Players
Player profiles with country, hand and birth date, career serve and return stats, recent form, and every match played.
- GET
/api/v1/tennis/players/search?q={name}Find a player by name and get their id.
- GET
/api/v1/tennis/players/{player_id}Name, country, hand, height, birth date and ranking.
- GET
/api/v1/tennis/players/{player_id}/statsCareer and season serve, return and win-loss stats.
- GET
/api/v1/tennis/players/{player_id}/formRecent results and win rate.
- GET
/api/v1/tennis/players/{player_id}/matchesThe player's match log, newest first.
Head-to-head
The full record between two players, split by surface and level, or a grid of records across a group of players.
- GET
/api/v1/tennis/h2h?player1_id=&player2_id=Wins, losses and every meeting between two players.
- GET
/api/v1/tennis/h2h/{player1_id}/{player2_id}The same record in path form.
- GET
/api/v1/tennis/h2h/matrix?player_ids=A head-to-head grid for several players at once.
Rankings
Weekly ATP and WTA singles rankings with points and movement, back through the decades, and any player's ranking history.
- GET
/api/v1/tennis/rankingsA full weekly ranking table for ATP or WTA.
- GET
/api/v1/tennis/rankings/topThe top N players of the latest list.
- GET
/api/v1/tennis/rankings/moversBiggest risers and fallers between two lists.
- GET
/api/v1/tennis/rankings/history/{player_id}One player's ranking, week by week.
- GET
/api/v1/tennis/rankings/datesEvery ranking date on file.
Tournaments
Tournaments from Grand Slams to ITF events, the season calendar, and the draw of any edition as a bracket.
- GET
/api/v1/tennis/tournamentsTournaments filtered by tour, level, surface and year.
- GET
/api/v1/tennis/tournaments/calendar?year=2026One season's tournament calendar.
- GET
/api/v1/tennis/tournaments/{tournament_id}One tournament: level, surface, location and editions.
- GET
/api/v1/tennis/tournaments/{tournament_id}/drawThe draw of the latest edition.
- GET
/api/v1/tennis/tournaments/{tournament_id}/editions/{year}/drawThe draw of any past edition.
Leaderboards
Season leaders for the stats that decide matches, computed from every match box score.
- GET
/api/v1/tennis/leaderboards/acesMost aces in the season.
- GET
/api/v1/tennis/leaderboards/1st-serve-wonBest first-serve points won %.
- GET
/api/v1/tennis/leaderboards/break-conversionBest break-point conversion.
- GET
/api/v1/tennis/leaderboards/tiebreak-win-pctBest tiebreak record.
- GET
/api/v1/tennis/leaderboards/comeback-winsMost wins after losing the first set.
Coverage
What the dataset holds: tours, levels, ITF and junior coverage, and the years on file.
- GET
/api/v1/tennis/coverageTours, levels, year range, ITF and junior event counts.
- GET
/api/v1/tennis/competitionsEvery competition with its tour and level.
Figures on this page were checked against the live API on 2026-10-24.
FAQs
Model inputs, backtests and coverage
Can't find what you're looking for? Contact our customer support team
More tennis data
Pages that feed the rest of a model.
- Tennis APILive scores, results, rankings and head-to-head on one key.
- Historical tennis dataEvery result since 1968, weekly rankings and 140k players.
- Tennis head-to-head APIEvery meeting between two players, split by surface and level.
- Tennis stats APIBox scores, career serve and return stats, leaderboards.
- Tennis live score APISet, game and server for every match in progress.
- Tennis Abstract alternativeThe Sackmann CSVs, as a maintained API with live data.
- ATP APIATP rankings, results, players and draws.
- WTA APIWTA rankings, results, players and draws.
- Tennis API docsEvery endpoint, its parameters, and a real response.
Feed your Elo model
Pull a decade of results on a free key, then scale to live data and higher limits when the model goes live.