---
title: Hockey API
description: Ice hockey data — leagues, teams with ELO ratings, matches with period scores, overtime/shootout flags and predictions.
badge: pro
---

# Hockey API

The Hockey API covers ice hockey (NHL and international leagues): leagues,
teams with ELO ratings, matches with period-by-period scores,
overtime/shootout flags, decimal odds and model-based predictions.

Base URL: `https://sports.bzzoiro.com/hockey/api/v2/`

## Access

Requires the **Sports Addon** ($5/month) — it unlocks the tennis, CS2, darts,
hockey, basketball and horse racing APIs plus their MCP servers. Get it at
[/addons/](/addons/). You also need a free account token from
[/register/](/register/).

```bash
Authorization: Token YOUR_API_KEY
```

Without a token you get **401**:

```json
{"error": "authentication required", "code": "authentication_required",
 "detail": "Send Authorization: Token <YOUR_TOKEN> on every request.",
 "register": "https://sports.bzzoiro.com/register/"}
```

With a token but no addon you get **402**:

```json
{"error": "Sports Addon required", "code": "addon_required",
 "detail": "Tennis, CS:GO, darts, hockey, basketball and horse racing APIs require the Sports Addon ($5/mo).",
 "pricing_url": "https://sports.bzzoiro.com/pricing/"}
```

List endpoints paginate with `limit` (default 50, max 200) and `offset`, and
return `{count, next, previous, results}`.

## Endpoints

| Endpoint | Description |
|---|---|
| `GET /hockey/api/v2/leagues/` | List leagues |
| `GET /hockey/api/v2/leagues/{id}/` | League detail |
| `GET /hockey/api/v2/teams/` | List/search teams with ELO |
| `GET /hockey/api/v2/teams/{id}/` | Team detail |
| `GET /hockey/api/v2/matches/` | List matches |
| `GET /hockey/api/v2/matches/live/` | Matches in play right now |
| `GET /hockey/api/v2/matches/{id}/` | Match detail with period scores and odds |
| `GET /hockey/api/v2/matches/{id}/odds/` | Per-bookmaker odds, every market |
| `GET /hockey/api/v2/matches/{id}/h2h/` | Head-to-head record and season form |
| `GET /hockey/api/v2/standings/` | League table |
| `GET /hockey/api/v2/predictions/` | Model win probabilities |
| `GET /hockey/api/v2/predictions/{id}/` | One prediction |

The machine-readable schema is public at `/hockey/api/schema/`, with an
interactive UI at [/hockey/api/docs/](/hockey/api/docs/) and a themed explorer
at [/docs/explorer/hockey/](/docs/explorer/hockey/).

## Leagues

**Query parameters**

| Param | Type | Description |
|---|---|---|
| `country` | string | ISO country code |
| `include_inactive` | bool | Include inactive leagues |
| `limit` / `offset` | int | Pagination (default 50, max 200) |

```json
{"count": 14, "results": [
  {"id": 3, "name": "NHL", "country": "USA"}
]}
```

## Teams

**Query parameters**

| Param | Type | Description |
|---|---|---|
| `search` | string | Name search |
| `country` | string | ISO country code |
| `limit` / `offset` | int | Pagination |

Responses are cached ~5 minutes.

```bash
curl -H "Authorization: Token YOUR_API_KEY" \
  "https://sports.bzzoiro.com/hockey/api/v2/teams/?search=rangers"
```

```json
{"count": 1, "results": [
  {"id": 118, "name": "New York Rangers", "short_name": "NYR",
   "country_code": "US", "elo_rating": 1671}
]}
```

## Matches

**Query parameters**

| Param | Type | Description |
|---|---|---|
| `league` | int | Filter by league id |
| `team` | int | Matches involving this team id |
| `status` | string | `scheduled`, `live`, `finished`, `awarded`, `postponed`, `cancelled` |
| `date_from` / `date_to` | date | `YYYY-MM-DD`. Without either, the list returns the next 7 days, oldest first. Any of the filters above lifts that window and returns the full archive, newest first. |
| `limit` / `offset` | int | Pagination |

```bash
curl -H "Authorization: Token YOUR_API_KEY" \
  "https://sports.bzzoiro.com/hockey/api/v2/matches/?team=118&status=finished&date_from=2026-04-01"
```

```json
{"count": 22, "results": [
  {"id": 40312, "league": {"id": 3, "name": "NHL"},
   "home_team": {"id": 118, "name": "New York Rangers", "short_name": "NYR"},
   "away_team": {"id": 121, "name": "Boston Bruins", "short_name": "BOS"},
   "match_date": "2026-04-12T23:00:00+00:00", "status": "finished",
   "round_name": "Regular season", "home_score": 4, "away_score": 3,
   "is_overtime": true, "is_shootout": false, "winner_id": 118}
]}
```

`GET /matches/live/` returns in-play matches only (cached 30 seconds).
`GET /matches/{id}/` adds period scores, the current period/minute for live
games and decimal odds:

```json
{"id": 40390, "status": "live",
 "home_score": 2, "away_score": 1,
 "periods_score": "1-0, 1-1", "current_period": 3, "current_minute": 7,
 "odds_home": 1.85, "odds_away": 1.95}
```

> **Note:** `is_overtime` and `is_shootout` tell you how a finished game was
> decided — useful for regulation-time markets, where an overtime win counts
> differently.

## Odds

`GET /hockey/api/v2/matches/{id}/odds/` returns every price we hold for one match: the
match-winner market flattened per bookmaker, and below it every other market the
books offered, grouped by kind, line and period.

```bash
curl -H "Authorization: Token YOUR_API_KEY" \
  "https://sports.bzzoiro.com/hockey/api/v2/matches/40390/odds/"
```

