Live sports scores in JavaScript
Four small Node scripts that share one helper: the games in progress right now for any league, a season schedule (NFL and college football by week), one game's details, box score and latest plays, and a watcher that polls the live endpoint and prints only what changed.
- Stack
- Node.js 18+, fetch
- Plan
- Free to build and try; Starter to poll one team's games all season
- Code
- javascript/ (MIT)
Updated 2026-11-06. Tested against the live API on that date. Data is aggregated from public sources and is typically 20-30 seconds behind live play (about 1 second after our source). There is no SLA.
1. Get a free API key
Sign up (no card) and copy the key from the dashboard. The Free plan gives you 1,000 calls in the first 30 days, then 125 a month, at 1 request per second. Every successful REST call counts as one call.
Check the key works with one call to the live-scores endpoint:
Terminal
export REALTIME_SPORTS_API_KEY=your_key
curl -H "Authorization: Bearer $REALTIME_SPORTS_API_KEY" \
"https://www.realtimesportsapi.com/api/v1/sports/football/leagues/nfl/events/live"2. Get the code
Everything below is in the javascript/ folder of the examples repo (MIT). Clone it and work from that folder.
Terminal
git clone https://github.com/ElcoDevRepos/realtime-sports-api-examples
cd realtime-sports-api-examples/javascript3. The helper
apiGet sends your key as a Bearer token and returns the response envelope (data plus meta). On an error it throws with the API's own code, message and hint, so a bad key or an exhausted month reads clearly instead of as a stack trace. formatGame turns a game into one line: away, home, scores, status and the event id in brackets. No packages: Node 18+ has fetch built in.
javascript/lib.mjs
// Shared helpers for the JavaScript examples. Node 18+ (uses the built-in fetch), no dependencies.
import { existsSync, readFileSync } from 'node:fs';
export const API_BASE = process.env.RSA_API_BASE || 'https://www.realtimesportsapi.com/api/v1';
// Load ../.env (KEY=value lines) if present; variables already in the environment win.
const envFile = new URL('../.env', import.meta.url);
if (existsSync(envFile)) {
for (const line of readFileSync(envFile, 'utf8').split(/\r?\n/)) {
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
}
}
export function requireKey() {
const key = process.env.REALTIME_SPORTS_API_KEY;
if (!key || key === 'your_api_key_here') {
console.error('Set REALTIME_SPORTS_API_KEY first (export it, or copy .env.example to .env).');
console.error('Get a free key at https://www.realtimesportsapi.com/signup');
process.exit(1);
}
return key;
}
export class ApiError extends Error {
constructor(status, body) {
const err = body?.error ?? {};
super(`HTTP ${status} ${err.code ?? ''}: ${err.message ?? 'request failed'}`.trim());
this.status = status;
this.code = err.code;
this.hint = err.hint;
this.retryAfter = undefined;
}
}
/**
* GET a path under /api/v1 and return the parsed envelope `{ success, data, meta }`.
* Throws ApiError on non-2xx responses (401 bad key, 429 monthly quota exhausted, ...).
*/
export async function apiGet(path, query = {}) {
const key = requireKey();
const url = new URL(API_BASE + path);
for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' } });
const body = await res.json().catch(() => null);
if (!res.ok) {
const e = new ApiError(res.status, body);
e.retryAfter = Number(res.headers.get('retry-after')) || undefined;
throw e;
}
return body;
}
/** One-line score: "KC 14 @ BAL 10 5:23 - 2nd Quarter" */
export function formatGame(g) {
const a = g.awayTeam ?? {};
const h = g.homeTeam ?? {};
const score = (t) => (t.score === undefined || t.score === null ? '' : ` ${t.score}`);
return `${a.abbreviation ?? a.name ?? '?'}${score(a)} @ ${h.abbreviation ?? h.name ?? '?'}${score(h)} ${g.status?.detail ?? ''} [${g.id}]`;
}
/** Run an async main() and print API errors cleanly instead of a stack trace. */
export function run(main) {
main().catch((err) => {
if (err instanceof ApiError) {
console.error(err.message);
if (err.hint) console.error(`Hint: ${err.hint}`);
if (err.status === 429) console.error(`Monthly quota exhausted. Retry after ${err.retryAfter ?? '?'} s or upgrade.`);
} else {
console.error(err);
}
process.exit(1);
});
}
4. Games in progress right now
Pass a sport and league slug: football nfl, basketball nba, hockey nhl, baseball mlb, basketball mens-college-basketball, or soccer eng.1 (soccer is soccer; football is American football). It is one call.
At 2:49 pm Central on 6 November 2026, node live-scores.mjs soccer eng.1 printed COV 0 @ EVE 0 HT [401879242]: Coventry at Everton, at half-time, followed by the calls left this month.
javascript/live-scores.mjs
// Games in progress right now for one league.
// Usage: node live-scores.mjs [sport] [league]
// node live-scores.mjs football nfl
// node live-scores.mjs soccer eng.1 (soccer is "soccer"; "football" is American football)
import { apiGet, formatGame, run } from './lib.mjs';
const [sport = 'football', league = 'nfl'] = process.argv.slice(2);
run(async () => {
const { data: games, meta } = await apiGet(`/sports/${sport}/leagues/${league}/events/live`);
if (!games.length) console.log(`No ${league} games live right now.`);
for (const g of games) console.log(formatGame(g));
if (meta?.rateLimit) console.log(`\nCalls remaining this month: ${meta.rateLimit.remaining}`);
});
5. A season schedule
NFL and college football take a week; other leagues return the season. node schedule.mjs football nfl 2026 9 listed 15 games for week 9, from JAX 33 @ BAL 10 Final [401873037] (Thursday night) to BUF 0 @ MIN 0 Mon, November 9th at 8:15 PM EST [401873051], each with its event id.
javascript/schedule.mjs
// Season schedule. NFL and college football also take a week.
// Usage: node schedule.mjs [sport] [league] [season] [week] [seasonType]
// node schedule.mjs football nfl 2026 5 2 (2026 regular season, week 5)
import { apiGet, formatGame, run } from './lib.mjs';
const [sport = 'football', league = 'nfl', season = '2026', week, seasonType = '2'] = process.argv.slice(2);
run(async () => {
const { data: games } = await apiGet(`/sports/${sport}/leagues/${league}/seasons/${season}/schedule`, { week, seasonType });
for (const g of games) console.log(`${new Date(g.date).toLocaleString()} ${formatGame(g)}`);
console.log(`\n${games.length} games`);
});
6. One game: details, box score and plays
Three calls: the game, its box score and the latest plays. For the Everton match the box score had homeTeam, awayTeam, homePlayers, awayPlayers, periodScores, and the plays read Kickoff, James Garner (Everton) Pass at 0', Jordan Pickford (Everton) Pass at 1' and so on.
javascript/event-details.mjs
// One game: details, box score and the latest plays (3 API calls).
// Usage: node event-details.mjs <sport> <league> <eventId>
// Find event ids with live-scores.mjs (printed in [brackets]).
import { apiGet, formatGame, run } from './lib.mjs';
const [sport, league, eventId] = process.argv.slice(2);
if (!eventId) {
console.error('Usage: node event-details.mjs <sport> <league> <eventId> (e.g. football nfl 401772982)');
process.exit(1);
}
run(async () => {
const base = `/sports/${sport}/leagues/${league}/events/${eventId}`;
const { data: event } = await apiGet(base);
console.log(formatGame(event));
const { data: box } = await apiGet(`${base}/boxscore`);
console.log('\nBox score keys:', Object.keys(box ?? {}).join(', '));
const { data: plays } = await apiGet(`${base}/plays`, { limit: 10 });
console.log('\nPlays:');
for (const p of plays ?? []) console.log(` - ${p.text ?? [p.type?.text ?? 'Play', p.clock?.displayValue].filter(Boolean).join(' ')}`);
});
7. Watch a league and print score changes
watch.mjs polls the live endpoint every N seconds and prints a line only when a game's score, clock, period or status changes. Each poll is one call, so a 3-hour game polled every 30 seconds is about 360 calls: try it on Free, then use Starter (10,000 calls a month) to follow one team all season. Data is typically 20-30 seconds behind live play, so polling faster than every 30 seconds buys nothing, and it is not suitable for in-play betting.
On a paid plan you can skip polling: webhooks push each score change to your server and the WebSocket stream pushes them to a long-running client.
Terminal
node live-scores.mjs basketball nba
node schedule.mjs football nfl 2026 9
node event-details.mjs soccer eng.1 401879242
node watch.mjs basketball nba 30javascript/watch.mjs
// Poll the live endpoint and print only what changed: score, clock/period, final.
// Usage: node watch.mjs [sport] [league] [seconds]
// node watch.mjs basketball nba 30
// Every poll is one API call: a 3-hour game polled every 30 s is about 360 calls.
// Data is typically 20-30 seconds behind live play; not for in-play betting.
import { apiGet, formatGame, run } from './lib.mjs';
const [sport = 'football', league = 'nfl', seconds = '30'] = process.argv.slice(2);
const last = new Map(); // event id -> last line printed
async function poll() {
const { data: games } = await apiGet(`/sports/${sport}/leagues/${league}/events/live`);
for (const g of games) {
const line = formatGame(g);
if (last.get(g.id) !== line) {
console.log(`${new Date().toLocaleTimeString()} ${line}`);
last.set(g.id, line);
}
}
if (!games.length && last.size === 0) console.log(`No ${league} games live right now.`);
}
run(async () => {
await poll();
setInterval(() => poll().catch((e) => console.error(e.message)), Number(seconds) * 1000);
});
8. In the browser
Don't put your key in front-end code: anyone can read it. Call the API from a server route and send your page only the games. The Next.js scoreboard guide does exactly that, and the Discord, Slack and Telegram bots build on the same calls.
Endpoints used
GET /sports/{sport}/leagues/{league}/events/liveGET /sports/{sport}/leagues/{league}/seasons/{season}/scheduleGET /sports/{sport}/leagues/{league}/events/{eventId}GET /sports/{sport}/leagues/{league}/events/{eventId}/boxscoreGET /sports/{sport}/leagues/{league}/events/{eventId}/plays
Full reference: docs · OpenAPI · what each endpoint returns, by league
FAQ
- Is there a free live sports scores API for JavaScript?
- Yes. Any fetch works; these scripts use Node's built-in fetch with no packages. A free key (no card) gives 1,000 calls in the first 30 days, then 125 a month, and includes every endpoint.
- Which leagues does it cover?
- NFL, NBA, NHL, MLB, college football and basketball, and soccer leagues such as the Premier League (eng.1), LaLiga and MLS. Pass the sport and league slug to any script.
- How fresh are the scores?
- Typically 20-30 seconds behind live play. That suits scoreboards, bots and alerts, not in-play betting.