Live Tennis API · docs · version 1.13.4

Tennis WebSocket feed and webhooks

How do you receive tennis data as it happens instead of polling? Two ways, both ULTRA. GET /ws-token mints a short-lived token for the high-fan-out push feed, which streams score and point changes as they are written. POST /webhooks registers an outbound HTTP callback for the same events; GET /webhooks lists your registrations and never returns the signing secret. Webhooks require a direct key, not a marketplace one.

4 endpoints on this page. Base URL https://api.livetennisapi.com/api/public/v1; authenticate with the X-API-Key header. Plans involved: ULTRA. A free key needs no card.

POST /webhooks

Register an outbound webhook (ULTRA, direct keys only)

Plan required: ULTRA · operationId: createWebhook

We POST the same frames the WebSocket sends to your HTTPS endpoint on every live score commit. Up to 3 webhooks per key (409 webhook_limit past that). The response is the ONLY time the signing secret is shown — store it. Each delivery carries X-LTAPI-Signature (sha256=<hex> — HMAC-SHA256 of the RAW request body with your webhook secret; verify with a constant-time compare), X-LTAPI-Timestamp (Unix seconds at send time — reject stale replays at your edge) and X-LTAPI-Event (the frame type: score, break_point, break_point_result or point). Delivery is best-effort, at-most-once, no replay: one attempt per frame with a ~3s timeout and redirects disabled. Every score frame is the complete current score, so a missed delivery self-corrects on the next commit. A point frame is an EVENT, not a state — a missed one does NOT self-correct; recover it with GET /matches/{matchId}/points?after_seq= and dedup by seq. After 25 consecutive failures the webhook is disabled automatically (enabled:false, last_error set — visible in GET /webhooks); delete and re-register to resume.

Responses

POST /webhooks — responses
StatusMeaning
201Created — includes secret (shown exactly once)
400Bad query parameter
401Missing, unknown, or disabled credentials
403Your tier doesn't unlock this endpoint
409Webhook limit reached (3 per key) — delete an existing webhook first
429Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

Example

curl https://api.livetennisapi.com/api/public/v1/webhooks \
  -H "Authorization: Bearer twjp_..."

GET /webhooks

List your webhooks (ULTRA, direct keys only; never includes the secret)

Plan required: ULTRA · operationId: listWebhooks

Responses

GET /webhooks — responses
StatusMeaning
200Your webhooks
401Missing, unknown, or disabled credentials
403Your tier doesn't unlock this endpoint
429Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

Response fields

GET /webhooks — response fields
FieldTypeDescription
dataarray of object
metaobject

Example

curl https://api.livetennisapi.com/api/public/v1/webhooks \
  -H "Authorization: Bearer twjp_..."

DELETE /webhooks/{webhookId}

Remove one of your webhooks (ULTRA, direct keys only)

Plan required: ULTRA · operationId: deleteWebhook

Parameters

DELETE /webhooks/{webhookId} — parameters
NameInTypeRequiredNotes
webhookIdpathintegeryes

Responses

DELETE /webhooks/{webhookId} — responses
StatusMeaning
200Deleted
401Missing, unknown, or disabled credentials
403Your tier doesn't unlock this endpoint
404No such resource, or no data yet
429Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

Response fields

DELETE /webhooks/{webhookId} — response fields
FieldTypeDescription
deletedinteger

Example

curl https://api.livetennisapi.com/api/public/v1/webhooks/{webhookId} \
  -H "Authorization: Bearer twjp_..."

GET /ws-token

Mint a connection token for the high-fan-out push feed (ULTRA)

Plan required: ULTRA · operationId: createWsToken

Returns a short-lived signed token plus the push WebSocket URL and the channel vocabulary: match:{match_id} per-match streams and slate:all for every live score frame. Frames are the same allowlist score objects the polling endpoints return. This is a separate surface from the native /ws feed described above — same ULTRA gate, built for high fan-out (no shared connection ceiling), and the recommended home for continuous/production streaming. The endpoint speaks the **Centrifugo client protocol** (v2, JSON). Easiest path: the official Python (livetennisapi ≥ 1.4.0) and JS (≥ 1.5.0) SDKs ship a built-in PushStream client — no extra dependency. Raw protocol, if you prefer your own client: (1) open a WebSocket to ws_url; (2) send {"connect": {"token": "<token>"}, "id": 1} — the token goes INSIDE this JSON frame, never as a raw first message; (3) subscribe per channel with {"subscribe": {"channel": "slate:all"}, "id": 2}; (4) publications arrive as {"push": {"channel": ..., "pub": {"data": <frame>}}}; (5) the server's heartbeat is an empty JSON object {} — reply with {} promptly or you will be disconnected. Messages may batch several newline-delimited JSON objects. Tokens are short-lived and the connection closes around token expiry: mint a fresh token on EVERY reconnect and re-subscribe. The channels object lists only channels that will actually deliver for your key right now (a channel name in this response is a promise). Where enabled server-side, additional channel families appear: point:match:{match_id} / point:slate (per-point events), listed — as point_match / point_slate in the vocabulary — only for keys whose plan carries the point surface, and signal:match:{match_id} / signal:slate (derived break_point, break_point_result and divergence events, and since 2026-09-12 the stoppage family: medical_timeout_start/end, trainer_called/_end, toilet_break_start/end, stoppage_start/end, pause_start/end). A family absent from the response will not deliver for your key right now. Deliberately separate channels: a slate:all subscriber asked for score states and never starts receiving events unasked. Point and signal frames are events, not states — a missed point does NOT self-correct on the next frame; recover it via GET /matches/{matchId}/points?after_seq= and dedup by seq.

Responses

GET /ws-token — responses
StatusMeaning
200Connection token, push URL and channel vocabulary
401Missing, unknown, or disabled credentials
403Your tier doesn't unlock this endpoint
429Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

Response fields

GET /ws-token — response fields
FieldTypeDescription
tokenstring
expires_ininteger
ws_urlstringThe push WebSocket URL to connect to with the token.
channelsobjectChannel vocabulary — match is the per-match pattern (match:{id}), slate is the every-live-score channel (slate:all). A channel listed here will actually deliver for your key; one missing will not.

Example

curl https://api.livetennisapi.com/api/public/v1/ws-token \
  -H "Authorization: Bearer twjp_..."