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:
- Initial REST snapshot to render immediately.
- 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:
- Reconnect with backoff.
- 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_atis older than 30 s. - Don't put the API key in client JS in production — proxy the WebSocket via your backend.
What's next?
- WebSocket reference — full message format.
- Timestamps — clock interpolation in detail.
- Pagination — for multi-match dashboards.