Skip to content

NHL API: Free Endpoints for Schedules, Scores, Play-by-Play and Stats

The free JSON feed behind nhl.com and the NHL app. No key needed, and every endpoint below answered.

Last updated:

What is the NHL API?

The NHL API is the free, undocumented JSON feed behind nhl.com and the NHL app. The league runs it, publishes no docs for outside developers and asks for no API key: a plain GET request returns JSON. api-web.nhle.com serves schedules, scores, standings, rosters, players and game feeds, and api.nhle.com/stats/rest answers filtered stat queries.

We called every URL on this page on 2 November 2026. For several leagues behind one URL pattern, the ESPN API serves the NHL under hockey/nhl. Its scoreboard for 1 November returned the same five games under other ids. Edmonton at the Rangers is 401892627 at ESPN and 2026020230 here, so join the two feeds on date and team codes.

NHL API Base URLs

Two hosts answered in our tests. The third, the old stats API, is gone.

Base URLWhat it servesStatus
https://api-web.nhle.com/v1Schedules, scores, standings, rosters, players, box scores, play-by-play200
https://api.nhle.com/stats/rest/enStat tables with filters, sorting and paging200
https://statsapi.web.nhl.com/api/v1The old stats APINo DNS record, no HTTP status

For https://statsapi.web.nhl.com/api/v1/teams, curl failed with "Could not resolve host", and a public DNS resolver returned NXDOMAIN (no such domain). No server answers there, so retries will never succeed. The bare roots of both live hosts return 404, and neither serves docs.

What the NHL API Covers

The feed covers the NHL only, from the preseason to the playoffs. Every game carries a gameType: 1 for preseason, 2 for regular season, 3 for playoffs. Toronto's 2026-27 schedule lists 84 regular-season games, and the stats host counts 1,344 league-wide. The final 2025-26 standings show 82 games per team, so code that hard-codes 82 needs a fix.

/v1/standings-season lists 109 seasons, back to 1917-18, and a play-by-play call for game 2010020001 (Montreal at Toronto, 7 October 2010) returned 305 plays. Names arrive as objects keyed by language, such as {"default": "Maple Leafs"}, so read .default unless you want a translation.

Key NHL API Endpoints

Every api-web.nhle.com path starts with /v1/. Dates are YYYY-MM-DD, seasons are eight digits (20262027) and teams are three-letter codes such as TOR. The examples show what each call returned for us.

EndpointWhat it returnsExample
/v1/schedule/{date}League schedule for seven days from the date/v1/schedule/2026-11-02: 51 games
/v1/score/{date}Scores, goals and clocks for one day/v1/score/2026-11-01: 5 games
/v1/standings/{date}League table on a date/v1/standings/2026-11-01: 32 teams
/v1/standings-seasonEvery season and its standings rules109 seasons
/v1/gamecenter/{gameId}/boxscoreTeam totals and player stat lines/v1/gamecenter/2026020224/boxscore
/v1/gamecenter/{gameId}/play-by-playEvery event of a game2026020224: 308 plays
/v1/gamecenter/{gameId}/landingScoring summary, three stars, penalties/v1/gamecenter/2026020224/landing
/v1/club-schedule-season/{team}/{season}One team's seasonTOR/20262027: 88 games
/v1/club-schedule/{team}/week/{date}One team's weekTOR/week/2026-11-02: 3 games
/v1/roster/{team}/{season}forwards, defensemen, goaliesTOR/20262027: 16, 10, 2
/v1/player/{playerId}/landingBio, draft, season and career totals, last 5 games8478402 (Connor McDavid)
/v1/player/{playerId}/game-log/{season}/{gameType}One row per game8478402/game-log/20262027/2: 15 games
/v1/skater-stats-leaders/{season}/{gameType}League leaders by category?categories=goals&limit=5
/v1/club-stats/{team}/{season}/{gameType}A team's skater and goalie statsTOR/20262027/2: 24 skaters

One key differs: the box score groups players under defense, and the roster uses defensemen.

Stats REST endpoints

The stats host wraps every answer in {"data": [...], "total": N}, and total is the full match count whatever limit you send.

