guide
"Stream CS2 match updates over WebSocket"
Jul 10, 2026
Bottom line: Instead of polling the CS2 API on a timer, you can hold one WebSocket connection and be notified the moment a match's data changes. The handshake is two steps: mint a short-lived ticket over REST, then open the socket with it. The push message is deliberately thin — it carries only the
match_idand whichtablechanged, not the new scores — so the pattern is "get the tap, then re-fetch that resource over REST." Built on the EsportsOdds CS2 data API.
Push beats polling
Polling means asking "anything new?" every few seconds, and almost always hearing "no." A WebSocket flips that: you open one connection and the server tells you when something actually changed, so you do work only when there's work to do.
This is the efficient way to keep the live-scores dashboard current without hammering the endpoint.
The handshake: ticket, then socket
You don't put your API key in the WebSocket URL. Instead you exchange it, over an ordinary authenticated REST call, for a short-lived signed ticket, then connect with that.
First, mint the ticket:
import os
import requests
BASE = "https://api.esportsodds.gg"
KEY = os.environ["ESPORTSODDS_API_KEY"]
def get_ticket() -> str:
resp = requests.post(
f"{BASE}/v1/cs2/ws-token",
headers={"Authorization": f"Bearer {KEY}"},
timeout=10,
)
resp.raise_for_status()
return resp.json()["data"]["token"]
The response is {"data": {"token": "…", "expires_at": "…"}}. The ticket is valid for 60 seconds — long enough to open the connection, not long enough to be worth leaking. Mint one right before you connect.
Connecting and reading the stream
Connect to wss://api.esportsodds.gg/v1/ws?token=<ticket>. Once connected, the server streams change signals. Each one looks like this:
{ "match_id": "0191f2c8-8a1e-7c3a-9f5e-2b6d4e8a1c00", "table": "matches" }
That's the whole message. There's no score in it, no line, no payload — just which match changed and which table. The table field (matches, odds_lines) tells you what to re-fetch. This keeps the stream tiny and means you always read canonical data over REST rather than trusting a fragment pushed down the socket.
import json
from websockets.sync.client import connect
def stream(on_change):
url = f"{BASE.replace('https', 'wss')}/v1/ws?token={get_ticket()}"
with connect(url) as ws:
for raw in ws:
msg = json.loads(raw)
on_change(msg["match_id"], msg["table"])
Re-fetch what you care about
The connection delivers change signals for matches across the feed, so filter for the ones you're tracking and re-fetch only those. Keep a set of match IDs you care about, and when a signal arrives for one, pull its fresh state:
WATCHING = {"0191f2c8-8a1e-7c3a-9f5e-2b6d4e8a1c00"}
HEADERS = {"Authorization": f"Bearer {KEY}"}
def refetch_match(match_id: str):
r = requests.get(f"{BASE}/v1/cs2/matches/{match_id}", headers=HEADERS, timeout=10)
r.raise_for_status()
return r.json()["data"]
def handle(match_id: str, table: str):
if match_id not in WATCHING:
return
match = refetch_match(match_id)
print(f"{match_id} updated ({table}): {match['score_a']}–{match['score_b']}")
stream(handle)
Remember that GET /matches/{id} is the detail endpoint, so it returns IDs, not names (see the quickstart). If you're keeping a scoreboard, you already have the names from the list — merge the fresh score into the row you're holding.
What the stream is, and isn't
Be clear with yourself about the granularity. This is a change-notification channel: the socket taps you when the underlying data is refreshed, so you replace polling with a push and stop wasting calls. It is not a play-by-play broadcast of every in-round event — the freshness of any given field is bounded by how often that data is updated upstream. For a scoreboard, a results bot, or a line-movement tracker, that's exactly the right tool. Design your UI around "this figure is current," and keep the presentation calm — a steady freshness cue, never a countdown.
Two operational notes: reconnect when the socket drops (mint a fresh ticket each time — they expire), and add a small backoff between reconnect attempts so a flapping network doesn't spin.
Next steps
- Build the dashboard this feeds — swap its polling interval for this stream.
- Handle rate limits — your re-fetches are ordinary REST calls and count against the limits.
Mint a ticket, open the socket, filter the signals, re-fetch over REST. The endpoint reference documents the ticket endpoint and every resource you'll re-fetch.