```json
{"match_id": 40390, "match_date": "2026-04-12T23:00:00+00:00",
 "home_team_name": "New York Rangers", "away_team_name": "Boston Bruins",
 "bookmakers_count": 14, "source": "multi",
 "bookmakers": [
   {"bookmaker": "Bet365", "bookmaker_slug": "bet365",
    "odds_home": 2.54, "movement_home": "SHORTENING",
    "odds_draw": 4.21, "movement_draw": "STABLE",
    "odds_away": 2.40, "movement_away": "DRIFTING",
    "updated_at": "2026-04-12T21:31:48Z"}
 ],
 "markets": [
   {"market_kind": "1X2", "market_family": "1X2", "market_line": null,
    "market_period": "FT", "selections": ["HOME", "DRAW", "AWAY"],
    "bookmakers": [
      {"bookmaker": "Bet365", "bookmaker_slug": "bet365",
       "prices": {"HOME": {"price": 2.54, "movement": "SHORTENING"},
                  "DRAW": {"price": 4.21, "movement": "STABLE"},
                  "AWAY": {"price": 2.40, "movement": "DRIFTING"}},
       "updated_at": "2026-04-12T21:31:48Z"}
    ]}
 ]}
```

| Field | Meaning |
|---|---|
| `source` | `multi` per-bookmaker prices, `consensus` the single stored price when the match is not yet linked to a priced feed event, `none` when nobody has priced it |
| `bookmakers_count` | Bookmakers with at least one match-winner price |
| `movement_*` | `SHORTENING`, `DRIFTING` or `STABLE` against the previous price we saw; `null` when there is no previous price |
| `market_kind` | Market type: `1X2`, `DNB`, `AH`, `OU` |
| `market_family` | Distinguishes markets sharing a kind (total goals vs corners vs a single side's total) |
| `market_line` | The handicap or total the market is quoted on; `null` for a plain winner market |
| `market_period` | `FT` for the full match; period codes otherwise |

> Prices come from several sources and are pooled per match. A bookmaker appears
> once per market with its most recent price.

## Standings

`GET /standings/?league={id}` ranks a league by points, then points per game,
then goal difference. Overtime results stay separate from regulation ones — in
hockey they are different events, and a table that folds them together cannot be
used for regulation-time markets.

```bash
curl -H "Authorization: Token YOUR_API_KEY" \
  "https://sports.bzzoiro.com/hockey/api/v2/standings/?league=3"
```

```json
{"league": {"id": 3, "name": "NHL", "country": "USA"},
 "standings": [
   {"position": 1, "team": {"id": 118, "name": "New York Rangers", "short_name": "NYR"},
    "matches_played": 82, "wins": 45, "losses": 27,
    "overtime_wins": 6, "overtime_losses": 4,
    "points": 100, "points_per_game": 1.22,
    "goals_scored": 254, "goals_conceded": 219, "goal_difference": 35,
    "form_string": "WWLOTLW", "current_streak": 2,
    "calculated_at": "2026-08-19T05:38:00+00:00"}
]}
```

The table is built from the daily form calculation, so `calculated_at` tells you
how fresh a row is. `league` is required.

## Predictions

**Query parameters**

| Param | Type | Description |
|---|---|---|
| `upcoming` | bool | Only matches not yet started |
| `limit` / `offset` | int | Pagination |

```json
{"count": 7, "results": [
  {"id": 5107, "match_id": 40395,
   "home_win_prob": 0.55, "away_win_prob": 0.45,
   "predicted_winner_id": 118, "confidence": 0.55}
]}
```

## Head-to-head and form

`GET /matches/{id}/h2h/` gives the record between the two teams and each side's
season form, split home and away. Everything is expressed in this match's terms
— `home` is the home team of *this* fixture, whichever way the record was
originally stored.

```bash
curl -H "Authorization: Token YOUR_API_KEY" \
  "https://sports.bzzoiro.com/hockey/api/v2/matches/16264/h2h/"
```

```json
{"match_id": 16264,
 "head_to_head": {"total_matches": 5, "home_wins": 4, "away_wins": 1,
   "home_goals": 15, "away_goals": 5, "avg_total_goals": 4.0,
   "recent_matches": [
     {"date": "2025-12-23T18:30:00+00:00", "winner": "home",
      "home_goals": 4, "away_goals": 1}]},
 "home_form": {"matches_played": 82, "wins": 45, "losses": 27,
   "overtime_wins": 6, "overtime_losses": 4, "points": 100,
   "points_per_game": 1.22, "form_string": "WWLOTLW", "current_streak": 2,
   "home": {"matches": 41, "wins": 26, "points_per_game": 1.44},
   "away": {"matches": 41, "wins": 19, "points_per_game": 1.0}},
 "away_form": {"...": "same shape"}}
```

`head_to_head` is `null` when the two teams have no recorded meeting, and a
`*_form` is `null` for a team with no season record yet.

## Coverage and off-season

Hockey runs October to June. Between those months the match list is correctly empty, and an empty list on its own cannot tell you whether the season is over or the sync is broken.

`GET /api/v2/coverage/?sport=hockey` says so directly, without a token:

```json
{"sport": "hockey", "status": "off_season", "events_next_7d": 0,
 "events_next_30d": 0, "priced_next_7d": 0, "live_now": 0,
 "next_event_at": null, "last_event_at": "2026-06-18T00:00:00+00:00"}
```

`status` is `in_season` (fixtures within seven days), `between_events` (nothing
this week, something later), `off_season` (nothing ahead and the last event is
more than a fortnight old) or `no_fixtures`. Drop the `?sport=` to get every
sport in one call — worth doing before a polling loop decides what to ask for.
