A JSON API that returns the scores and goals from the latest finished or on-going NHL games. The data is sourced from the same NHL Stats API at https://api-web.nhle.com that the NHL website uses. The NHL Stats API is undocumented but unofficial documentation exists:
- https://github.com/Zmalski/NHL-API-Reference: fairly recent, seems very comprehensive and updated lately
- https://gitlab.com/dword4/nhlapi: older, plenty of discussion in its issues (thoughly mainly on the previous NHL Stats API version)
How we use the NHL Stats API:
- schedule gives us a list of the week's games; we check the game statuses and get the game IDs to fetch the games' gamecenter landing page and right-rail data
- landing gives us basic details of an individual game
- right-rail gives us more details of an individual game, like game stats and recap video links
- standings gives us team stats
This API is available at https://nhl-score-api.herokuapp.com/, and it serves as the backend for nhl-recap.
Returns an object with the date and the scores from the latest round’s games.
The date
object contains the date in a raw format and a prettier, displayable format, or
null
if there are no scores.
The games
array contains details of the games, each game item containing these fields:
status
(object)startTime
(string)goals
(array)scores
(object)teams
(object)gameStats
(object)preGameStats
(object)currentStats
(object)links
(object)errors
(array) (only present if data validity errors were detected)
The fields are described in more detail in Response fields.
Returns an array of objects with the date and the scores from given date range’s games.
Both startDate
and endDate
are inclusive, and endDate
is optional. The range is
limited to a maximum of 7 days to set some reasonable limit for the (cached) response;
this also matches the NHL Stats API that returns one week's schedule at a time.
The date
object contains the date in a raw format and a prettier, displayable format. Contrary to the
/api/scores/latest
endpoint, the date
is included even if that date has no scheduled games.
Though see the "If a date has no scheduled games" part below for possible peculiarities in that case.
The games
array contains details of the games, each game item containing these fields:
status
(object)startTime
(string)goals
(array)scores
(object)teams
(object)gameStats
(object)preGameStats
(object)currentStats
(object)links
(object)errors
(array) (only present if data validity errors were detected)
If a date has no scheduled games, you will either get:
- no entry for that date in the response, or
- an entry with an empty
games
array
This variety comes directly from the NHL Stats API response, I don’t know why it
behaves differently for some date ranges than others. Check the entries’ date
> raw
field to see what dates are actually included.
The fields are described in more detail in Response fields.
{
"date": {
"raw": "2017-10-16",
"pretty": "Mon Oct 16"
},
"games": [
{
"status": {
"state": "FINAL"
},
"startTime": "2016-02-29T00:00:00Z",
"goals": [
...
{
"period": "OT",
"scorer": {
"player": "David Krejci",
"playerId": 8471276,
"seasonTotal": 1
},
"assists": [
{
"player": "Torey Krug",
"playerId": 8476792,
"seasonTotal": 3
},
{
"player": "Zdeno Chara",
"playerId": 8465009,
"seasonTotal": 2
}
],
"team": "BOS",
"min": 2,
"sec": 36,
"strength": "PPG"
}
],
"scores": {
"BOS": 4,
"CHI": 3,
"overtime": true
},
"teams": {
"away": {
"abbreviation": "BOS",
"id": 6,
"locationName": "Boston",
"shortName": "Boston",
"teamName": "Bruins"
},
"home": {
"abbreviation": "CHI",
"id": 16,
"locationName": "Chicago",
"shortName": "Chicago",
"teamName": "Blackhawks"
}
},
"gameStats": {
"blocked": {
"BOS": 8,
"CHI": 9
},
"faceOffWinPercentage": {
"BOS": "45.5",
"CHI": "54.5"
},
"giveaways": {
"BOS": 5,
"CHI": 12
},
"hits": {
"BOS": 22,
"CHI": 22
},
"pim": {
"BOS": 6,
"CHI": 4
},
"powerPlay": {
"BOS": {
"goals": 0,
"opportunities": 2,
"percentage": "0.0"
},
"CHI": {
"goals": 1,
"opportunities": 3,
"percentage": "33.3"
}
},
"shots": {
"BOS": 37,
"CHI": 25
},
"takeaways": {
"BOS": 8,
"CHI": 9
}
},
"preGameStats": {
"records": {
"BOS": {
"wins": 43,
"losses": 31,
"ot": 7
},
"CHI": {
"wins": 50,
"losses": 22,
"ot": 9
}
},
"streaks": {
"BOS": {
"count": 1,
"type": "WINS"
},
"CHI": {
"count": 2,
"type": "LOSSES"
}
},
"standings": {
"BOS": {
"divisionRank": "3",
"leagueRank": "9",
"pointsFromPlayoffSpot": "+15"
},
"CHI": {
"divisionRank": "6",
"leagueRank": "25",
"pointsFromPlayoffSpot": "-3"
}
}
},
"currentStats": {
"records": {
"BOS": {
"wins": 44,
"losses": 31,
"ot": 7
},
"CHI": {
"wins": 50,
"losses": 22,
"ot": 10
}
},
"streaks": {
"BOS": {
"count": 2,
"type": "WINS"
},
"CHI": {
"count": 1,
"type": "OT"
}
},
"standings": {
"BOS": {
"divisionRank": "2",
"leagueRank": "8",
"pointsFromPlayoffSpot": "+17"
},
"CHI": {
"divisionRank": "6",
"leagueRank": "25",
"pointsFromPlayoffSpot": "-4"
}
}
},
"links": {
"gameCenter": "https://www.nhl.com/gamecenter/bos-vs-chi/2023/10/24/2023020092",
"videoRecap": "https://www.nhl.com/video/recap-bruins-at-blackhawks-10-24-23-6339814966112"
}
},
{
"status": {
"state": "LIVE",
"progress": {
"currentPeriod": 3,
"currentPeriodOrdinal": "3rd",
"currentPeriodTimeRemaining": {
"pretty": "01:58",
"min": 1,
"sec": 58
}
}
},
"startTime": "2016-02-29T02:30:00Z",
"goals": [
...
{
"period": "OT",
"scorer": {
"player": "Kyle Turris",
"playerId": 8474068,
"seasonTotal": 1
},
"assists": [
{
"player": "Mika Zibanejad",
"playerId": 8476459,
"seasonTotal": 3
}
],
"team": "OTT",
"min": 17,
"sec": 30,
"emptyNet": true
}
],
"scores": {
"OTT": 3,
"DET": 1
},
"teams": {
"away": {
"abbreviation": "OTT",
"id": 9,
"locationName": "Ottawa",
"shortName": "Ottawa",
"teamName": "Senators"
},
"home": {
"abbreviation": "DET",
"id": 17,
"locationName": "Detroit",
"shortName": "Detroit",
"teamName": "Red Wings"
}
},
"gameStats": {
"blocked": {
"OTT": 6,
"DET": 3
},
"faceOffWinPercentage": {
"OTT": "42.3",
"DET": "57.7"
},
"giveaways": {
"OTT": 4,
"DET": 7
},
"hits": {
"OTT": 11,
"DET": 15
},
"pim": {
"OTT": 2,
"DET": 4
},
"powerPlay": {
"OTT": {
"goals": 1,
"opportunities": 2,
"percentage": "50.0"
},
"DET": {
"goals": 0,
"opportunities": 1,
"percentage": "0.0"
}
},
"shots": {
"OTT": 19,
"DET": 24
},
"takeaways": {
"OTT": 4,
"DET": 7
}
},
"preGameStats": {
"records": {
"OTT": {
"wins": 43,
"losses": 28,
"ot": 10
},
"DET": {
"wins": 33,
"losses": 36,
"ot": 12
}
},
"streaks": {
"OTT": {
"count": 3,
"type": "LOSSES"
},
"DET": {
"count": 1,
"type": "WINS"
}
},
"standings": {
"OTT": {
"divisionRank": "8",
"leagueRank": "29",
"pointsFromPlayoffSpot": "0"
},
"DET": {
"divisionRank": "7",
"leagueRank": "23",
"pointsFromPlayoffSpot": "+2"
}
}
},
"currentStats": {
"records": {
"OTT": {
"wins": 43,
"losses": 28,
"ot": 10
},
"DET": {
"wins": 33,
"losses": 36,
"ot": 12
}
},
"streaks": {
"OTT": {
"count": 1,
"type": "WINS"
},
"DET": {
"count": 1,
"type": "LOSSES"
}
},
"standings": {
"OTT": {
"divisionRank": "8",
"leagueRank": "29",
"pointsFromPlayoffSpot": "+2"
},
"DET": {
"divisionRank": "7",
"leagueRank": "23",
"pointsFromPlayoffSpot": "0"
}
}
},
"links": {
"gameCenter": "https://www.nhl.com/gamecenter/ott-vs-det/2023/12/09/2023020412"
}
}
]
}
{
"date": {
"raw": "2017-10-16",
"pretty": "Mon Oct 16"
},
"games": [
{
"status": {
"state": "PREVIEW"
},
"startTime": "2016-02-29T02:30:00Z",
"goals": [],
"scores": {
"NYR": 0,
"PIT": 0
},
"teams": {
"away": {
"abbreviation": "NYR",
"id": 3,
"locationName": "New York",
"shortName": "NY Rangers",
"teamName": "Rangers"
},
"home": {
"abbreviation": "PIT",
"id": 5,
"locationName": "Pittsburgh",
"shortName": "Pittsburgh",
"teamName": "Penguins"
}
},
"preGameStats": {
"records": {
"NYR": {
"wins": 48,
"losses": 28,
"ot": 6
},
"PIT": {
"wins": 50,
"losses": 21,
"ot": 11
}
},
"playoffSeries": {
"round": 0,
"wins": {
"NYR": 1,
"PIT": 1
}
}
},
"currentStats": {
"records": {
"NYR": {
"wins": 48,
"losses": 28,
"ot": 6
},
"PIT": {
"wins": 50,
"losses": 21,
"ot": 11
}
},
"playoffSeries": {
"round": 0,
"wins": {
"NYR": 1,
"PIT": 1
}
}
},
"links": {}
}
]
}
raw
(string): the raw date in "YYYY-MM-DD" format, usable for any kind of processingpretty
(string): a prettified format, can be shown as-is in the client
status
object: current game status, with the fields:state
(string):"FINAL"
if the game has ended"LIVE"
if the game is still in progress"PREVIEW"
if the game has not started yet"POSTPONED"
if the game has been postponed
progress
object: game progress, only present ifstate
is"LIVE"
, with the fields:currentPeriod
(number): current period as a numbercurrentPeriodOrdinal
(string): current period as a display string (e.g."2nd"
)currentPeriodTimeRemaining
(object): time remaining in current period:pretty
(string): time remaining in prettifiedmm:ss
format;"END"
if the current period has endedmin
(number): minutes remaining;0
if the current period has endedsec
(number): seconds remaining;0
if the current period has ended
startTime
string: the game start time in standard ISO 8601 format "YYYY-MM-DDThh:mm:ssZ"goals
array: list of goal details, in the order the goals were scored- gameplay goal:
assists
(array) of objects with the fields (an empty array for unassisted goals):player
(string): the name of the player credited with the assistplayerId
(number): player ID in NHL APIs (can be used to fetch other resources from NHL APIs)seasonTotal
(number): the number of assists the player has had this season
emptyNet
(boolean): set totrue
if the goal was scored in an empty net, absent if it wasn’tmin
(number): the goal scoring time minutes, from the start of the periodperiod
(string): in which period the goal was scored;"OT"
means regular season 5 minute overtimescorer
(object):player
(string): the name of the goal scorerplayerId
(number): player ID in NHL APIs (can be used to fetch other resources from NHL APIs)seasonTotal
(number): the number of goals the player has scored this season
sec
(number): the goal scoring time seconds, from the start of the periodstrength
(string): can be set to"PPG"
(power play goal) or"SHG"
(short handed goal); absent if the goal was scored on even strengthteam
(string): the team that scored the goal
- shootout goal:
period
(string):"SO"
scorer
(object):player
(string): the name of the goal scorerplayerId
(number): player ID in NHL APIs (can be used to fetch other resources from NHL APIs)
team
(string): the team that scored the goal
- gameplay goal:
scores
object: each team’s goal count, plus one of these possible fields:overtime
: set totrue
if the game ended in overtime, absent if it didn’tshootout
: set totrue
if the game ended in shootout, absent if it didn’t
teams
object:away
(object): away team info:abbreviation
: team name abbreviationid
: team ID in NHL APIs (can be used to fetch other resources from NHL APIs)locationName
: team location name, e.g."New York"
shortName
: team short name, e.g."NY Rangers"
teamName
: team name, e.g."Rangers"
home
(object): home team info:abbreviation
: team name abbreviationid
: team ID in NHL APIs (can be used to fetch other resources from NHL APIs)locationName
: team location name, e.g."St. Louis"
shortName
: team short name, e.g."St Louis"
(note: "St" without a period)teamName
: team name, e.g."Blues"
gameStats
object: each teams’ game statistics, with the fields (only included in started games):blocked
: blocked shotsfaceOffWinPercentage
: what it saysgiveaways
: what it sayshits
: what it sayspim
: penalties in minutespowerPlay
(object):goals
: number of power play goalsopportunities
: number of power play opportunitiespercentage
: power play efficiency, e.g.50.0
shots
: shots on goaltakeaways
: what it says
preGameStats
object: each teams’ season statistics before the game, with the fields:records
object: each teams’ record for this regular season, with the fields:wins
(number): win count (earning 2 pts)losses
(number): regulation loss count (0 pts)ot
(number): loss count for games that went to overtime (1 pt)
playoffSeries
object: current playoff series related information (only present in playoff games), with the fields:round
(number): the game’s playoff round;0
for the Stanley Cup Qualifiers best-of-5 series (in 2020 due to COVID-19), actual playoffs start from1
wins
(object): each team’s win count in the series
streaks
object: each teams’ current form streak (only present in regular season games), with the fields (ornull
if the team hasn’t played during the season yet):type
(string):"WINS"
(wins in regulation, OT or SO),"LOSSES"
(losses in regulation) or"OT"
(losses in OT or SO)count
(number): streak’s length in consecutive games
standings
object: each teams’ standings related information, with the fields:divisionRank
(string): the team's regular season ranking in their division (based on point percentage); this comes as a string value from the NHL Stats APIleagueRank
(string): the team's regular season ranking in the league (based on point percentage); this comes as a string value from the NHL Stats APIpointsFromPlayoffSpot
(string): point difference to the last playoff spot in the conference- for teams currently in the playoffs, this is the point difference to the first team out of the playoffs; i.e. by how many points the team is safe
- for teams currently outside the playoffs, this is the point difference to the team in the last playoff spot (2nd wildcard position); i.e. by how many points (at minimum) the team needs to catch up
- Note: this value only indicates point differences and doesn’t consider which team is ranked higher if they have the same number of points
currentStats
object: each teams’ current (ie. after the game if it has finished and NHL have updated their stats) season statistics on the game date, with the fields:records
object: each teams’ record for this regular season, with the fields:wins
(number): win count (earning 2 pts)losses
(number): regulation loss count (0 pts)ot
(number): loss count for games that went to overtime (1 pt)
streaks
object (ornull
if querying coming season’s games): each teams’ current form streak (only present in regular season games), with the fields:type
(string):"WINS"
(wins in regulation, OT or SO),"LOSSES"
(losses in regulation) or"OT"
(losses in OT or SO)count
(number): streak’s length in consecutive games
standings
object (ornull
if querying coming season’s games): each teams’ standings related information, with the fields:divisionRank
(string): the team's regular season ranking in their division (based on point percentage); this comes as a string value from the NHL Stats APIleagueRank
(string): the team's regular season ranking in the league (based on point percentage); this comes as a string value from the NHL Stats APIpointsFromPlayoffSpot
(string): point difference to the last playoff spot in the conference- for teams currently in the playoffs, this is the point difference to the first team out of the playoffs; i.e. by how many points the team is safe
- for teams currently outside the playoffs, this is the point difference to the team in the last playoff spot (2nd wildcard position); i.e. by how many points (at minimum) the team needs to catch up
- Note: this value only indicates point differences and doesn’t consider which team is ranked higher if they have the same number of points
playoffSeries
object: current playoff series related information (only present in playoff games), with the fields:round
(number): the game’s playoff round;0
for the Stanley Cup Qualifiers best-of-5 series (in 2020 due to COVID-19), actual playoffs start from1
wins
(object): each team’s win count in the series
links
object: links to related pages on the official NHL site, with the optional fields:gameCenter
: game summary with lots of related infoplayoffSeries
: playoff series specific info (only present in playoff games)videoRecap
: 5-minute video recap (once available)
errors
array: list of data validation errors, only present if any were detected. Sometimes the NHL Stats API temporarily contains invalid or missing data. Currently we check if the goal data from the NHL Stats API (read from itsscoringPlays
field) contains the same number of goals than the score data (read from itsteams
field). If it doesn't, two different errors can be reported:{ "error": "MISSING-ALL-GOALS" }
: all goal data is missing; this has happened occasionally{ "error": "SCORE-AND-GOAL-COUNT-MISMATCH", "details": { "goalCount": 3, "scoreCount": 4 } }
: goal data exists but doesn't contain the same number of goals than the teams' scores; haven't noticed this happen but good to check anyway
Note on overtimes: Only regular season 5 minute overtimes are considered "overtime" in the
goals
array. Playoff overtime periods are returned as period 4, 5, and so on, since they are
20 minute periods. However, all games (including playoff games) that went into overtime are
marked as having ended in overtime in the scores
object.
- Java version 8
- Leiningen is used for all project management.
- Docker can be used optionally for running the application locally.
Using Docker
To run the application locally in Docker containers, install Docker and run:
./docker-up.sh
Downloading the Clojure image will take quite a while on the first run, but it will be reused after that.
To delete all containers, run:
./docker-down.sh
You can also run the application locally with lein run
.
Run tests with the Kaocha test runner for improved test failure reporting:
lein kaocha [--watch]
Run single tests or test groups with Kaocha's --focus
argument, e.g.:
lein kaocha --focus nhl-score-api.fetchers.nhlstats.game-scores-test/game-scores-parsing-scores
Or with the regular test runner:
lein test
The NHL API responses change from time to time, so the responses used in tests also need to be updated to remain accurate.
Especially the game-specific API responses need frequent updating, so there is a helper script to fetch the current responses with game IDs and save them. It's also useful for checking if the NHL API responses have changed in case of errors. Though note that not all data should be updated; at least game progress data changes should be discarded so that the tests that rely on that still work.
The script is called update-game-test-data.sh
and it uses curl
for fetching and jq
for formatting, so you'll need those installed.
Example:
$ ./scripts/update-game-test-data.sh 2023020205 2023020206
Fetching landing for game ID 2023020205
Landing response saved to test/nhl_score_api/fetchers/nhl_api_web/resources/landing-2023020205.json
Fetching right-rail for game ID 2023020205
Right-rail response saved to test/nhl_score_api/fetchers/nhl_api_web/resources/right-rail-2023020205.json
Fetching landing for game ID 2023020206
Landing response saved to test/nhl_score_api/fetchers/nhl_api_web/resources/landing-2023020206.json
Fetching right-rail for game ID 2023020206
Right-rail response saved to test/nhl_score_api/fetchers/nhl_api_web/resources/right-rail-2023020206.json
The API is deployed to Heroku from a development machine, no CI/CD setup. 😬
Usual deployment process:
# Bump version
lein release <:minor|:patch>
# Deploy to Heroku
./deploy.sh
# Push to Git
git push origin master --tags
The API responses are cached in-memory for one minute.
- Create a Java web app in Heroku
- Add and set up the New Relic APM Heroku add-on
- Copy the New Relic JAR files (
newrelic.jar
etc.) to thenewrelic
directory in this repository
- Copy the New Relic JAR files (
- Install and set up the Heroku CLI
- Install the Heroku Java CLI plugin:
heroku plugins:install java
# alternative if the above doesn't work:
heroku plugins:install @heroku-cli/plugin-java
- If you have multiple Heroku apps, set the default app for this repository:
heroku git:remote -a <heroku-app-name>
This project has been a grateful recipient of the Futurice Open Source sponsorship program.