Skip to main content
The books-relayer WebSocket gives you live game state (scores, clocks, period changes) and market movements (every price change from every sportsbook we track) in real time. There are two separate channels. Pick one or both:

/ws/live

Live game state only. Scores, clock, period, status changes. Quiet by design — only emits when something actually happens. Low bandwidth.

/ws/odds

Every price_change event from every book, plus the full game-state stream. High volume — 100 to 500 events per second during peak.

Endpoints

wss://books.polynode.dev/ws/live?key=pn_live_YOUR_KEY
wss://books.polynode.dev/ws/odds?key=pn_live_YOUR_KEY
Both channels accept the API key as either a query parameter or an Authorization: Bearer pn_live_YOUR_KEY header.

Which channel do I use?

If you want to…Use
Build a live scoreboard UI/ws/live
Send push notifications on score changes/ws/live
Track real-time odds movements for arbitrage / alerts/ws/odds
Monitor a specific book’s price action/ws/odds
Both scoreboard + odds/ws/odds (it includes everything /ws/live has)
/ws/live is the correct default for 90% of consumers. Subscribe to /ws/odds only if you actually need the raw price firehose.

Connection lifecycle

  1. Open connection with your API key. Bad/missing key → HTTP 401 before the upgrade.
  2. Receive welcome frame (JSON). Contains your conn_id and the channel name.
  3. Receive snapshot frame (JSON). Contains every currently-live game and their full state. Initialize your in-memory view from this, then apply subsequent deltas. See snapshot event.
  4. Receive delta events (JSON) as they happen. Apply each event on top of the snapshot baseline.
  5. Server sends Ping every 30 seconds to keep the TCP connection alive through NATs and middleboxes. Your client library auto-responds with Pong.
  6. Close the socket cleanly when done. On reconnect, discard your state and wait for the next snapshot — do NOT try to resume.

Welcome frame

First message received after a successful connection:
{
  "type": "welcome",
  "channel": "live",
  "conn_id": 1,
  "message": "connected to books-relayer live channel"
}

Snapshot frame

Second message, immediately after welcome. Contains every currently-live game. Use this as your baseline before applying any subsequent delta events.
{
  "type": "snapshot",
  "channel": "live",
  "ts": 1776210900000,
  "count": 11,
  "games": [
    {
      "game_id": "40664-16839-2026-04-14",
      "pn_slug": "lib-gar-est-2026-04-14",
      "pn_league_code": "lib",
      "scores": { "score_home": 2, "score_away": 1, "period": "2H", "clock": "94", "is_live": true },
      "home_team": { "name": "Club Estudiantes de La Plata" },
      "away_team": { "name": "Cusco FC" }
    }
  ]
}
See snapshot event reference for the full schema. This is the standard pattern for all polynode WebSockets that carry stateful data (scores, orderbooks, position books): always emit a snapshot on connect before streaming deltas. Ephemeral streams (mempool transactions, trade prints) skip this step because there is no persistent state.

Keepalive & client-side pings

The server sends protocol-level Ping frames every 30 seconds. Your WebSocket library should auto-respond with Pong — every mainstream client (websockets in Python, ws in Node, tokio-tungstenite in Rust, browser WebSocket) does this by default. Some proxies strip WebSocket control frames. If you are behind one, send an application-level JSON ping periodically:
{"action": "ping"}
The server will reply immediately with:
{"type": "pong", "ts": 1776208974186}
This round-trip confirms the connection is alive end-to-end, not just at the TCP layer.

Scale and limits

  • Hard cap: 20,000 total concurrent subscribers across both channels. New connections past the cap get HTTP 503.
  • Per-subscriber bounded channel: 256 events. If your consumer stalls, events are dropped on the server (not buffered indefinitely).
  • Server-side fan-out is serialize-once, refcount-cloned. Each event is encoded to JSON exactly once per broadcast, then distributed to all matching subscribers via Arc<String> — so scaling to thousands of subscribers is cheap on CPU.

See also