Skip to main content

Build a live scoreboard

This guide builds a full scoreboard for a single match in a single page, combining a REST snapshot with WebSocket push updates.

The pattern works for any UI framework — we'll use vanilla JavaScript so the technique is the focus, not the tooling.

The two-phase pattern

Every live scoreboard does the same two things:

  1. Initial REST snapshot to render immediately.
  2. WebSocket subscription for incremental updates.

Don't try to bootstrap a scoreboard from WebSocket alone — you might join mid-update and miss the current state.

1. Render the snapshot

<div id="scoreboard">
<span class="home"></span>
<span class="score"></span>
<span class="away"></span>
<span class="status"></span>
</div>
const API_KEY = 'sk_live_...';
const MATCH_ID = 'abc123';

async function loadSnapshot() {
const res = await fetch(
`https://api.scorelytics.pro/v1/football/matches/${MATCH_ID}`,
{ headers: { 'X-API-Key': API_KEY } }
);
if (!res.ok) throw new Error(`Snapshot ${res.status}`);
return res.json();
}

function render(m) {
document.querySelector('.home').textContent = m.home_team;
document.querySelector('.away').textContent = m.away_team;
document.querySelector('.score').textContent =
`${m.home_score} - ${m.away_score}`;
document.querySelector('.status').textContent = formatStatus(m);
}

function formatStatus(m) {
if (m.status_code === 1) return new Date(m.kickoff_ts * 1000).toLocaleTimeString();
if (m.status_code === 3) return 'FT';
if (m.status_code === 2) return `${m.minute}'`;
return '';
}

let state = await loadSnapshot();
render(state);

2. Subscribe to WebSocket updates

const ws = new WebSocket(
`wss://api.scorelytics.pro/v1/ws/match/${MATCH_ID}?api_key=${API_KEY}`
);

ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'score_update') {
state.home_score = msg.data.home;
state.away_score = msg.data.away;
state.minute = msg.data.minute;
} else if (msg.type === 'status_change') {
state.status_code = msg.data.to;
}
render(state);
};

ws.onerror = (e) => console.error('WS error', e);
ws.onclose = () => {
// simple reconnect with backoff
setTimeout(connect, 3_000);
};

That's a working scoreboard.

3. Handle reconnections

WebSocket connections drop. When yours does, you must:

  1. Reconnect with backoff.
  2. Re-fetch the REST snapshot before re-rendering — the score may have changed while you were offline.
let reconnectDelay = 1_000;

async function connect() {
state = await loadSnapshot(); // catch up
render(state);
reconnectDelay = 1_000; // reset on success

const ws = new WebSocket(/* … */);
ws.onmessage = handleMessage;
ws.onclose = () => {
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
};
}
connect();

4. Interpolate the minute (optional)

The minute field updates whenever the server polls (every 5 s). To make the displayed minute increment smoothly between updates, recompute it client-side:

let lastMinuteAt = Date.now();
let lastMinute = state.minute;

setInterval(() => {
if (state.status_code !== 2) return;
const drift = Math.floor((Date.now() - lastMinuteAt) / 60_000);
document.querySelector('.status').textContent =
`${lastMinute + drift}'`;
}, 1_000);

// reset whenever a new minute arrives over WS
function setMinute(m) {
lastMinute = m;
lastMinuteAt = Date.now();
}

For basketball, swap the minute interpolation for the game-clock interpolation using game_clock_secs.

5. Show events as they happen

To display the goals/cards timeline, fetch events on first render and re-fetch on relevant status_change events:

async function loadEvents() {
const res = await fetch(
`https://api.scorelytics.pro/v1/football/matches/${MATCH_ID}/events`,
{ headers: { 'X-API-Key': API_KEY } }
);
return (await res.json()).events;
}

The WebSocket stream emits score_update for goals — when that arrives, re-call loadEvents() to pick up the new entry.

Production checklist

  • Reconnection with exponential backoff (cap at ~30 s).
  • Always re-snapshot after a reconnect.
  • Handle status_change → 3 (FT) by closing the WebSocket.
  • Show a "stale data" indicator if updated_at is older than 30 s.
  • Don't put the API key in client JS in production — proxy the WebSocket via your backend.

What's next?