Poll GET /matches?status=live on a timer if you need the scoreline. Use the WebSocket feed if you need every point as it happens and the delay between polls would change what your code does. That is the whole decision, and for most applications the answer is REST — which is also the one that works on a free key.
Base URL https://api.livetennisapi.com/api/public/v1, X-API-Key header on every request. Tier limits below are the published figures as of September 14, 2026. A free key needs no card: livetennisapi.com/subscribe/free.
What each one gives you
| REST polling | WebSocket push | |
|---|---|---|
| Endpoint | GET /matches?status=live, GET /matches/{id}/score |
push feed via GET /ws-token (or the native WS /ws) |
| Tier | Free ($0, no card) | Ultra ($99.99/mo) |
| Granularity | the score when you asked | every point as it is written |
| Cost model | one request per poll, against 100/day on Free | one connection |
| Fails by | going stale between polls | needing reconnect logic |
What does polling actually cost?
The free tier allows 30 requests per minute and 100 per day. A scoreboard polling one call every 20 seconds uses 3 per minute — comfortably inside the rate limit, but it exhausts the daily 100 in about 33 minutes.
That is the real constraint on Free, and it is a daily cap rather than a speed limit. Options, in the order most people should consider them:
- Poll the slate, not each match.
GET /matches?status=livereturns every in-progress match in one request. Polling that once every 30 seconds is 2 requests/minute regardless of how many matches are on — which is the single biggest saving available, and the mistake most often made is polling per match. - Poll only while matches are live. Tennis has long dead periods. A scheduler that sleeps when
GET /fixturessays nothing starts for six hours costs nothing during those six hours. - Move up a tier. Basic ($9.99/mo) raises the cap to 60/min and 1,000/day, which sustains a 30-second poll all day.
When is REST genuinely not enough?
Polling gives you the score at the moment you asked. If a point is won and lost between two polls, you never see it. That matters for exactly three kinds of application:
- Per-point logic — anything reacting to individual points, break points, or the shape of a game rather than the scoreline.
- Latency-sensitive reactions — where acting on a 20-second-old score is materially worse than acting on a current one.
- Many concurrent matches at high frequency — where per-match polling would blow the rate limit but a single connection would not.
All three are Ultra: the push feed reached via GET /ws-token, live per-point events at GET /matches/{id}/points, and model win-probability at GET /matches/{id}/analysis.
Connecting to the push feed
There are two WebSocket surfaces and they are not interchangeable. PushStream is the high-fan-out push feed and the one to use for anything continuous; the native /ws feed rides shared infrastructure with a concurrent-connection ceiling per key.
The push feed speaks the Centrifugo client protocol rather than plain JSON frames, so use the SDK rather than a bare WebSocket client — it mints the short-lived token from /ws-token, connects to the URL that response returns, subscribes, answers heartbeats, and mints a fresh token on every reconnect:
from livetennisapi import PushStream
with PushStream() as stream: # slate:all — every live match
for update in stream:
print(update)
with PushStream(match_ids=[18953]) as stream: # one match
for update in stream:
print(update)
with PushStream(match_ids=[18953], points=True) as stream: # per-point events
for update in stream:
print(update)
pip install livetennisapi (≥ 1.4.0) or npm i livetennisapi (≥ 1.5.0) ships it. Writing your own Centrifugo client is possible and documented, but the token-refresh-on-reconnect behaviour is the part people get wrong, and it is the part the SDK already handles.
How do you read the score on either transport?
One detail catches people on both transports. Every score array is player-major: the first element is player one, the second is player two.
{ "sets": [1, 0], "games": [[6, 3], [4, 4]], "points": ["40", "30"], "server": 1 }
That reads: player one leads by a set, won the first set 6–4, the second is 3–4, the current game is 40–30, and player one is serving.
The honest recommendation
Start on REST with a free key. Poll the slate rather than each match, sleep when nothing is live, and see whether the staleness between polls actually costs you anything. Most live-score displays, notification bots and dashboards never need more. Move to the push feed when you can name the specific behaviour that polling makes impossible — not before, because a WebSocket adds reconnection, backfill-on-reconnect and ordering concerns that a polling loop simply does not have.
Full reference at docs.livetennisapi.com, with topic pages for live scores and the push feed and webhooks. Tiers and limits: pricing.