EndpointWhat it returnsExample
/stats/rest/en/teamEvery team in the records, defunct ones included62 rows, the Quebec Nordiques first
/stats/rest/en/skater/summaryOne row per skater: goals, points, ice time?limit=10&cayenneExp=seasonId=20262027: total 741
/stats/rest/en/gameOne row per game with ids and scores?cayenneExp=season=20262027&limit=5: total 1409

NHL API Play-by-Play Endpoint

The play-by-play feed is the NHL endpoint developers search for most. It lists every faceoff, hit, shot, penalty and goal of one game, with rink coordinates and the players involved.

URL
https://api-web.nhle.com/v1/gamecenter/{gameId}/play-by-play

Where the game id comes from

Every schedule, score and team schedule entry has a ten-digit id: the year the season starts, a two-digit game type and a four-digit game number. The stats host confirms the split. Game 2026020232, Vegas at Boston on 2 November 2026, has season 20262027, gameType 2 and gameNumber 232. A playoff game from April 2026 reads 2025030186, because it belongs to the 2025-26 season.

What a play contains

Our test game, 2026020224 (Toronto 4, Vancouver 2 on 31 October 2026), returned 308 plays and 40 rosterSpots. William Nylander's opening goal looked like this, trimmed:

JSON
{"eventId": 66,
 "periodDescriptor": {"number": 1, "periodType": "REG"},
 "timeInPeriod": "01:23", "timeRemaining": "18:37",
 "situationCode": "1551", "homeTeamDefendingSide": "right",
 "typeCode": 505, "typeDescKey": "goal", "sortOrder": 26,
 "details": {"xCoord": 82, "yCoord": 4, "zoneCode": "O",
   "shotType": "backhand", "scoringPlayerId": 8477939,
   "assist1PlayerId": 8477426, "eventOwnerTeamId": 10,
   "goalieInNetId": 8482447, "awayScore": 1, "homeScore": 0}}
FieldTypeWhat it holds
typeDescKeystringEvent type: 57 hit, 54 faceoff, 50 shot-on-goal and 6 goal in our game
typeCodenumberThe type as a number: 505 goal, 506 shot on goal
sortOrdernumberEvent order. Sort on this, because eventId is out of order (a shot at 01:01 has id 151, the goal at 01:23 has id 66)
periodDescriptor.numbernumberPeriod
timeInPeriodstringElapsed clock as MM:SS
details.xCoord, details.yCoordnumberRink position: x from -98 to 99, y from -41 to 41 in our game
homeTeamDefendingSidestringleft or right. It flipped every period, and Toronto's goals moved from positive x in period 1 to negative x in period 2
situationCodestringFour digits for the players on the ice: 1551 on 248 of the 308 plays
detailsobjectPlayer ids such as scoringPlayerId and shootingPlayerId. Absent on 7 plays

Map the player ids in details to rosterSpots for names and positions. A live game adds a situation object, and for Vegas at Boston it paired code 1351 with an away strength of 3 and a home strength of 5.

Dates, Seasons and the now Redirect

Paths with a date or season also take now or current. All eight that we tried answered with HTTP 307, an empty body and a location header that holds the dated URL:

URL
/v1/schedule/now        -> /v1/schedule/2026-11-02
/v1/score/now           -> /v1/score/2026-11-02
/v1/roster/TOR/current  -> /v1/roster/TOR/20262027
/v1/club-stats/TOR/now  -> /v1/club-stats/TOR/20262027/2
/v1/skater-stats-leaders/current?categories=goals&limit=5
                        -> /v1/skater-stats-leaders/20262027/2?categories=goals&limit=5

Your client has to follow redirects. Python requests and JavaScript fetch do it by default, and curl needs -L. Prefer the dated form in stored code, because it skips a round trip and always means the same day. The redirect uses the North American date: at 01:10 UTC on 3 November, /v1/schedule/now still went to 2 November. Any start date works for the schedule: /v1/schedule/2026-11-04, a Wednesday, returned 4 to 10 November. Past dates work too, and /v1/standings/2026-10-15 put Vegas first with 14 points.

