Live WebSockets

The live socket pushes every point of every live match as the scoreboard changes, and a final event when a match ends. Available on Scale, and on Pro with the Live WebSockets add-on.

Connect

text
wss://api.citoapi.com/api/v1/tennis/live/ws

Authenticate on the upgrade with the x-api-key header, Authorization: Bearer <key>, or the api_key query parameter. A key without WebSocket access is refused with HTTP 403 and error.code WEBSOCKETS_REQUIRED.

javascript
const ws = new WebSocket("wss://api.citoapi.com/api/v1/tennis/live/ws?api_key=YOUR_API_KEY");

ws.onopen = () => ws.send(JSON.stringify({ action: "subscribe", rooms: ["all"] }));

ws.onmessage = (event) => {
  const frame = JSON.parse(event.data);
  if (frame.type === "POINT_UPDATE") console.log(frame.match_id, frame.data.game_score, frame.data.sets);
  if (frame.type === "MATCH_FINISHED") console.log("final", frame.match_id, frame.data.sets);
};

Messages you send

MessageEffect
{"action":"subscribe","rooms":["all"]}Every live match.
{"action":"subscribe","rooms":["match:s365_4869062"]}One match, by the id from /matches/live.
{"action":"unsubscribe","rooms":["match:s365_4869062"]}Stop one match.
{"action":"ping"}The server answers pong.

Nothing is sent until you subscribe to a room. Invalid JSON or an unknown room answers an error frame (INVALID_JSON, BAD_ROOMS); the connection stays open.

Frames you receive

These three are real frames from the production socket. The point frame below shows its shape, with values abbreviated:

json
{"type":"ready","stream":"tennis.live","path":"/api/v1/tennis/live/ws","rooms":[],"protocol":{"subscribe":{"action":"subscribe","rooms":["match:{matchId}","all"]},"unsubscribe":{"action":"unsubscribe","rooms":["match:{matchId}"]},"ping":{"action":"ping"}},"events":["POINT_UPDATE","MATCH_FINISHED"]}
{"type":"subscribed","rooms":["all"],"changed":["all"]}
{"type":"ping","ts":"2026-10-02T23:06:00.037Z"}

POINT_UPDATE and MATCH_FINISHED share one shape. data carries the scoreboard: sets, the current game score, the server, the tiebreak score when one is on, the last points played and the match statistics so far.

json
{
  "type": "POINT_UPDATE",
  "match_id": "s365_4869062",
  "data": {
    "match_id": "s365_4869062",
    "tour": "ATP",
    "tournament_name": "Challenger-D, Curitiba",
    "round": "R16",
    "round_name": "Round of 16",
    "player1": { "id": "atp_…", "name": "…" },
    "player2": { "id": "atp_…", "name": "…" },
    "current_set": 2,
    "game_score": "30-40",
    "server": 1,
    "status": "IN_PROGRESS",
    "sets": [ { "set_num": 1, "score": "4-6" }, { "set_num": 2, "score": "2-1" } ],
    "tiebreak_score": null,
    "point_history": [ "…last 10 points…" ],
    "stats": [ "…match statistics so far…" ]
  }
}

Timing and recovery

  • Updates arrive within seconds of each point: the live feed is read every few seconds and each change is pushed as it lands.
  • The server sends a ping frame every 15 seconds; a socket that sees none for 45 seconds should reconnect.
  • After a reconnect, call GET /tennis/matches/live once to resynchronise every scoreboard, then keep the socket. REST and the socket carry the same ids and the same scores (checked on every live match in our launch sweep).
  • A match that ends is archived into the match history immediately, under the same id, so /tennis/matches/{id} and player match logs have it as soon as MATCH_FINISHED arrives.