DEV Community

orbistats
orbistats

Posted on

Building a Live Scoreboard with a Sports Data API (Vanilla JS + WebSocket)

In this tutorial we'll build a working live scoreboard — the kind you'd embed on a sports site or a fan-engagement app — using Orbistats as the data source. We'll cover the initial fixture fetch, the WebSocket connection for live updates, and basic UI rendering. No framework required; this works as a drop-in you can adapt into React/Vue later if you want.</p> <p>What We&#39;re Building</p> <p>A scoreboard that:</p> <p>Loads today&#39;s fixtures on page load (REST)<br> Connects to a live feed and updates scores in real time (WebSocket)<br> Shows a pulsing &quot;LIVE&quot; badge on in-progress matches<br> Falls back gracefully if the socket drops<br> Step 1: Get an API Key (or Skip This Entirely)</p> <p>You can test everything in this tutorial without signing up at all, using Orbistats&#39; public sandbox — pick an endpoint, run it, see the JSON. When you&#39;re ready to wire it into a real app, grab a free-tier key from the pricing page (150 requests/day, all endpoints, no card required).</p> <p>Step 2: Fetch Today&#39;s Fixtures<br> javascript<br> const API_KEY = &quot;YOUR_API_KEY&quot;;<br> const BASE_URL = &quot;<a href="https://api.orbistats.com/v1">https://api.orbistats.com/v1</a>&quot;;</p> <p>async function getFixtures() {<br> const res = await fetch(<code>${BASE_URL}/football/fixtures?date=today</code>, {<br> headers: {<br> Authorization: <code>Bearer ${API_KEY}</code>,<br> },<br> });</p> <p>if (!res.ok) {<br> throw new Error(<code>Fixtures request failed: ${res.status}</code>);<br> }</p> <p>return res.json();<br> }</p> <p>Response shape follows the standard pattern documented on the Sports Data API page — same structure you&#39;d get for basketball or any other sport, just swap the path segment.</p> <p>Step 3: Render the Initial Board<br> javascript<br> function renderScoreboard(fixtures) {<br> const container = document.getElementById(&quot;scoreboard&quot;);<br> container.innerHTML = &quot;&quot;;</p> <p>fixtures.forEach((match) =&gt; {<br> const card = document.createElement(&quot;div&quot;);<br> card.className = &quot;match-card&quot;;<br> card.dataset.matchId = match.id;<br> card.innerHTML = <code><br> &lt;div class=&quot;teams&quot;&gt;<br> &lt;span class=&quot;home&quot;&gt;${match.home.name}&lt;/span&gt;<br> &lt;span class=&quot;score&quot;&gt;${match.home.score ?? &quot;-&quot;} : ${match.away.score ?? &quot;-&quot;}&lt;/span&gt;<br> &lt;span class=&quot;away&quot;&gt;${match.away.name}&lt;/span&gt;<br> &lt;/div&gt;<br> &lt;div class=&quot;status ${match.status === &quot;live&quot; ? &quot;live&quot; : &quot;&quot;}&quot;&gt;<br> ${match.status === &quot;live&quot; ?</code>🔴 LIVE ${match.minute}&#39;<code>: match.status}<br> &lt;/div&gt;<br> </code>;<br> container.appendChild(card);<br> });<br> }<br> Step 4: Connect to the Live Feed</p> <p>This is the part that separates a real scoreboard from a static page that refreshes every 30 seconds. Instead of polling, we open a persistent WebSocket connection:</p> <p>javascript<br> function connectLiveFeed(onUpdate) {<br> const ws = new WebSocket(<code>wss://api.orbistats.com/v1/live?token=${API_KEY}</code>);</p> <p>ws.onopen = () =&gt; console.log(&quot;Live feed connected&quot;);</p> <p>ws.onmessage = (event) =&gt; {<br> const update = JSON.parse(event.data);<br> onUpdate(update);<br> };</p> <p>ws.onerror = (err) =&gt; console.error(&quot;WebSocket error:&quot;, err);</p> <p>ws.onclose = () =&gt; {<br> console.warn(&quot;Live feed disconnected, retrying in 3s...&quot;);<br> setTimeout(() =&gt; connectLiveFeed(onUpdate), 3000);<br> };</p> <p>return ws;<br> }</p> <p>That onclose retry is easy to forget but matters a lot in production — sockets drop on tab backgrounding, network hiccups, and server restarts, and a scoreboard that silently goes stale is worse than one that never went live at all.</p> <p>Step 5: Wire the Update Handler<br> javascript<br> function handleLiveUpdate(update) {<br> const card = document.querySelector(<code>[data-match-id=&quot;${update.match_id}&quot;]</code>);<br> if (!card) return;</p> <p>const scoreEl = card.querySelector(&quot;.score&quot;);<br> scoreEl.textContent = <code>${update.home.score} : ${update.away.score}</code>;</p> <p>const statusEl = card.querySelector(&quot;.status&quot;);<br> if (update.status === &quot;live&quot;) {<br> statusEl.className = &quot;status live&quot;;<br> statusEl.textContent = <code>🔴 LIVE ${update.minute}&#39;</code>;<br> } else if (update.status === &quot;finished&quot;) {<br> statusEl.className = &quot;status&quot;;<br> statusEl.textContent = &quot;FT&quot;;<br> }<br> }</p> <p>The payload here follows the shape documented on the Live Scores API page — match_id, status, minute, and per-team score, which is exactly what we need to patch the DOM without re-rendering the whole board.</p> <p>Step 6: Put It Together<br> javascript<br> async function initScoreboard() {<br> const fixtures = await getFixtures();<br> renderScoreboard(fixtures);<br> connectLiveFeed(handleLiveUpdate);<br> }</p> <p>initScoreboard();<br> Step 7: Basic CSS<br> css<br> .match-card {<br> display: flex;<br> flex-direction: column;<br> gap: 6px;<br> padding: 12px 16px;<br> border-radius: 8px;<br> background: #12151c;<br> color: #f5f5f5;<br> margin-bottom: 8px;<br> font-family: system-ui, sans-serif;<br> }</p> <p>.teams {<br> display: flex;<br> justify-content: space-between;<br> align-items: center;<br> font-weight: 600;<br> }</p> <p>.score {<br> font-size: 1.2rem;<br> color: #ff7a00;<br> }</p> <p>.status {<br> font-size: 0.85rem;<br> color: #999;<br> }</p> <p>.status.live {<br> color: #ff4444;<br> animation: pulse 1.5s infinite;<br> }</p> <p>@keyframes pulse {<br> 0%, 100% { opacity: 1; }<br> 50% { opacity: 0.4; }<br> }<br> What to Add Next</p> <p>A few directions worth taking this further:</p> <p>Fallback to polling if WebSocket isn&#39;t available in the environment — check the Webhooks API as an alternative delivery method if you&#39;d rather receive server-side push events instead of holding a client-side socket open.<br> Odds column — if you want to show live betting lines alongside scores, the Odds API returns normalized odds you can slot into another column on each card.<br> Multi-sport support — since the schema is consistent across sports, swapping /football/ for /basketball/ in Steps 2 and 4 is close to the entire change needed.<br> Skip building the UI at all — if you just need something embeddable fast, Orbistats also ships pre-built widgets that do this out of the box.</p> <p>Full endpoint reference and more language examples (Python, PHP, Go, etc.) are in the API reference and SDK examples if you want to port this to a backend service instead of client-side JS.</p>

Top comments (1)

Collapse
 
khawaja_khurrammak_6ad profile image
Khawaja Khurram (MAK)

Nice work done, looking forward to work on project like this where you get live data and manipulate it :)