ParameterWhat it doesExample
categoriesStat names for the leaders endpoint. With none, it returned 9 categories?categories=goals,assists
limit (leaders)Rows per category. -1 returned all 481 skaters with a goal?limit=-1
{gameType}Path segment: 2 regular season, 3 playoffs/game-log/20252026/3: 6 games
cayenneExpFilter expression on the stats hostseasonId=20262027 and gameTypeId=2
sortURL-encoded JSON list of sort keys[{"property":"points","direction":"DESC"}]
limit, start (stats)Page size and offsetlimit=5&start=5

Field names in cayenneExp change by endpoint. The game endpoint filters on season, and the skater summary needs seasonId: it returned HTTP 500 for season. Paging needs a tiebreak too. Sorted on points alone, the pages at start=0 and start=5 both listed Nathan MacKinnon (24 points) and left out Auston Matthews, who also had 24. A second sort key on playerId fixed it.

NHL API Code Examples

Scores for one day in Python

Python
import requests

url = 'https://api-web.nhle.com/v1/score/2026-11-01'
resp = requests.get(url, timeout=10)
resp.raise_for_status()
for game in resp.json()['games']:
    away, home = game['awayTeam'], game['homeTeam']
    print(game['id'], away['abbrev'], away['score'], '@', home['abbrev'], home['score'], game['gameState'])

The first line it printed was 2026020227 WPG 0 @ CAR 7 OFF.

Goals from the play-by-play in Python

Python
import requests

url = 'https://api-web.nhle.com/v1/gamecenter/2026020224/play-by-play'
data = requests.get(url, timeout=10).json()

names = {p['playerId']: p['lastName']['default'] for p in data['rosterSpots']}
plays = sorted(data['plays'], key=lambda p: p['sortOrder'])
for play in plays:
    if play['typeDescKey'] == 'goal':
        d = play['details']
        print(play['periodDescriptor']['number'], play['timeInPeriod'],
              names[d['scoringPlayerId']], d['shotType'], f"{d['awayScore']}-{d['homeScore']}")

It printed six goals, from 1 01:23 Nylander backhand 1-0 to 3 18:24 Blueger wrist 4-2.

Standings by date with cURL

cURL
curl -s 'https://api-web.nhle.com/v1/standings/2026-11-01' \
  | jq -r '.standings[:3][] | "\(.teamName.default) \(.points) pts in \(.gamesPlayed) GP"'

# now answers 307 first; -L follows it to the dated URL
curl -sL 'https://api-web.nhle.com/v1/standings/now' | jq '.standings[0].points'

The first command printed Carolina Hurricanes 26 pts in 16 GP on its first line. Drop the jq part to see the raw JSON.

Points leaders in JavaScript

JavaScript
const sort = JSON.stringify([
  { property: 'points', direction: 'DESC' },
  { property: 'playerId', direction: 'ASC' },
]);
const url = 'https://api.nhle.com/stats/rest/en/skater/summary?limit=5'
  + '&cayenneExp=' + encodeURIComponent('seasonId=20262027 and gameTypeId=2')
  + '&sort=' + encodeURIComponent(sort);

const res = await fetch(url);
const { data, total } = await res.json();
data.forEach(p => console.log(p.skaterFullName, p.teamAbbrevs, p.points));
console.log(total, 'skaters in total');

We ran it as an .mjs file on Node 22. It printed Connor McDavid EDM 39 first and 741 skaters in total last.

NHL API Errors: 404, 307 and 500

404 Not Found, as an HTML page

A game id that does not exist (/v1/gamecenter/2026029999/boxscore), an impossible date (/v1/schedule/2026-13-45) and a mistyped path (/v1/scores/now) all returned 404. The body is an HTML error page with content-type: text/html, so .json() raises an exception on it. Check the status code before you parse the body.

307, then 404, for a bad team code

/v1/roster/XYZ/current answered 307 to /v1/roster/XYZ/20262027, and that URL returned 404. The redirect comes before the server checks the team, so a 307 says nothing about whether a code is valid.

500 from the stats host

A wrong field name in cayenneExp returns HTTP 500 with {"message":"Exception processing Agrest request"}. We got it from /stats/rest/en/skater/summary?limit=5&cayenneExp=season=20262027.

200 with no stats yet

