Skip to main content

Connection

Authentication

Pass your API key via:
  • Query string (recommended for browser clients): ?x-api-key=YOUR_API_KEY
  • Header: x-api-key: YOUR_API_KEY

Sport Selection

Optionally set the x-sport header during connection to scope live-scores to a specific sport (default: football).
x-sport must be sent as a connection header — it is not read from the query string. Passing ?x-sport=... in the URL is ignored and the connection stays scoped to football (the welcome frame echoes the applied sport, so check it). If your client cannot set headers, subscribe to match:{matchId} instead — that channel is sport-agnostic and works for any sport.
Supported values: football, basketball, ice-hockey, tennis, rugby, handball, volleyball, baseball, american-football, cricket, futsal, mma, motorsport, darts, snooker, badminton, table-tennis, beach-volley, cycling, esports, bandy, aussie-rules, floorball, minifootball, waterpolo.

Message Format

All messages are JSON. Send actions to subscribe/unsubscribe from channels:

Available Channels

There is no global or sport-wide odds channel. Odds are per-match only — live-odds, odds:{sport}, and live-odds:{sport} are not valid channels and return an error frame. To follow odds across a sport, subscribe to live-scores (or live-scores:{sport}) to track which matches are live, then fan out one match:{matchId}:odds subscription per match on the same connection. A single socket handles many concurrent match subscriptions.
match:{matchId}:odds is a single-provider relay and is not sub-second. Verified in production:
  • Provider: the channel relays provider 1 (bet365) only — the upstream call is odds/1/all. There is no provider selector, no multi-provider WebSocket stream, and a provider query parameter is ignored. Multi-bookmaker odds are REST-only on the V4 feed (GET /v4/football/match/{matchId}/odds, per-bookmaker offers across a 179-book catalogue).
  • Market set: the same markets as GET /api/match/{matchId}/odds/all — typically 11–24 markets: Full time 1X2 (live and pre-match variants), Double chance, Draw no bet, Both teams to score, Match goals, Asian handicap, Corners 2-way, Cards in match, Next goal. Player props are not available on WebSocket or REST today.
  • Frames are complete snapshots, never deltas. Every snapshot and update carries the full { markets[], eventId, liveStreamUrl } state — replace your local state wholesale.
  • Cadence: ~300ms after a match event (goal, card, period change) via the NATS-triggered fetch; otherwise a change-gated ~30s refresh. Measured inter-frame gaps on live top-tier fixtures: 24s, 27s, 30s, 30s, and 63–149s where prices had not moved. Worst case for a pure price move with no match event is about 30 seconds — treat these odds as reference data, not as a live bet-acceptance gate.
  • timestamp is our server’s fetch/emit time in epoch milliseconds (receipt skew measured at 11–30ms). It is not a bookmaker-side fetch time; the upstream feed does not expose one.
Suspension and closure have no dedicated frame. Each market object carries suspended: true|false, so a suspension arrives as the next full snapshot with that market flagged. A market that disappears from markets[] is closed. Because of the cadence above, a suspension can reach you up to ~30s late unless a match event triggers the fast path.
Missed-update recovery: there is no sequence number, cursor, or replay buffer — and because every frame is a full snapshot, you don’t need one. On reconnect, resubscribe and you immediately receive a snapshot with current state that supersedes anything you missed. Recommended pattern: track lastFrameAt per match and, if it exceeds ~90s while the match is live, drop and re-establish the subscription rather than waiting. WebSocket traffic is unmetered, so aggressive resubscription is free.
WebSocket traffic does not count against your quota. The handshake and every frame you receive — subscriptions, snapshots, and updates — are unmetered, including the internal NATS-triggered fetches behind :odds, :incidents, and :stats. Only REST calls consume quota.

Delivery Modes

  • NATS push — Data is pushed directly from the real-time feed with no delay
  • NATS-triggered fetch — When any match update is detected, detailed data is fetched instantly (no waiting for poll cycle)
  • Polling — Data is fetched at regular intervals (used for lineups which rarely change)

No updates received?

If the connection opens and the welcome / subscribed frames arrive but no update frames follow, work through this list before assuming an outage:
  1. Confirm the match is actually in progress with GET /v2/{sport}/live. This is the authoritative liveness source. The schedule endpoint lists a whole day of fixtures with a status snapshot, so a fixture you read there may already have finished — a finished match emits nothing on match:{matchId} or on live-scores.
  2. Check the sport in the welcome frame. live-scores is scoped to it. Connecting on /v2/{sport}/ws sets the scope automatically; the subscribed frame confirms it as live-scores:{sport}.
  3. Expect quiet gaps. Frames are emitted on change only. Between deliveries, at a wicket break, at half-time or during a rain delay, a genuinely live match can be silent for a minute or more. Test against a sport-wide live-scores subscription rather than one fixture.
  4. Check natsConnected in the welcome frame. If it is false, push channels fall back to their safety-net poll cadence.
  5. De-duplicate on changes.changeTimestamp. A single change can arrive two to three times on match:{matchId}, plus once more on live-scores — that is expected, not a bug.

What’s delivered on :incidents

The match:{matchId}:incidents channel covers every discrete event the live feed emits:
Non-carded fouls are not emitted as incidents. If you need foul counts, subscribe to match:{matchId}:stats — it carries the aggregate per-team foul count, updated in real time. There is no per-player foul stream.
The welcome payload surfaces "natsConnected": true while the NATS bridge is healthy. If it ever reports false, channels fall back to their safety-net poll cadence (15s for :incidents, 30s for :stats) — your code keeps working without changes.

Response Types

welcome

Sent immediately on connection:

subscribed

Confirms a channel subscription:

snapshot

Last known data sent immediately upon subscribing (if available):

update

New data pushed in real-time:

pong

Response to a ping (for keep-alive):

Quick Start Examples

Best Practices

Each subscription consumes server resources. Only subscribe to channels your application actively uses.
Use match:{id}:incidents for real-time match events rather than polling the REST endpoint.
When you subscribe, you immediately get the last known state so your UI is never blank.
Send {"action":"ping","channel":"live-scores"} every 30s to keep the connection alive.
If disconnected, reconnect with exponential backoff (1s, 2s, 4s, 8s…).
Unsubscribe from match channels when the user navigates away.
Last modified on September 20, 2026