GuidesNHL

NHL API from JavaScript

Prefix https://proxy.cors.dev/ to any api-web.nhle.com URL: free, no signup, no key, 600 requests per minute, JSON up to 1 MiB. The NHL API sends no Access-Control-Allow-Origin header, so fetch() from your page fails with a CORS error, and the statsapi.web.nhl.com host used by older tutorials no longer resolves at all. Scores, standings, schedules, rosters, player stats, box scores and play-by-play all come from api-web.nhle.com/v1.

Why your NHL fetch() fails

There are two different failures. If your code still calls statsapi.web.nhl.com/api/v1, the request never reaches a server: the host was retired and DNS no longer resolves it, so Chrome reports net::ERR_NAME_NOT_RESOLVED. Move to https://api-web.nhle.com/v1, which has different paths and a different JSON shape.

The current API answers with no Access-Control-Allow-Origin header. The data arrives, but the browser blocks your script from reading it and logs blocked by CORS policy. Prefixing the URL with https://proxy.cors.dev/ sends the request from a server and returns it with the header your page needs. No key is involved at any step.

Today's scores in the browser

/v1/score/now lists today's games with teams, score, shots on goal, period, clock and goals. It was 9.5 KB on a day with 2 games, and 82 KB for 2026-10-15, a night with 11 games and 70 goals. Games that have not started have no score field, so the example shows the start time for those.

Today's NHL scores on your page
// Markup: <ul id="scores"></ul>
async function nhl(path) {
  const response = await fetch('https://proxy.cors.dev/https://api-web.nhle.com/v1' + path, {
    credentials: 'omit',
  });
  if (!response.ok) {
    throw new Error(`NHL request failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
  }
  return response.json();
}

function describe(game) {
  const { awayTeam: away, homeTeam: home } = game;
  if (away.score === undefined) {
    const start = new Date(game.startTimeUTC).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    return `${away.abbrev} at ${home.abbrev}, ${start}`;
  }
  const status =
    ['FINAL', 'OFF'].includes(game.gameState) ? 'final' : `period ${game.periodDescriptor.number}, ${game.clock.timeRemaining} left`;
  return `${away.abbrev} ${away.score}, ${home.abbrev} ${home.score} (${status})`;
}

const list = document.querySelector('#scores');
nhl('/score/now')
  .then(({ games }) => {
    if (games.length === 0) {
      list.textContent = 'No games today';
      return;
    }
    list.replaceChildren(
      ...games.map((game) => {
        const item = document.createElement('li');
        item.textContent = describe(game);
        return item;
      }),
    );
  })
  .catch((error) => {
    list.textContent = error.message;
  });

gameState is FUT before a game and OFF once the result is official. Team names come as objects with a default key and often fr, because the API is bilingual. Logos are SVG URLs on assets.nhle.com that load in an <img> tag without the proxy.

Standings, schedules and player stats

/v1/standings/now returns all 32 teams in league order, with points, games played, wins, losses, overtime losses, goal differential, division and conference.

League standings as a table
// Uses nhl() from the example above, in a module script or the DevTools console
const { standings } = await nhl('/standings/now');

console.table(
  standings.map((team) => ({
    team: team.teamName.default,
    division: team.divisionName,
    gp: team.gamesPlayed,
    w: team.wins,
    l: team.losses,
    otl: team.otLosses,
    pts: team.points,
  })),
);

Most endpoints accept a date in place of now, for example /v1/score/2026-10-15, /v1/standings/2026-10-15 or /v1/schedule/2026-10-15. Seasons are eight-digit ids such as 20262027, and game ids start with the season year and a game type, 02 for the regular season.

NHL API endpoints and response sizes

Paths are on https://api-web.nhle.com unless a host is given. Replace TOR with any team abbreviation and the ids with your own; player 8478402 is Connor McDavid and 2026020108 is a regular-season game from October 2026.

NHL API endpoints through cors.dev, checked October 2026
PathReturnsSize
/v1/score/nowToday's games: score, shots, period, clock, goals9.5 KB with 2 games
/v1/score/2026-10-15Games and goals for one date82 KB with 11 games
/v1/scoreboard/nowScoreboard for 11 days around today126 KB
/v1/standings/nowAll 32 teams: points, record, division, conference58 KB
/v1/schedule/nowThis week's schedule88 KB
/v1/club-schedule-season/TOR/nowOne team's full season189 KB
/v1/roster/TOR/currentForwards, defensemen and goalies11 KB
/v1/player/8478402/landingProfile, season and career stats for one player25 KB
/v1/skater-stats-leaders/current?categories=goals&limit=5League leaders in a category1.6 KB
/v1/gamecenter/2026020108/boxscoreBox score for one game13 KB
/v1/gamecenter/2026020108/play-by-playEvery event in one game134 KB
api.nhle.com/stats/rest/en/teamTeam list with ids and abbreviations6.7 KB

Limits and polling

  • No official documentation: the NHL does not publish docs for api-web.nhle.com. The community reference Zmalski/NHL-API-Reference lists the known endpoints and parameters.
  • Polling: live scores only change when you ask again. Polling /v1/score/now every 15 seconds is 4 requests per minute per visitor, far below the free tier limit of 600 per minute per client IP.
  • Size: every endpoint in the table fits the free tier limit of 1 MiB; the largest, a full team season, is about 189 KB.
  • Free tier rules: GET and HEAD only, no daily or monthly quota, 10 seconds for the NHL to answer, and only Accept, If-None-Match and If-Modified-Since are forwarded. Pro adds custom headers, other methods and 6 MiB responses for $5 per month.

Good questions.

Is statsapi.web.nhl.com down?

Yes, for good. The host no longer resolves in DNS, so every request fails before it reaches a server. The data moved to api-web.nhle.com/v1 with new paths, for example /v1/standings/now instead of /api/v1/standings.

Does the NHL API need an API key?

No. The endpoints on this page are public and keyless. The only thing that stops browser code is the missing CORS header, which the proxy prefix adds.

Where is the NHL API documentation?

There is no official documentation. The community-maintained Zmalski/NHL-API-Reference repository on GitHub documents the known endpoints and their parameters.

How do I get live NHL scores?

Poll /v1/score/now every 10 to 30 seconds while games are on. It includes the period, the clock and every goal, needs no key, and stays far below 1 MiB: 82 KB for a night with 11 games.

Why are team names objects instead of strings?

The API is bilingual, so names come as objects such as default Vegas Golden Knights and fr Golden Knights de Vegas. Read .default and fall back to it when fr is missing.

Can I use this on GitHub Pages or another static host?

Yes. The prefixed URLs work from any origin, including github.io sites, custom domains and pages opened from a local file, because anonymous requests need no signup and no allowlist.