Blog Docs Markets Portal Sign up
HomeBlog › Sportsbook widget in 50 lines

How to Build a Sportsbook Widget in 50 Lines of JavaScript

Tutorial 21 May 2026 · 8 min read · Vanilla JS · No build step

The cheapest way to add live sports content to your site isn't a 12 MB sportsbook frontend — it's a small widget that fetches data and renders it. This post walks through the entire thing in about 50 lines of vanilla JavaScript, no React, no Webpack, no service workers. It runs on a static HTML page.

By the end, you'll have a live-updating list of soccer matches with prematch odds (1x2, total goals, both teams to score) refreshing every 10 seconds. The same pattern works for any of the 25 sports in the Euro365 catalog.

What you'll build

1. Get a free API key (60 seconds)

Sign up at the developer portal. You'll get an API key starting with ek_ in your inbox immediately after email verification. The free tier covers 100 requests/minute — enough to build, test, and run this widget on a small site without ever upgrading.

Domain whitelist: in the portal, add the domain you'll embed the widget on (e.g., example.com). This locks your key to your own origin so a leaked key can't be reused elsewhere. Optional but recommended before going live.

2. The HTML scaffold

Start with the simplest possible page:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live Soccer Matches</title>
<style>
  body{font-family:system-ui,sans-serif;background:#0a0f1a;color:#e6edf3;padding:24px}
  .match{padding:14px;border-bottom:1px solid #1c2433;display:grid;grid-template-columns:1fr auto;gap:12px}
  .teams{font-weight:600}
  .meta{color:#8b949e;font-size:13px;margin-top:4px}
  .odds{display:flex;gap:6px;font-family:'SF Mono',monospace;font-size:13px}
  .odd{background:#0c1622;border:1px solid #1c2433;padding:4px 10px;border-radius:4px}
</style>
</head>
<body>
<h2>Soccer — upcoming matches</h2>
<div id="matches">Loading…</div>
<script>
const KEY = 'ek_your_key_here';
const API = 'https://api.euro365.bet';
// …code below…
</script>
</body>
</html>

3. Fetch the prematch list

The /v1/prematch endpoint returns the upcoming events for a given sport. Soccer is sport ID 1 — see /v1/sports for the full list.

async function fetchMatches() {
  const res = await fetch(`${API}/v1/prematch?sport=1&limit=10`, {
    headers: { 'x-api-key': KEY }
  });
  if (!res.ok) throw new Error('API ' + res.status);
  const json = await res.json();
  return Object.entries(json.data); // [[eventId, eventData], …]
}

Each entry looks like this (truncated):

{
  "s5-18938270": {
    "h": "Ferro Carril Oeste",
    "a": "CA Central Norte",
    "t": [93703, "Primera B Nacional", 7],
    "c": [32, "Argentina", 69, "ar"],
    "ts": 1779649200,
    "map": { "s5_p18938270": {"p": 0} }
  }
}

The fields you care about are h (home team), a (away team), t[1] (tournament), c[1] (country), ts (Unix timestamp), and map (the keys to fetch live-priced outcomes with).

4. Render the rows

function renderMatches(matches) {
  const container = document.getElementById('matches');
  container.innerHTML = matches.map(([id, m]) => {
    const kickoff = new Date(m.ts * 1000).toLocaleString();
    return `
      <div class="match" data-id="${id}">
        <div>
          <div class="teams">${m.h} vs ${m.a}</div>
          <div class="meta">${m.c[1]} · ${m.t[1]} · ${kickoff}</div>
        </div>
        <div class="odds" id="odds-${id}">…</div>
      </div>`;
  }).join('');
}

5. Fetch live odds for each match

Each event's map object lists which odds keys to fetch. Pass them comma-separated to /v1/odds?ids=…:

async function fetchOdds(matchIds) {
  const res = await fetch(`${API}/v1/odds?ids=${matchIds.join(',')}`, {
    headers: { 'x-api-key': KEY }
  });
  return (await res.json()).data;
}

function renderOdds(matchId, oddsData) {
  const slot = document.getElementById(`odds-${matchId}`);
  if (!slot) return;
  // Pull the three 1x2 outcomes if present
  const o = oddsData[Object.keys(oddsData)[0]] || {};
  const ones = ['2001','2002','2003']  // home / draw / away outcome IDs
    .map(k => o[k] ? `<span class="odd">${(o[k]/1000).toFixed(2)}</span>` : '')
    .join('');
  slot.innerHTML = ones || '<span class="odd">—</span>';
}
Why divide by 1000? Euro365 ships odds as scaled integers to avoid floating-point drift across hops. A price of 1850 means 1.85. This is the same convention used by every aggregation feed we know of.

6. Refresh on a loop

async function tick() {
  try {
    const matches = await fetchMatches();
    renderMatches(matches);
    const mapKeys = matches.flatMap(([, m]) => Object.keys(m.map || {}));
    if (mapKeys.length) {
      const odds = await fetchOdds(mapKeys);
      matches.forEach(([id]) => renderOdds(id, odds));
    }
  } catch (e) {
    document.getElementById('matches').textContent = 'Could not load: ' + e.message;
  }
}
tick();
setInterval(tick, 10000); // refresh every 10s

That's the whole widget. Count the JS lines — it's right at 50 including the helper functions. Drop it on any static page, run a local server (python3 -m http.server), and you'll see the list live-updating.

7. Polish: style, error handling, deploy

Cache responses to stay under the rate limit

A 10-second refresh on the prematch list is overkill — the list changes once or twice per minute. Pull the list every 60 seconds and the odds every 10:

setInterval(refreshList, 60000);   // prematch list: 1× per minute
setInterval(refreshOdds, 10000);   // odds: 6× per minute
// Total: ~7 req/min — well under 100/min free tier.

Switch to WebSocket for actual real-time

For live (in-play) matches, polling is wasteful. Open a WebSocket connection to wss://api.euro365.bet/ws?api_key=ek_… and you'll get pushed updates the moment odds change. We'll cover this in a follow-up post.

Use the prebuilt widget if you don't want to code

If you just need a live match tracker on your page, skip the JS entirely and embed our drop-in iframe widget. Same data, one line of HTML. See the widget demo ›

Deploy: pin the key to your domain

Before deploying to production:

  1. In the portal, add your production domain to the Domain whitelist on the key.
  2. Remove any local-dev domains so the key only works from your real site.
  3. Watch the Usage tab to confirm requests are flowing.

What you just built, in numbers

The same code pattern works for tennis (sport=5), basketball (sport=2), or any of the 25 sports we cover. Swap the sport ID, optionally swap the outcome IDs for the markets you care about (see soccer main markets for the full list of IDs), and you're set.

Get a free API key Read the full API docs Subscribe via RapidAPI