Live product
Live WebSocket
Live is a separate product channel from REST. It streams entitled bookmaker updates over WebSocket after plan, source, sport, and bookmaker checks pass.
Examples use Tipico for illustration only. Tipico ingestion is offline for a source review and is not part of current customer coverage.
Availability and entitlement
Live is fully documented here, but it is not automatically enabled for every account. A connection needs an active API key, an active organization, live_enabled plan access, and a live-enabled bookmaker entitlement for the subscribed channel. If any of those checks fail, the server rejects the connection or subscription instead of falling back to REST.
Connect
import WebSocket from "ws";
const socket = new WebSocket("wss://stream.betspread.io", {
headers: {
Authorization: `Bearer ${process.env.BETSPREAD_API_KEY}`,
},
});
socket.on("open", () => {
socket.send(JSON.stringify({
type: "subscribe",
channel: "odds.football.tipico"
}));
});
socket.on("message", (raw) => {
console.log(JSON.parse(raw.toString()));
});For a complete client with replay, rejection handling and reconnect with backoff, follow the guide Live odds over WebSocket.
Protocol contract
Endpoint
wss://stream.betspread.ioProduction Live WebSocket endpoint.
Authentication
Authorization: Bearer <API_KEY>Live currently authenticates via the WebSocket upgrade headers. Browser clients cannot set this header directly; use a trusted backend client or relay.
Channel
odds.{sport}.{bookmaker}Sport must be a public sport slug and bookmaker must be an entitled source slug, e.g. odds.football.tipico.
Plan gate
plan_limits.live_enabled = trueREST access does not imply Live access. Live must be enabled on the organization plan and bookmaker entitlement.
Replay
replayFromOptional numeric sequence string. Replay is limited by the plan's live_replay_window_seconds (Growth 300 s, Pro 900 s) and needs no separate History product. Older cursors answer replay_window_exceeded.
Connection subscriptions
64 channels by defaultSubscriptions above the connection ceiling are rejected while the WebSocket remains open. Repeating an existing channel without replayFrom is idempotent and does not replay history.
Subscribe and replay
Send a JSON message with type: "subscribe" and a channel. Add replayFrom when the client wants the server to replay recent updates after a known sequence.
{
"type": "subscribe",
"channel": "odds.football.tipico",
"replayFrom": "1779670000000"
}Messages
{
"type": "odds.update",
"sequence": "1779670123456",
"channel": "odds.football.tipico",
"sport": "football",
"bookmaker": "tipico",
"event": {
"id": "143401",
"homeTeam": "SC Paderborn 07",
"awayTeam": "VfL Wolfsburg",
"kickoffAt": "2026-05-25T18:30:00.000Z"
},
"market": {
"type": "match_result_1x2",
"line": null
},
"outcome": "away",
"odds": 2.1,
"status": "open",
"observedAt": "2026-05-25T10:58:20.000Z"
}{
"type": "odds.update",
"sequence": "1779670123999",
"channel": "odds.football.tipico",
"sport": "football",
"bookmaker": "tipico",
"event": {
"id": "143401",
"homeTeam": "SC Paderborn 07",
"awayTeam": "VfL Wolfsburg",
"kickoffAt": "2026-05-25T18:30:00.000Z"
},
"market": {
"type": "match_result_1x2",
"line": null
},
"outcome": "away",
"odds": 2.1,
"status": "suspended",
"observedAt": "2026-05-25T11:02:41.000Z"
}{
"type": "subscription.rejected",
"channel": "odds.football.tipico",
"code": "unauthorized",
"message": "Subscription is not authorized"
}{
"type": "replay.rejected",
"channel": "odds.football.tipico",
"code": "replay_window_exceeded",
"message": "Replay cursor is outside the allowed window"
}Quote status transitions
The live stream carries quote status transitions (open → suspended → closed) in real time. Clients that need suspension awareness must consume the live stream; REST does not expose non-open statuses and always serves status: "open".
Suspension signals depend on what each bookmaker's feed exposes. For sources without status semantics, quotes are emitted as open and age out of REST responses via the freshness window (nine minutes for Bet365, 24 hours for other sources).
Bet365 Live also sends odds.freshness controls and an initial odds.freshness.reset. These have no replay sequence; staleness does not change provider status. Invalidate cached freshness on reset or disconnect and enforce each quote's expiresAt. Historical replay does not establish current freshness. See Quote freshness for recovery semantics.
Failures and close codes
Connection rejected
close code 1008Missing, invalid, expired, revoked, or non-active API key.
Subscription rejected
subscription.rejectedCodes: invalid_message, invalid_channel, api_key_scope_denied, channel_limit_exceeded, subscription_limit_exceeded, or unauthorized.
Replay rejected
replay.rejectedCodes: replay_window_exceeded or replay_limit_exceeded. Restricted keys replay with the Live scope.
Server error
close code 1011Authentication or subscription handling failed server-side.
Replay limit and resync
Replay includes only Live changes for the requested sport and bookmaker. At most 100 eligible changes are returned. If more are pending, the server sends no historical updates and responds with replay.rejected, code replay_limit_exceeded, a human-readable message, and resyncRequired: true. The Live subscription remains active; buffered Live updates newer than the requested cursor are delivered after the rejection. These updates do not fill the historical gap. Discard the incomplete local state and refresh a current snapshot through the separately entitled REST API before considering the state synchronized. Repeating the same replay cursor does not recover an oversized gap.
Usage metering
Live usage is metered separately from REST. Successful subscriptions record ws.subscribe, replayed messages record ws.replay, and delivered fanout updates record ws.update. Rejected subscriptions and replay attempts are still recorded with a rejected status for support and audit visibility.