DEV Community

Cover image for Sports APIs Explained: How to Get Live Match Data (A Beginner's Guide With Code)
orbistats
orbistats

Posted on

Sports APIs Explained: How to Get Live Match Data (A Beginner's Guide With Code)

You have used live-score apps a hundred times. The number changes, the minute ticks, a goal appears on the timeline. Today you are going to build one.

This guide assumes no prior API experience. If you can run a command in a terminal and read a few lines of Python or JavaScript, you can follow along. By the end you will understand what a sports API is, make your first request, handle errors properly, and have a working live match tracker running on your own machine.

What you will learn:

What a sports API is, in plain English
The five types of sports data and which one you need
How to get live match data with curl, Python and JavaScript
How to handle errors, rate limits and retries
How to build a small live tracker with caching
When to move from polling to WebSocket and webhooks
How developers and businesses actually use this data

What you need: Node.js 18+ (for the project), Python 3.9+ (optional), and a free API key.

  1. What is a sports API?

An API (Application Programming Interface) is a way for your code to ask another system for data. A sports API is that doorway for sports information: you send a request, you get structured data back.

Think of a restaurant. You (the app) tell the waiter (the API) what you want, and the waiter brings it from the kitchen (the provider's database). You never walk into the kitchen.

Here is a real-looking request:

GET /v1/football/live

And a response in JSON, the standard text format for API data:

json
{
"match_id": "match_50231",
"status": "live",
"minute": 72,
"home": { "name": "Manchester City", "score": 2 },
"away": { "name": "Arsenal", "score": 1 }
}

That response shape follows the live-score example on the Orbistats Live Scores API page. Everything in this guide builds on that one idea: ask, receive JSON, display.

  1. Beginner vocabulary (read this once, it saves hours) Term Meaning Endpoint A specific URL that returns specific data, like /v1/football/live Base URL The start of every endpoint, like https://api.orbistats.com/v1 API key A secret string that identifies you. Never share it or commit it to Git Header Extra info sent with a request, like your key in Authorization JSON The text format most APIs use to send data Status code A number telling you what happened: 200 ok, 401 bad key, 429 too many requests Rate limit How many requests your plan allows in a time window Polling Asking again and again on a timer WebSocket A live connection where the server pushes updates to you Webhook The server calls your URL when an event happens Latency The delay between a real-world event and you seeing it

If any sports term is new to you (xG, implied probability, and so on), the glossary explains them simply.

  1. The five types of sports data

"Sports API" does not only mean live scores. There are five distinct data types, and buying only what you need saves money.

Fixtures, results and standings. Schedules, final scores, league tables, teams. See the Sports Data API.
Live scores and events. What is happening right now: goals, cards, substitutions. See the Live Scores API.
Statistics. Team and player numbers like possession, shots, assists and xG. See the Sports Statistics API.
Odds. Bookmaker prices across markets, pre-match and live. See the Odds API. Odds are informational data, not predictions or advice.
Historical data. Past seasons, for models and head-to-head pages. See the Historical Sports Data API.

Beginner tip: for this tutorial you only need type 2, live scores. Start small.

  1. Which sports can you get?

Orbistats currently lists 13 sports, each with its own coverage page:

Football
Basketball
American Football
Cricket
Tennis
Baseball
Esports
Combat Sports
Volleyball
Handball
Ice Hockey
Golf
Horse Racing

The URL pattern is sport, then resource:

/v1/football/live
/v1/basketball/live
/v1/tennis/live

Switching sports is usually a one-word change. But the meaning of fields differs. A football match has minutes, a tennis match has sets and a golf event has a leaderboard. Always read the sport's page before assuming a field exists.

  1. Four ways to get live match data (and why most devs choose one)

Before writing code, here is the honest comparison of your options.

Option 1: Scraping a website
Pros: free, quick to hack together.
Cons: breaks when the page layout changes, slow, may violate the site's terms, no uptime guarantee.
Verdict: fine for a weekend experiment, risky for anything users rely on.
Option 2: Licensing an official league feed
Pros: authoritative and accurate.
Cons: expensive, one contract per competition, a different format for each.
Verdict: suits large broadcasters, rarely startups or hobbyists.
Option 3: Collecting the data yourself
Pros: total control.
Cons: you need people at matches, tooling, verification and 24/7 coverage.
Verdict: almost never worth it unless data is your core business.
Option 4: A sports data API
Pros: fastest to launch, one schema across sports, docs and support, predictable cost.
Cons: you depend on the provider's coverage, speed and terms.
Verdict: the default for almost every app. This is what this guide uses.

To compare providers yourself, start with the comparison and alternatives pages. Some providers focus on odds only, some are enterprise suites and some cover many sports under one API.

  1. REST vs WebSocket vs webhooks

Three ways data can reach you. This confuses most beginners, so here it is side by side:

REST    WebSocket   Webhooks
Enter fullscreen mode Exit fullscreen mode

Who starts it? You ask Connection stays open, server pushes Server calls your URL
Best for Fixtures, standings, stats Live scoreboards Alerts, automation
Complexity Low Medium Medium
Wasteful if live? Yes, if you poll No No

REST for pages. WebSocket for live screens. Webhooks for alerts.

Read more in the WebSocket API and Webhooks API docs. If you want a score display with almost no code, ready-made widgets can be embedded directly.

We will start with REST, because it is the simplest, and add WebSocket later.

Step 1: Get your free API key
Go to the sign-up page and create a free account (no card needed to start).
Copy your API key.
Store it as an environment variable, never in your code.

On macOS or Linux:

bash
export ORBISTATS_API_KEY="your_key_here"

On Windows PowerShell:

powershell
$env:ORBISTATS_API_KEY = "your_key_here"

Want to see real responses before touching code? Open the public sandbox. The quickstart and documentation cover authentication in more detail.

Step 2: Your first request with curl

curl is the fastest way to test an API, with no code needed:

bash
curl -s \
-H "Authorization: Bearer $ORBISTATS_API_KEY" \
"https://api.orbistats.com/v1/football/live"

If you have jq installed, pretty-print it:

bash
curl -s -H "Authorization: Bearer $ORBISTATS_API_KEY" \
"https://api.orbistats.com/v1/football/live" | jq

Three things happened: you sent a GET request, your key travelled in an Authorization header, and you got JSON back. If you got an error instead, jump to Step 5.

Field names can differ by endpoint, so keep the API reference open in another tab.

Step 3: The same request in Python
bash
pip install requests python-dotenv

Create a .env file (and add it to .gitignore):

ORBISTATS_API_KEY=your_key_here
python
import os
import requests
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("ORBISTATS_API_KEY")
BASE_URL = "https://api.orbistats.com/v1"

def get_live(sport="football"):
res = requests.get(
f"{BASE_URL}/{sport}/live",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
res.raise_for_status()
return res.json().get("data", [])

for m in get_live("football"):
print(f'{m["minute"]}\' {m["home"]["name"]} {m["home"]["score"]}'
f' - {m["away"]["score"]} {m["away"]["name"]}')

Notice the habits that matter: timeout=10 stops your script hanging forever, and raise_for_status() turns hidden failures into visible errors.

Step 4: The same request in JavaScript

Node.js 18+ has fetch built in:

javascript
const API_KEY = process.env.ORBISTATS_API_KEY;
const BASE_URL = "https://api.orbistats.com/v1";

async function getJson(path) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const res = await fetch(${BASE_URL}/${path}, {
headers: { Authorization: Bearer ${API_KEY} },
signal: controller.signal,
});
if (!res.ok) throw new Error(HTTP ${res.status});
return await res.json();
} finally {
clearTimeout(timer);
}
}

getJson("football/live")
.then((json) => {
for (const m of json.data ?? []) {
console.log(${m.minute}' ${m.home.name} ${m.home.score} - ${m.away.score} ${m.away.name});
}
})
.catch((err) => console.error("Request failed:", err.message));

Prefer a library to raw HTTP? Check the SDKs and the examples page.

Step 5: Handle errors like a professional

Beginners write code for the day everything works. Professionals write code for the day it does not. Learn these status codes:

Code Meaning What to do
200 Success Use the data
400 Bad request Check your path and parameters
401 Bad or missing key Check your key and header
403 Not allowed on your plan Check your plan or endpoint
404 Not found Check the sport or resource name
429 Too many requests Slow down, wait, retry
5xx Server problem Retry with backoff

Here is a Python helper that retries sensibly:

python
import os
import time
import requests
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("ORBISTATS_API_KEY")
BASE_URL = "https://api.orbistats.com/v1"

class ApiError(Exception):
pass

def retry_delay(res, attempt):
value = res.headers.get("Retry-After", "")
return int(value) if value.isdigit() else 2 ** attempt

def get_json(path, retries=3):
url = f"{BASE_URL}/{path.lstrip('/')}"
headers = {"Authorization": f"Bearer {API_KEY}"}

for attempt in range(retries):
    try:
        res = requests.get(url, headers=headers, timeout=10)
    except requests.RequestException:
        time.sleep(2 ** attempt)
        continue

    if res.status_code in (401, 403):
        raise ApiError("Auth problem: check your key and plan")
    if res.status_code == 429 or res.status_code >= 500:
        time.sleep(retry_delay(res, attempt))
        continue

    res.raise_for_status()
    return res.json()

raise ApiError("Gave up after retries")
Enter fullscreen mode Exit fullscreen mode

print(get_json("football/live"))

The key ideas: never retry 401 or 403 (retrying will not fix a bad key), do retry 429 and 5xx with a growing wait, and respect Retry-After when the server sends it. Check the changelog and status page when something looks wrong on the provider's side.

Step 6: Build a live match tracker

Time for the real project. We will build a tiny Express server that talks to the API and a plain HTML page that shows matches. Why a server in the middle?

Your API key stays secret. If you call the API from browser JavaScript, anyone can steal your key from the page.
One shared cache. If 1,000 visitors load your page, your server can still make just one upstream request every 15 seconds.
A single place to fix things when the API changes.
Project setup
bash
mkdir live-tracker && cd live-tracker
npm init -y
npm pkg set type=module
npm install express
mkdir public
server.js
javascript
import express from "express";

const app = express();

const KEY = process.env.ORBISTATS_API_KEY;
const BASE = "https://api.orbistats.com/v1";
const USE_MOCK = process.env.USE_MOCK === "1" || !KEY; // works without a key

const SPORTS = new Set([
"football", "basketball", "american-football", "cricket", "tennis",
"baseball", "esports", "combat-sports", "volleyball", "handball",
"ice-hockey", "golf", "horse-racing",
]);

// Fake data so you can build the UI before you have a key
const MOCK = {
football: [
{
match_id: "mock_1",
status: "live",
minute: 72,
home: { name: "Blue Rovers", score: 2 },
away: { name: "Red City", score: 1 },
},
],
};

// Tiny in-memory cache
const cache = new Map();
async function cached(key, ttlMs, loader) {
const hit = cache.get(key);
if (hit && Date.now() - hit.fetchedAt < ttlMs) return hit;
const entry = { value: await loader(), fetchedAt: Date.now() };
cache.set(key, entry);
return entry;
}

async function loadLive(sport) {
if (USE_MOCK) return MOCK[sport] ?? [];
const res = await fetch(${BASE}/${sport}/live, {
headers: { Authorization: Bearer ${KEY} },
});
if (!res.ok) throw new Error(Upstream returned ${res.status});
const json = await res.json();
return json.data ?? [];
}

app.get("/api/live/:sport", async (req, res) => {
const { sport } = req.params;
if (!SPORTS.has(sport)) {
return res.status(400).json({ error: "Unknown sport" });
}
try {
const { value, fetchedAt } = await cached(live:${sport}, 15_000, () =>
loadLive(sport)
);
res.json({
sport,
mock: USE_MOCK,
fetchedAt: new Date(fetchedAt).toISOString(),
data: value,
});
} catch (err) {
console.error(err.message);
res.status(502).json({ error: "Could not load live data" });
}
});

app.use(express.static("public"));
app.listen(3000, () => console.log("Open http://localhost:3000"));

Three details worth studying:

Validation. We only accept sports from a fixed list, so nobody can make your server request random URLs.
fetchedAt. We return when the data was fetched, not when the response was sent, so the UI can show honest freshness.
Mock mode. With no key set, the server uses fake data, so the whole project runs on day one. In production you would also add a short cache for failures so errors do not hammer the upstream API.
public/index.html
html
<!doctype html>




Live Match Tracker
<br> body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }<br> select { padding: .4rem; text-transform: capitalize; }<br> .card { border: 1px solid #ddd; border-radius: 8px; padding: .75rem 1rem;<br> margin: .5rem 0; display: flex; justify-content: space-between; }<br> .clock { color: #c00; font-weight: 600; }<br> small { color: #666; }<br>


Live Matches




<br> const SPORTS = [&quot;football&quot;, &quot;basketball&quot;, &quot;american-football&quot;, &quot;cricket&quot;,<br> &quot;tennis&quot;, &quot;baseball&quot;, &quot;esports&quot;, &quot;combat-sports&quot;, &quot;volleyball&quot;,<br> &quot;handball&quot;, &quot;ice-hockey&quot;, &quot;golf&quot;, &quot;horse-racing&quot;];</p> <div class="highlight"><pre class="highlight plaintext"><code>const select = document.getElementById("sport"); const list = document.getElementById("list"); const stamp = document.getElementById("stamp"); SPORTS.forEach((s) =&gt; { const opt = document.createElement("option"); opt.value = s; opt.textContent = s.replace(/-/g, " "); select.appendChild(opt); }); function card(m) { const el = document.createElement("div"); el.className = "card"; const teams = document.createElement("span"); teams.textContent = `${m.home?.name ?? "?"} ${m.home?.score ?? 0} - ` + `${m.away?.score ?? 0} ${m.away?.name ?? "?"}`; const clock = document.createElement("span"); clock.className = "clock"; clock.textContent = m.status === "live" &amp;&amp; m.minute ? `${m.minute}'` : (m.status ?? ""); el.append(teams, clock); return el; } async function refresh() { try { const res = await fetch(`/api/live/${select.value}`); if (!res.ok) throw new Error(res.status); const { data, fetchedAt, mock } = await res.json(); if (data.length) { list.replaceChildren(...data.map(card)); } else { const p = document.createElement("p"); p.textContent = "No live matches right now."; list.replaceChildren(p); } stamp.textContent = `Data from ${new Date(fetchedAt).toLocaleTimeString()}` + (mock ? " (mock data)" : ""); } catch { stamp.textContent = "Couldn't refresh. Retrying..."; } } select.addEventListener("change", refresh); refresh(); setInterval(refresh, 15000); </code></pre></div> <p>

Notice we use textContent and not innerHTML. If an API ever returned a weird team name containing HTML, textContent shows it as plain text instead of running it. That one habit prevents a whole class of security bugs.

Run it
bash

Without a key (mock data):

node server.js

With your real key:

ORBISTATS_API_KEY="your_key_here" node server.js

Open http://localhost:3000. You now have a working live tracker.

Honest limitation: this card layout works for football-style scores (home vs away). Cricket, tennis and golf have different shapes. When you add them, write a small adapter function per sport that converts each payload into one internal card shape, so your UI never needs to know which sport it is showing.

Step 7: Caching and rate limits (the math nobody tells you)

Free plans have request limits. At the time of writing, Orbistats documents roughly 150 requests per day on the free tier, but always check the live pricing page for current limits.

Now do the maths. If you poll all 13 sports once a minute:

13 sports × 1,440 minutes = 18,720 requests per day

That would exhaust a small allowance in minutes. Fixes:

Cache on your server (we did, 15 seconds).
Only poll what users are looking at.
Poll fast only near match time. Use fixtures to know when matches start.
Use WebSocket for genuinely live screens.

Suggested cache times:

Data Cache for
Live scores None (use WebSocket) or 10 to 20 seconds
Fixtures 1 to 5 minutes
Standings 5 to 15 minutes
Team and player stats Minutes to hours
Historical data Hours or days
Step 8: Go real-time with WebSocket

Polling means your data is always a little stale and you spend requests on "nothing changed" answers. A WebSocket flips it: the server tells you when something changes.

The golden pattern is snapshot first, then updates:

javascript
import WebSocket from "ws"; // npm install ws

const API_KEY = process.env.ORBISTATS_API_KEY;
const WS_URL = "PASTE_URL_FROM_WEBSOCKET_DOCS"; // copy it from the docs

const matches = new Map();
let attempts = 0;

function merge(update) {
const prev = matches.get(update.match_id) || {};
const next = {
...prev,
...update,
home: { ...prev.home, ...update.home },
away: { ...prev.away, ...update.away },
};
matches.set(update.match_id, next);
return next;
}

function connect() {
const ws = new WebSocket(WS_URL, {
headers: { Authorization: Bearer ${API_KEY} },
});

ws.on("open", () => {
attempts = 0;
// Refetch a REST snapshot here to catch anything missed
});

ws.on("message", (raw) => {
const msg = JSON.parse(raw);
const items = Array.isArray(msg.data) ? msg.data : [msg.data ?? msg];
items.forEach((u) => console.log("update:", merge(u)));
});

ws.on("close", () => {
const delay = Math.min(30000, 1000 * 2 ** attempts++) + Math.random() * 500;
setTimeout(connect, delay);
});

ws.on("error", (e) => console.error("WS error:", e.message));
}

connect();

Four rules baked in:

Snapshot first. Begin from a full picture.
Merge, never replace. Live updates are often partial, such as { match_id, home: { score: 1 } }. Replacing the whole object would erase team names.
Reconnect with backoff. Connections drop. Wait longer after each failure.
Refetch after reconnecting. You may have missed updates.

The real WebSocket URL and message format are in the WebSocket API docs. For a complete walkthrough, see the Orbistats tutorial on building a live scoreboard in 50 lines of JavaScript.

Step 9: Alerts with webhooks (the short version)

Want a Discord message when a match ends? Do not poll. Give the provider a URL and let them call you:

javascript
import express from "express";

const app = express();
app.use(express.json());

const seen = new Set();

app.post("/webhooks/orbistats", (req, res) => {
const event = req.body;
// TODO: verify the signature as described in the docs

if (event.id && seen.has(event.id)) return res.sendStatus(200); // duplicate
if (event.id) seen.add(event.id);

console.log("event:", event.type, event);
res.sendStatus(200); // reply FAST, do heavy work later
});

app.listen(3001);

Golden rules: reply with 200 quickly, process later, and ignore duplicates, because providers retry slow responses. The event names and payload here are illustrative, so use the real ones from the Webhooks API page.

Security checklist for beginners
Never commit your API key. Add .env to .gitignore.
Never call the API from browser JavaScript with your key in it.
Validate any user input before putting it in a URL (we used a fixed sport list).
Use textContent, not innerHTML, for API data.
Set timeouts on every request.
Rotate your key if you ever paste it in a public place.
The business angle: who pays for live match data?

Live data is not just a hobby market. It powers whole industries, and understanding them helps you pick project ideas that matter.

Media and publishers use live scoreboards to keep readers on the page and bring them back. See Media & Publishers.
Fantasy and AI products need instant player updates and deep history. See Fantasy & AI Data.
Sportsbooks and trading desks react to live events within seconds, so latency is money. See Sportsbooks and Trading Desks.
Analytics and data science teams build models and dashboards from live and historical data. See Analytics & Data Science.
Clubs, federations and leagues use data for performance analysis and fan engagement. See Teams & Clubs and Federations & Leagues.

What this means for you as a developer: collecting live data is expensive, so most businesses buy it and spend their effort on the product. That is also true for you. A sports API lets a solo developer ship what once needed a data team. Good ways to use that:

Portfolio project. A live tracker with caching, error handling and a clean UI shows real engineering skills.
Niche apps. One sport, one league, one audience (a college league, an esports title, a local club).
Freelance work. Clubs, blogs and small media sites often need score widgets and dashboards.
Content plus tools. Calculators and explainers attract search traffic. The Orbistats tools and guides pages are good examples of the idea.

No guarantees on income, of course. The point is that the data is no longer the barrier.

Eight project ideas to build next
Add cricket and tennis with sport-specific adapters.
A "what's live right now" page across all 13 sports.
A Discord or Telegram goal-alert bot using webhooks.
A standings page with 10-minute caching.
A head-to-head page using historical data.
A "spoiler-free" delayed scoreboard that matches TV timing.
A fantasy points updater based on live events.
A mobile-friendly PWA of your tracker.

For more code to learn from, Orbistats has published tutorials on consuming a sports data API in Python with FastAPI, consuming it in .NET Core, building a live dashboard with Next.js and a +EV bet finder in Python.

How to choose a sports API: a 10-point checklist
Sport coverage. Does it cover what you need now and later? Orbistats covers 13 sports.
Data depth. Fixtures only, or live, stats, odds and history too?
Latency. Test it yourself. Orbistats advertises a sub-50ms live feed, but measure from your own region.
Delivery options. REST, WebSocket and webhooks?
Consistent schema. One shape across sports saves weeks.
Free tier and sandbox. Can you test before paying?
Docs and reliability signals. Look for a changelog and status page.
Versioning. Stable versions like /v1/ mean updates will not suddenly break your app.
Licensing. Read the data licensing terms before building a commercial product.
Price at scale. Compare costs as usage grows, not just the entry plan. Orbistats lists Free at $0, Starter at $19/month, Growth at $79/month and custom Enterprise. See the pricing page for current details.
Common mistakes (and how to avoid them)
Putting the API key in front-end code. Use a server in the middle.
Polling every second. It burns your quota and still lags. Cache, poll smartly or use WebSocket.
No timeouts. One hung request can freeze your app.
Retrying 401 and 403 errors. Retrying will not fix a bad key.
Replacing instead of merging live updates. Team names will vanish.
Trusting a score as final. Scores can be corrected until full time.
Assuming every sport has the same fields. Read each sport's docs and use adapters.
Ignoring time zones. Store UTC, convert only for display.
Not showing data freshness. Always show when the data was fetched.
FAQ

What is a sports API?
A service that gives you sports data (scores, fixtures, stats, odds, history) as structured JSON through requests your code can make.

Is there a free sports API?
Many providers offer a free tier for prototypes. Orbistats offers a free key with no card required, plus a public sandbox.

Do I need a backend to use a sports API?
For anything public, yes. A small server keeps your key secret and lets you cache.

Which language should I use?
Any language that can make HTTP requests: Python, JavaScript, C#, Java, PHP, Go and more.

What is the difference between REST and WebSocket?
With REST you ask for data. With WebSocket the server pushes updates to you as they happen.

How fast is "live"?
It depends on the provider and delivery method. Polling is delayed by your interval, while WebSocket pushes updates as they happen.

Can I get data for sports other than football?
Yes. Orbistats covers 13: Football, Basketball, American Football, Cricket, Tennis, Baseball, Esports, Combat Sports, Volleyball, Handball, Ice Hockey, Golf and Horse Racing.

Can I use the data commercially?
Check the provider's data licensing and terms first.

Final thoughts

Getting live match data used to be a data-engineering problem. Today it is an HTTP request.

The recipe is short. Get a key. Test with curl. Keep the key on the server. Handle errors and rate limits. Cache aggressively. Show data freshness. Use WebSocket when "live" really means live. Use webhooks for alerts. Write one adapter per sport.

Now run the tracker, break it, fix it and make it yours.

👉 Get a free API key and run the code from this article today.
📚 Explore the developer hub for docs, guides and examples.
🔬 Like data-driven reads? Bookmark the research section.
🏢 Curious who is behind it? Visit the About page, or explore the full platform at Orbistats.

What will you build first: a tracker, an alert bot or a fantasy tool? Tell me in the comments.

Top comments (0)