Skip to main content
Live Esports Scores and Status over WebSocket
Back to Blog

Live Esports Scores and Status over WebSocket

James Whitfield

James Whitfield

5 min read

Polling a REST endpoint for scores is fine for a results page. For a live betting product it is not: you need to know the moment a score changes or a match goes live. This guide covers streaming live esports scores and match status over WebSocket with the Esports Odds API, and what the messages actually look like.

Connect with the channels you need

The WebSocket lives at wss://api.odds-api.io/v3/ws. Two parameters matter up front: apiKey, and markets, which is required for the odds channel. The channels parameter is an allowlist, so you receive only the channels you list.

wscat -c "wss://api.odds-api.io/v3/ws?apiKey=YOUR_KEY&markets=ML,Spread,Totals&channels=odds,scores,status&sport=esports"

Omit channels entirely and you keep the default odds-only stream. Include scores and status and you get score changes and status transitions on the same socket. Filters help: sport (max 10), leagues (max 20) or eventIds (max 50), and status set to live or prematch. leagues and eventIds cannot be used together, and the sport, leagues, eventIds and status filters apply to score and status messages too.

On connect the server sends a welcome message echoing your active filters, including the channels list, so you can assert your subscription is what you expected before you trust the stream.

The scores channel

A score message arrives when the live score changes. It describes the event itself, so it carries no bookie and no markets:

{
  "type": "score",
  "timestamp": 1723992773,
  "id": "63017989",
  "scores": {
    "home": 2,
    "away": 1,
    "periods": { "map1": { "home": 13, "away": 8 } }
  },
  "clock": { "period": 2, "running": true, "statusDetail": "Map 2" }
}

The optional clock snapshot has the same shape as the clock field on GET /events/live and rides along when clock data is available. It only arrives with an event, so a match you start watching mid-map sends nothing until the next score change. Seed your local ticker with one call to GET /events/live on connect and let the WebSocket re-anchor it after that. For esports, statusDetail reads as "Map 1", "Map 2" and so on.

The status channel

A status message fires on transitions between pending, live, settled and cancelled. It includes the current scores, so a status-only subscriber still gets the final result on settle:

{
  "type": "status",
  "timestamp": 1723992773,
  "id": "63017989",
  "status": "settled",
  "scores": {
    "home": 2,
    "away": 1,
    "periods": {
      "map1": { "home": 13, "away": 8 },
      "map2": { "home": 11, "away": 13 },
      "map3": { "home": 13, "away": 10 }
    }
  }
}

That is the settlement hook: open markets when a match flips to live, settle the moment it flips to settled, and void on cancelled. No polling loop in between.

Period keys for esports

The periods map is keyed by period, and the keys are sport dependent. For esports the per-map scores are map1, map2, map3 and so on, and ft is the full-time or regulation result. Generic period keys p1, p2 through pN cover halves, quarters and sets in other sports, ot covers overtime, and ap is the penalty shootout tally, so neither is what you parse for a best-of-three series. If you are migrating from an older feed, note that the full-time key is ft; the legacy feed used fulltime.

Read the keys you need rather than assuming a fixed set. Which keys appear depends on the title and on how the match resolved.

A minimal client

const WebSocket = require('ws')

const params = new URLSearchParams({
  apiKey: process.env.ODDS_API_KEY,
  markets: 'ML,Spread,Totals',
  channels: 'odds,scores,status',
  sport: 'esports'
})

const ws = new WebSocket(`wss://api.odds-api.io/v3/ws?${params}`)

ws.on('message', (raw) => {
  const msg = JSON.parse(raw)
  switch (msg.type) {
    case 'welcome':
      return console.log('channels:', msg.channels)
    case 'updated':
    case 'created':
      // replace stored markets for this event and bookie, do not merge
      return setMarkets(msg.id, msg.bookie, msg.markets, msg.seq)
    case 'score':
      return renderScore(msg.id, msg.scores, msg.clock)
    case 'status':
      return msg.status === 'settled'
        ? settleMarkets(msg.id, msg.scores)
        : updateStatus(msg.id, msg.status)
  }
})

One rule to internalise on the odds channel: every updated message carries the complete current market set for that event and bookmaker, not just what changed. Overwrite your stored markets. Suspended markets are removed from the payload rather than flagged, so their absence is the signal, and merging leaves suspended markets in your data forever.

Reconnects, replay and resync

Odds messages carry a globally increasing seq. Persist it and reconnect with lastSeq to have the server replay what you missed. Replay is compacted latest-state replay per event and bookmaker, not every intermediate tick. If replay is too large or payloads have expired, the server pushes current state as created messages and continues live on the same connection. Only when complete state cannot be guaranteed do you get resync_required, at which point you discard your stored sequence and rebuild from REST.

The recovery path is: call /odds or /odds/multi with includeSeq=true, read the X-OddsAPI-Seq response header, then reconnect with lastSeq set to that value.

Score and status messages are best-effort live state. They carry no seq and are not replayed on reconnect, so refetch the current value from GET /events after reconnecting rather than assuming your scoreboard survived the gap.

Where to go next

The same connection carries every esports title we cover, so a scoreboard built for CS2 works unchanged for League of Legends, Dota 2 and Valorant. Odds-API.io covers 15 esports titles with odds from 265+ bookmakers, alongside 34 sports and 12,000+ leagues.

If you want the market side of the picture, read esports betting markets explained, or follow the full walkthrough in how to build an esports betting app. The free tier is 100 requests per hour with no card required, so you can wire up the Esports Odds API before you commit to anything.