WebSockets

Live data is delivered through socket.io rooms bridged to the orchestrator's Redis pub/sub. Subscribe to one or more rooms, receive typed events as they fire. Score changes are pushed as our poller detects them; that poll runs every 15 seconds while a match is live. Requires the Pro plan or higher.

WebSocket subscriptions require the Pro plan or higher. On lower tiers (including Edge) the handshake is refused with a socket.io connect_error reading “Real-time WebSocket access requires the Pro plan or higher.” This is a WebSocket handshake, not an HTTP request, so there is no status code and no plan_required body to inspect — read err.message.

Connecting

socket.io-clienttypescript
import { io } from 'socket.io-client';

const socket = io('https://api.bigballsdata.com', {
  path: '/live',                                  // required — see below
  auth: { apiKey: 'bbs_live_a1b2c3d4e5f6...' },
  transports: ['websocket'],
});

socket.on('connect', () => {
  socket.emit('join', 'sport:football');          // a string, not an object
});

socket.on('connect_error', (err) => {
  // "missing API key" | "invalid API key" | the plan-gate message
  console.error(err.message);
});

path: '/live' is not optional. socket.io-client defaults to /socket.io, which this API does not serve — omit it and the connection fails before authentication is ever reached.

Pass the API key as auth.apiKey, or as an x-api-key header; both work and the header matches REST. Rejection arrives as a socket.io connect_error whose message is one of missing API key, invalid API key, or the plan-gate message — there is no HTTP status code on this path to read.

Room model

Three room scopes; join as many as you need. league: and match: take the same uuids REST returns, not slugs:

RoomScopeExample
sport:<sport>All matches in a sportsport:football
league:<leagueId>All matches in a leagueleague:64184273-6963-…
match:<matchId>A single matchmatch:91056403-2c0d-…

Joining a broader room (sport:*) does not auto-deliver events that would also match a narrower room, each emit hits every room it matches, but the client only receives an event once per connection.

Event types

The event name is the type, and your handler receives the payload directly — there is no { type, data } wrapper around it.

typescripttypescript
socket.on('score_update', (data) => {
  // data: { match_id, sport, league_id, status, period, clock, linescore }
  // linescore is per-period: { home: [0, 1], away: [0, 0] }
});

socket.on('match_start', (data) => {
  // same shape; fired when a match transitions to status 'live'
});

socket.on('match_end', (data) => {
  // same shape; fired when a match transitions to status 'finished'
});

Those three are what the live score feed emits today. The transport also carries odds_move, lineup_confirmed, goal, card and substitution, which are accepted and routed but have no publisher behind them yet — subscribing to them is valid and currently silent. We would rather say that than list them as though they were live.

Unsubscribing

typescripttypescript
socket.emit('leave', 'league:64184273-6963-428d-834a-66a11f169708');
socket.disconnect();

Sockets carry no server-side state across reconnects. After a reconnect, re-issue every join you need. The connect handler is the right place to do this.

Delivery guarantees

  • At-most-once. A momentary disconnect during emit is a missed event, webhooks are the right transport if loss is unacceptable.
  • No ordering guarantee across rooms. Within a room, events arrive in the order the orchestrator emitted them.
  • No replay. There is no backfill of events sent while disconnected. Use the REST endpoints to reconcile state on reconnect.

Quota

Nothing on this transport counts against the request quota — not received events, and not join / leave either. The rate limiter sits on the HTTP path and this one never reaches it.