Multiplexed WebSocket: subscribe to markets, scores, plays, stats, and live channels
curl --request GET \
--url https://therundown.io/api/v2/ws \
--header 'X-TheRundown-Key: <api-key>'import requests
url = "https://therundown.io/api/v2/ws"
headers = {"X-TheRundown-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-TheRundown-Key': '<api-key>'}};
fetch('https://therundown.io/api/v2/ws', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://therundown.io/api/v2/ws",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-TheRundown-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://therundown.io/api/v2/ws"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-TheRundown-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://therundown.io/api/v2/ws")
.header("X-TheRundown-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://therundown.io/api/v2/ws")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-TheRundown-Key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyV2 WebSocket
V2 WebSocket: multiplexed channels
Subscribe to market, score, play, stat and live channels on one V2 WebSocket. Review server-side authentication and entitlements; Ultra or higher.
GET
/
api
/
v2
/
ws
Multiplexed WebSocket: subscribe to markets, scores, plays, stats, and live channels
curl --request GET \
--url https://therundown.io/api/v2/ws \
--header 'X-TheRundown-Key: <api-key>'import requests
url = "https://therundown.io/api/v2/ws"
headers = {"X-TheRundown-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-TheRundown-Key': '<api-key>'}};
fetch('https://therundown.io/api/v2/ws', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://therundown.io/api/v2/ws",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-TheRundown-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://therundown.io/api/v2/ws"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-TheRundown-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://therundown.io/api/v2/ws")
.header("X-TheRundown-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://therundown.io/api/v2/ws")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-TheRundown-Key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyEndpoint details
Establishes a single WebSocket connection carrying multiple logical channels. Instead of query-parameter filters, send JSON subscribe messages after connecting. Connection:wss://therundown.io/api/v2/ws
Authenticate the WebSocket upgrade with X-TheRundown-Key: $THERUNDOWN_API_KEY
from a server-side client. Native browser WebSocket clients cannot set custom
headers; use an authenticated backend relay for browser consumers. WebSocket
access requires an Ultra plan or higher.
Channels:
markets— market price updates (same payloads as/api/v2/ws/markets)scores— score and event-status updates (no live game state fields)plays— play-by-play deltas for live games. Requires an Ultra plan or higher (the live game state entitlement); aplays,stats, orlivesubscribe from a non-entitled key is rejected with error codeforbidden.stats— changed team and player box-score rows, delivered inline withmeta.type=game_stats. Requires an Ultra plan or higher. This is a delta-only channel with no snapshot, replay, or resume; bootstrap and recover from both event game-stat REST endpoints. Each changed stat row is one stats data point.live— score/status deltas includinglive_game_state, play-by-play deltas, and game-stat deltas, on one subscription. Requires an Ultra plan or higher;live_game_stateandgame_stateare accepted aliases.
{
"action": "subscribe",
"id": "p1",
"channel": "plays",
"params": { "sport_ids": [3], "event_ids": ["<eventID>"] }
}
{"type":"subscribed","id":"p1","sequence":N,"message":"subscribed to plays"}.
Supported params filters: sport_ids, event_ids (plus market_ids and
affiliate_ids on the markets channel). stats has no stat, team, or player
filter. Unsubscribe with
{"action":"unsubscribe","id":"p1"}.
Delta messages (server → client) are wrapped in an envelope tagged with your
subscription id:
{
"type": "delta",
"id": "p1",
"sequence": 42,
"sub_sequence": 7,
"delta_last_id": "...",
"data": { "meta": { "type": "play" }, "data": { ... } }
}
meta.type=game_stats; data.team_stats[] and data.player_stats[] contain
only changed rows, and each row’s value remains a JSON string. Upsert those
rows into the REST-bootstrapped box score rather than replacing a whole group.
If both row arrays are absent and complete=true, the frame is the terminal
completion marker; mark the cached box complete and treat repeats as idempotent.
If both arrays are absent without complete=true, the frame is an invalidation
fallback: refetch both GET /api/v2/events/{eventID}/stats and
GET /api/v2/events/{eventID}/players/stats. Row deltas and completion markers
use an RFC 3339 updated_at; the fallback uses numeric Unix seconds. Do not use
delta_last_id as a game-stat replay cursor. Each changed nested row is one
stats data point; either zero-row variant costs one stats data point.
Snapshots: add "snapshot": true to params to receive current state
(snapshot frames, then snapshot_complete) before deltas begin; an active
subscription can request a fresh snapshot at any time with
{"action":"snapshot","id":"p1"}. Snapshot requests need a bounded scope and
are metered as data points like the equivalent REST reads. Snapshots are not
supported on stats; a request returns snapshot_error with code
unsupported_snapshot. A live snapshot does not include a game-stat baseline.
Errors: {"type":"error","id":"...","code":"...","message":"..."} with codes
forbidden (plays/stats/live without Ultra+), invalid_channel, missing_id,
duplicate_id, and subscription_limit (plan’s concurrent subscription cap
reached). A slow multiplexed client may receive the connection-level error code
buffer_overflow immediately before the connection closes with reason
buffer_overflow:reconnect_and_catchup.
Queue and recovery: each non-market subscription has its own 1024-message
outbound queue; market subscriptions are sized separately. If a live frame
cannot be queued, the server closes the connection rather than continue with a
silent gap. Reconnect, re-send subscriptions, and recover current state before
applying new deltas. For stats, refetch both event game-stat REST resources.
Concurrent connection and subscription limits vary by tier. See the
WebSocket reference for full protocol details.Authorizations
Recommended API key request header for new integrations
Response
101
WebSocket upgrade successful