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

Node.js
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.io

Production 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 = true

REST access does not imply Live access. Live must be enabled on the organization plan and bookmaker entitlement.

Replay

replayFrom

Optional 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 default

Subscriptions 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.

Subscribe with replay
{
  "type": "subscribe",
  "channel": "odds.football.tipico",
  "replayFrom": "1779670000000"
}

Messages

Live update
{
  "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"
}
Live update (suspended)
{
  "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"
}
Subscription rejected
{
  "type": "subscription.rejected",
  "channel": "odds.football.tipico",
  "code": "unauthorized",
  "message": "Subscription is not authorized"
}
Replay rejected
{
  "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 1008

Missing, invalid, expired, revoked, or non-active API key.

Subscription rejected

subscription.rejected

Codes: invalid_message, invalid_channel, api_key_scope_denied, channel_limit_exceeded, subscription_limit_exceeded, or unauthorized.

Replay rejected

replay.rejected

Codes: replay_window_exceeded or replay_limit_exceeded. Restricted keys replay with the Live scope.

Server error

close code 1011

Authentication 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.