Live tennis data API for your next build.
Bring live scores into a scoreboard, an analysis tool or an AI agent. Start with a free REST key. Add match events, statistics or streaming when your project needs them.
No card required · 100 calls/day · Paid plans from $9.99/mo
{
"status": "live",
"tour": "atp",
"sets": [1, 0],
"games": [[6, 3], [4, 4]],
"points": ["40", "30"],
"server": 1,
"is_tiebreak": false
}
Sets, games, points and server. Every score array is player-major: player one, then player two.
What are you building?
Choose the data your project needs, then size the request allowance to the way it runs.
A live scoreboard
Read live and upcoming matches with a free key. Cache one paginated slate on your server and share it with your viewers.
Build the first request →Match analysis or research
Pro adds events and market prices. Ultra adds model win-probability and in-play statistics. For completed point sequences, compare the separate historical collections.
Explore historical data →An app or AI agent
Use the Python or JavaScript SDK for API calls, or connect an agent through MCP. Ultra adds WebSocket and webhook push for point-driven applications.
Connect through MCP →Your first live-score request
Create your free key and complete the email
verification. Set LIVE_TENNIS_API_KEY in your terminal, then run:
curl --fail-with-body --silent --show-error \
"https://api.livetennisapi.com/api/public/v1/matches?status=live&limit=200" \
-H "X-API-Key: $LIVE_TENNIS_API_KEY"
The response contains data (matches) and meta (pagination).
An empty data array can mean no matches are live right now. Try
status=upcoming to inspect fixtures. Each request returns at most 200 matches.
If meta.has_more is true, request the next page using
offset = meta.offset + meta.limit.
Python example: read every page of the live slate
Python 3, standard library only. Set the same environment variable first. This script reads one snapshot, then stops. Each page consumes one request from your allowance.
import json
import os
from urllib.parse import urlencode
from urllib.request import Request, urlopen
base = "https://api.livetennisapi.com/api/public/v1"
headers = {"X-API-Key": os.environ["LIVE_TENNIS_API_KEY"]}
offset = 0
matches = []
while True:
query = urlencode({"status": "live", "limit": 200, "offset": offset})
request = Request(f"{base}/matches?{query}", headers=headers)
with urlopen(request, timeout=20) as response:
page = json.load(response)
matches.extend(page["data"])
if not page["meta"]["has_more"]:
break
offset = page["meta"]["offset"] + page["meta"]["limit"]
print(json.dumps(matches, indent=2))
Keep the key on your server. Cache the result for your website or app. Do not put a secret key in public browser code. A changing live slate is not an atomic snapshot across multiple pages.
If a request fails: check the key on a 401 and read the required capability on a 403. On a 429, inspect the limit/reset response before retrying or upgrading. Open the live-score reference.
What counts as live data here?
Four different things, and they are not all on the same plan. Being explicit about that is the point of this page:
- The scoreline. Sets, games, points, who is serving, tiebreak flag. This is what most people mean by live scores, and it is on the free tier.
- Match events. The individual things that happen inside a match, not the state they leave behind.
- Market prices. Match-winner bid, ask and mid, with history.
- Model win-probability and in-play statistics. Our own model's read of the current state, and the serve and return numbers as the match unfolds.
Which plan carries which
| Data | Endpoint | Plan |
|---|---|---|
| Live and upcoming matches, scoreline, players, fixtures | GET /matches?status=live | Free, $0, no card |
| Completed results and the point-by-point tape | GET /history/matches/{id} | Basic, $9.99/mo |
| Match events and match-winner market prices | GET /matches/{id}/events, /prices | Pro, $29.99/mo |
| Model win-probability, in-play statistics, streaming push feed | GET /matches/{id}/analysis, GET /ws-token | Ultra, $99.99/mo |
Full plan comparison, annual pricing and the per-minute and per-day quotas are on the pricing page.
What a live response actually looks like
One detail catches almost every first integration: every score array is player-major. The first element is player one, the second is player two. Not "home and away", and not ordered by who is winning.
{
"id": 18953,
"status": "live",
"tour": "atp",
"sets": [1, 0],
"games": [[6, 3], [4, 4]],
"points": ["40", "30"],
"server": 1,
"is_tiebreak": false
}
That reads: player one leads by a set, took the first 6-4, and the second stands at 3-4. The game in progress is 40-30, and player one is serving.
Polling or streaming?
Poll REST when a refreshed scoreline is enough. The free tier allows 30 requests a minute and 100 a day, so it suits a prototype or occasional checks. At one page every 30 seconds, the 100-call allowance lasts 50 minutes. Eight hours costs 960 calls. A slate needing two pages doubles that cost.
| Refresh interval | Calls in eight hours | Daily allowance that fits |
|---|---|---|
| Every 60 seconds | 480 | Basic: 1,000/day · $9.99/mo |
| Every 30 seconds | 960 | Basic: 1,000/day, with only 40 left |
| Every 10 seconds | 2,880 | Pro: 10,000/day · $29.99/mo |
Daily calls = pages per refresh × active hours × 3,600 ÷ refresh interval in seconds. Add calls to other endpoints and keep within the separate burst limits. Cache the slate once for all viewers: per-visitor polling multiplies your usage. Compare every allowance on the pricing page.
Use the streaming push feed when you need every point as it happens and the gap between polls
would change what your code does. That means per-point logic, or reacting where a twenty-second-old score
is materially worse than a current one. That is the Ultra tier, reached by minting a short-lived
token from GET /ws-token. The feed speaks the Centrifugo client protocol, not
plain JSON frames. So use the PushStream client in the official
Python or
JavaScript SDK instead of a bare
WebSocket. It mints the token, subscribes, answers heartbeats and mints a fresh token on every
reconnect, which is the part hand-rolled clients get wrong.
The boundary between the two, with the arithmetic and no rule of thumb, is in REST or WebSocket for live tennis scores?. Poll REST for the scoreline, and use the push feed only when you need per-point events.
Live data FAQ
Is there a free tennis live scores API?
Yes. The free tier returns live and upcoming matches across ATP, WTA, Challenger and ITF, with
no card required. Each match carries the full scoreline: sets, games, points, server and tiebreak flag.
It allows 30 requests a minute and 100 a day. The daily cap binds before the rate limit does. So
read the slate with GET /matches?status=live&limit=200 and follow
meta.has_more for additional pages. Each page costs a request. At one page
every 30 seconds, the daily free allowance lasts 50 minutes.
What is the difference between live scores and live match data?
The scoreline is the state a match has reached. Live match data also covers what produced it and what it implies. Scores (sets, games, points, server) are on the free tier. Individual match events and match-winner market prices are Pro at $29.99 a month. Model win-probability and in-play serve and return statistics are Ultra at $99.99. Being able to read a scoreboard and being able to reason about a match in progress are different products, and they are priced differently.
Do I need a WebSocket for live tennis data?
Usually not. Polling GET /matches?status=live returns a page of current live
match states and needs no reconnection handling. Follow pagination to read the slate.
Reach for the streaming push feed when you can
name a behaviour polling makes impossible. Reacting to individual points is one. Acting where a
twenty-second-old score is materially worse than a current one is another. That is the Ultra tier. Starting
on REST and moving up when you hit a real limit is cheaper than maintaining a connection you do
not yet need.
Which tours are covered live?
The feed includes ATP, WTA, Challenger and ITF, singles and doubles, including qualifying draws.
This is a scope statement, not a guarantee that every match or field is available. Check the
current slate and representative matches for the tours you need. Historical per-tour
completeness is published at GET /history/coverage with an as-of date. It is not a
measurement of today's live completeness. For a required field, date range or commercial-use
question, send us your requirements
before choosing a paid plan.
How do I read the score arrays correctly?
Every array is player-major: the first element is player one, the second is player two. So
"sets": [1, 0] means player one leads by a set. "games": [[6, 3], [4, 4]]
means the first set finished 6-4 and the second stands at 3-4. And
"server": 1 means player one is serving. This is the single detail that most often
trips up a first integration. The arrays look as though they should be ordered by match
position instead of by player.
Full endpoint detail is in the live scores reference and the complete API reference. Plans and quotas: pricing.
Get the first request working.
Explore the live response on a free key, then choose the plan for your fields and workload.