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/javascript

3. 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 30

javascript/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/live
  • GET /sports/{sport}/leagues/{league}/seasons/{season}/schedule
  • GET /sports/{sport}/leagues/{league}/events/{eventId}
  • GET /sports/{sport}/leagues/{league}/events/{eventId}/boxscore
  • GET /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.

More guides