A scheduled game's box score answers 200 with gameState FUT and no playerByGameStats key. Finished games read OFF or FINAL: Toronto's preseason games read FINAL and its 16 finished regular-season games read OFF, so treat both as final.

Limitations and Terms

Personal, non-commercial use only

The NHL publishes no separate API terms. Its Terms of Service (last updated 29 October 2025) cover nhl.com and the NHL apps, which run on this feed. Section 7 allows use "only for non-commercial, informational, personal use". Section 2 forbids "unauthorized spidering, scraping, or harvesting of content or information". A personal dashboard or a class project fits those words. A paid app, a resold feed or a bulk download of every game does not.

  • No published rate limit: the terms and the responses state none. We made about 60 requests in 20 minutes and got no 429 and no rate-limit headers. Responses from api-web carried cache-control: max-age values of up to 19 seconds, so poll a live game no faster than that.
  • No docs or changelog: the NHL documents none of these paths, and the old host has already disappeared.
  • robots.txt: both hosts allow all user agents, block a list of named AI crawlers and set ai-train=no.
  • Odds: schedule and score responses carry an oddsPartners list and, for a game about to start, an odds array per team. On the evening of 2 November, only Calgary at San Jose, the one game still to start, had prices. For spreads, totals and props across many books, see NHL odds APIs.
  • Shot data for models: MoneyPuck publishes per-season NHL shot files with expected-goals values, listed on our free sports datasets page.

When your app needs an SLA, a commercial license or support when a field changes, compare licensed sports data providers.

Browse API Providers →

Community Resources

We fetched each of these on 2 November 2026.

  • Zmalski/NHL-API-Reference on GitHub: an unofficial endpoint reference for both hosts, with curl examples and parameter notes. MIT license; last push November 2025.
  • nhl-api-py on PyPI: a Python wrapper (pip install nhl-api-py, then from nhlpy import NHLClient). Version 3.7.1 came out on 29 October 2026 and needs Python 3.10 or later.
  • coreyjs/nhl-api-py on GitHub: the source and docs of that wrapper, under the Apache-2.0 license.

Frequently Asked Questions

Yes. Both hosts answered our calls on 2 November 2026 with no key, account or payment. The NHL Terms of Service allow only "non-commercial, informational, personal use", so free covers personal projects. A commercial app needs a license from the league or a licensed data provider.
The NHL runs the servers, because this is the feed behind nhl.com and the NHL app. It publishes no docs for outside developers, though, and its terms never mention an API. The paths on this page come from our own tests and from community references such as Zmalski/NHL-API-Reference on GitHub.
No. We sent no key, cookie or custom header to api-web.nhle.com or api.nhle.com, and every valid URL returned 200. Make sure that your client follows redirects, because now and current URLs answer with HTTP 307 first.
The old stats API is gone. On 2 November 2026, curl failed with "Could not resolve host" for https://statsapi.web.nhl.com/api/v1/teams. Use the new hosts instead: https://api.nhle.com/stats/rest/en/team lists the teams, and https://api-web.nhle.com/v1/roster/TOR/20262027 gives a roster.
The NHL only: preseason (game type 1), regular season (2) and playoffs (3), with standings back to 1917-18. For several leagues in one feed, the ESPN API serves the NHL under hockey/nhl next to the NBA, NFL and MLB.
The NHL publishes none, and the responses carry no rate-limit headers. We made about 60 requests in 20 minutes and got no 429. Responses on api-web.nhle.com are cached for up to 19 seconds, so poll a live game no faster than that, and cache finished games.
Take the game id from a schedule or score response and call https://api-web.nhle.com/v1/gamecenter/{gameId}/play-by-play. For Toronto at Vancouver on 31 October 2026, /v1/gamecenter/2026020224/play-by-play returned 308 plays. Sort them on sortOrder. Field table above.
For a personal project, yes. A commercial product falls outside the terms, which allow only non-commercial, personal use. The NHL can also change these paths at any time, and the old host is already gone. Compare licensed sports data providers, or browse the API directory for odds.

Need Licensed NHL Data?

The NHL API suits personal projects. A commercial app needs licensed data, an SLA and support. Compare providers in our directory.