NHL Odds API: Puck Lines, Totals & Player Props (Python + Free Tier)

NHL Odds API - OddsPapi API Blog
How To Guides April 28, 2026

NHL Odds API: The Problem

The NHL doesn’t run a public odds API. DraftKings, FanDuel, BetMGM, and Caesars all carry NHL puck lines, totals, and player props — but none of them expose a developer-facing endpoint. Pinnacle has sharp NHL lines but shut down public API access for US retail. If you want to build anything around NHL odds — a line-shopping tool, an arb scanner, a player-prop model, a Closing Line Value tracker — your options have historically been:

  • Scrape sportsbook websites — fragile, rate-limited, and increasingly blocked by Cloudflare.
  • Pay enterprise feeds — Sportradar and Betgenius quote $3,000+/month for real-time hockey.
  • Use a generic odds API — most cap out at 20–40 books and paywall player props and historical data.

There’s a third option. OddsPapi aggregates NHL odds from 350+ bookmakers — including Pinnacle, Bet365, DraftKings, FanDuel, BetMGM, Caesars, BetRivers, and the full Betfair Exchange — on a free developer tier. This tutorial shows you exactly how to pull live moneylines, puck lines, totals, and player props in Python, with real API responses from a verified NHL game.

Old Way vs OddsPapi

Problem Scraping / Enterprise OddsPapi
NHL coverage Variable — depends on each book’s scraping defenses 112 books on a single game (verified)
Sharp lines (Pinnacle, Bet365) Pinnacle API is closed; Bet365 blocks scrapers Included on the free tier
Player props Paywalled on most APIs Goals, Assists, Points, Shots on Goal — all native
Historical puck lines Enterprise-only ($3k+/month) Free tier includes /historical-odds
Rate limits Get IP-banned after a few hundred requests ~0.88s cooldown, no bans
Setup time Weeks of scraper engineering 10 minutes

Step 1: Authenticate

OddsPapi uses a query-parameter API key, not a header. Grab a free key from the dashboard, then:

import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

# Smoke test
r = requests.get(f"{BASE_URL}/sports", params={"apiKey": API_KEY})
print(r.status_code)  # 200

nhl = [s for s in r.json() if s["sportId"] == 15]
print(nhl)
# [{'sportId': 15, 'slug': 'ice-hockey', 'sportName': 'Ice Hockey'}]

Every call needs apiKey as a query parameter. Ice hockey is sportId=15. NHL is one tournament within that sport — we’ll filter for it in Step 2.

Step 2: Fetch NHL Fixtures

The /fixtures endpoint returns every ice hockey game in a date range (max 10 days). Each fixture includes tournamentName and categoryName, so you can filter to NHL games only:

r = requests.get(
    f"{BASE_URL}/fixtures",
    params={
        "apiKey": API_KEY,
        "sportId": 15,
        "from": "2026-04-18",
        "to": "2026-04-28",
    },
)
fixtures = r.json()

nhl = [
    f for f in fixtures
    if f.get("tournamentName") == "NHL" and f.get("hasOdds")
]
print(f"NHL games with odds: {len(nhl)}")

for f in nhl[:5]:
    print(f"{f['participant1Name']:<25} vs {f['participant2Name']:<25} "
          f"{f['startTime']}  ({f['fixtureId']})")

Sample output:

NHL games with odds: 35
Carolina Hurricanes       vs Ottawa Senators           2026-04-18T19:00:00.000Z  (id1500087370712566)
Dallas Stars              vs Minnesota Wild            2026-04-18T21:30:00.000Z  (id1500054070718042)
Edmonton Oilers           vs Los Angeles Kings         2026-04-18T22:00:00.000Z  (...)

Two fields matter for filtering:

  • tournamentName == "NHL" — excludes AHL, KHL, international games, and college hockey
  • hasOdds == True — the fixture has at least one bookmaker price (otherwise /odds returns metadata only)

Step 3: Pull Live Moneylines

The /odds endpoint returns the current price from every bookmaker carrying the game. The response is deeply nested — bookmaker → market → outcome → players[“0”] → price. Here’s how to extract NHL moneylines across all books:

fixture_id = "id1500023470558100"  # Dallas vs Minnesota

r = requests.get(
    f"{BASE_URL}/odds",
    params={"apiKey": API_KEY, "fixtureId": fixture_id},
)
data = r.json()
books = data["bookmakerOdds"]
print(f"Books with odds: {len(books)}")

# Market 151 = "Winner (incl. overtime and penalties)" — NHL moneyline
# Outcome 151 = Home (participant1), 152 = Away (participant2)
MONEYLINE = "151"

print(f"{'Book':<18} {'Home':<10} {'Away':<10}")
for slug, book in sorted(books.items()):
    m = book.get("markets", {}).get(MONEYLINE, {})
    outs = m.get("outcomes", {})
    home = outs.get("151", {}).get("players", {}).get("0", {}).get("price")
    away = outs.get("152", {}).get("players", {}).get("0", {}).get("price")
    if home and away:
        print(f"{slug:<18} {home:<10} {away:<10}")

Live prices pulled just now for Dallas Stars vs Minnesota Wild:

Bookmaker Dallas (Home) Minnesota (Away)
pinnacle 1.847 2.07
bet365 1.83 2.00
draftkings 1.83 2.00
fanduel 1.83 2.00
caesars 1.833 2.00
betrivers 1.82 2.07
bovada.lv 1.82 2.02
betfair-ex 1.88 2.10

Note the line-shopping edge: Betfair Exchange is paying 1.88 on Dallas vs 1.82 at BetRivers — a 3.3% pricing gap on the same game. Running this query in a loop across your book list is the foundation of any value-betting system.

Pinnacle sits between the US retail books (1.83) and the exchange (1.88). That’s the classic signature of a sharp market versus a retail book carrying a shaded line.

Step 4: Puck Lines (±1.5 Goals)

The NHL standard puck line is ±1.5 goals, including overtime and shootout. OddsPapi uses two market IDs for the two sides:

  • 15228 — Handicap -1.5 (favorite giving 1.5 goals)
  • 15240 — Handicap +1.5 (underdog getting 1.5 goals)
# Puck line -1.5 (favorite) and +1.5 (underdog)
for slug, book in books.items():
    fav = book.get("markets", {}).get("15228", {}).get("outcomes", {}).get(
        "15228", {}).get("players", {}).get("0", {}).get("price")
    dog = book.get("markets", {}).get("15240", {}).get("outcomes", {}).get(
        "15241", {}).get("players", {}).get("0", {}).get("price")
    if fav or dog:
        print(f"{slug:<18} DAL -1.5: {fav}   MIN +1.5: {dog}")

# Sample output:
# pinnacle           DAL -1.5: 3.13   MIN +1.5: 3.6
# bovada.lv          DAL -1.5: 3.1    MIN +1.5: 1.4

Puck lines have much thinner coverage than moneylines — only Pinnacle, Bovada, and a handful of others carried this game’s ±1.5 at query time. If you’re building a puck-line arb scanner, filter your book list to the ones that actually reliably post this market.

Step 5: Totals (Over/Under)

NHL totals (incl. overtime) live under the Total (incl. overtime and penalties) market. Each line is its own market ID:

Line Market ID Over Outcome Under Outcome
5.5 15174 15174 15175
6.0 15176 15176 15177
6.5 15178 15178 15179
TOTALS = {
    5.5: (15174, 15174, 15175),
    6.0: (15176, 15176, 15177),
    6.5: (15178, 15178, 15179),
}

for slug in ["pinnacle", "caesars", "draftkings"]:
    b = books.get(slug, {})
    print(f"\n{slug}:")
    for line, (mid, over_oid, under_oid) in TOTALS.items():
        m = b.get("markets", {}).get(str(mid), {})
        over = m.get("outcomes", {}).get(str(over_oid), {}).get(
            "players", {}).get("0", {}).get("price")
        under = m.get("outcomes", {}).get(str(under_oid), {}).get(
            "players", {}).get("0", {}).get("price")
        if over and under:
            print(f"  O/U {line}: {over} / {under}")

# Sample output:
# pinnacle:
#   O/U 5.5: 1.854 / 2.05
#   O/U 6.0: 2.04 / 1.819
#   O/U 6.5: 2.27 / 1.671
# caesars:
#   O/U 6.0: 2.0 / 1.769
#   O/U 6.5: 2.2 / 1.667

Pinnacle’s middle total is 6.0 pricing roughly even juice (2.04 / 1.819), which tells you the model thinks this is a ~6-goal game. Caesars agrees on the number but slightly different juice. If you’re a totals bettor, Pinnacle is your benchmark — deviations from its line signal where the soft books are off.

Step 6: Player Props

This is where most generic odds APIs fall off a cliff. OddsPapi returns NHL player props natively — Goals, Assists, Points, Shots on Goal, Blocks, Saves, Power Play Points — with player names embedded in the response.

Player prop markets use the players dict differently: each player is their own key (not just "0"):

# Market 15140 = Over Under Player Shots On Goal, line 2.5 (incl. overtime)
# Outcome 15140 = Over, 15141 = Under
SHOTS_OVER_25 = "15140"

pin = books["pinnacle"]["markets"][SHOTS_OVER_25]["outcomes"]

print(f"{'Player':<28} {'Over 2.5':<10} {'Under 2.5':<10}")
over_players = pin["15140"]["players"]    # dict of {player_id: {...}}
under_players = pin["15141"]["players"]

for pid, over in over_players.items():
    if not over.get("active"):
        continue
    under = under_players.get(pid, {})
    name = over.get("playerName", "?")
    print(f"{name:<28} {over['price']:<10} {under.get('price', '-'):<10}")

Live Pinnacle shots-on-goal props for Dallas-Minnesota:

Player Over 2.5 SOG Under 2.5 SOG
Eriksson Ek, Joel 1.833 1.909
Hartman, Ryan 2.17 1.641
Rantanen, Mikko 2.06 1.709
Boldy, Matt 1.54 2.37
Johnston, Wyatt 1.769 1.98

Core NHL player-prop markets worth hardcoding:

Market ID Description Handicap
15124 Goals — Anytime Goalscorer 0.5
15118 Assists 0.5
15130 Points (G + A) 0.5
15138 Shots on Goal 1.5
15140 Shots on Goal 2.5
15142 Shots on Goal 3.5

Don’t hardcode everything. The full NHL market catalog lives at /v4/markets?sportId=15 (filter the response to sportId == 15 — the endpoint returns cross-sport data by default):

r = requests.get(f"{BASE_URL}/markets", params={"apiKey": API_KEY, "sportId": 15})
cat = [m for m in r.json() if m["sportId"] == 15]

print(f"Total NHL markets: {len(cat)}")
# Total NHL markets: 745

# Build a lookup for any market ID you see in the odds response
market_names = {
    m["marketId"]: f"{m['marketName']} (hcap={m['handicap']})"
    for m in cat
}

# Look up an unknown market ID from the odds response
print(market_names[15140])
# 'Over Under Player Shots On Goal (incl. overtime) (hcap=2.5)'

Step 7: Find the Best NHL Line Across 112 Books

Put it all together — a simple best-price scanner across every book carrying the NHL moneyline:

def best_price(books, market_id, outcome_id):
    best_book, best_price = None, 0
    for slug, b in books.items():
        p = (b.get("markets", {})
              .get(str(market_id), {})
              .get("outcomes", {})
              .get(str(outcome_id), {})
              .get("players", {})
              .get("0", {})
              .get("price"))
        if p and p > best_price:
            best_book, best_price = slug, p
    return best_book, best_price

# Best Dallas moneyline across all 112 books
home_book, home_odds = best_price(books, 151, 151)
away_book, away_odds = best_price(books, 151, 152)

print(f"Best Dallas: {home_odds} at {home_book}")
print(f"Best Minnesota: {away_odds} at {away_book}")

# Sample output:
# Best Dallas: 1.88 at betfair-ex
# Best Minnesota: 2.10 at betfair-ex

Now check for an arb. Implied probability = 1 / decimal_odds. If the sum of the two sides’ best-price implied probs is below 1.0, you have a risk-free opportunity:

arb = (1 / home_odds) + (1 / away_odds)
print(f"Book sum: {arb:.4f}")
# Book sum: 1.0083  → 0.83% overround, not an arb but tight

For a full arb scanner with multi-outcome staking math, see our dedicated Python arbitrage bot tutorial.

Historical NHL Odds (Free)

Every sharp bettor needs Closing Line Value. OddsPapi’s /historical-odds endpoint returns the full price history for any NHL fixture, free. The response shape differs from live odds — players["0"] is a list of snapshots, not a dict:

r = requests.get(
    f"{BASE_URL}/historical-odds",
    params={
        "apiKey": API_KEY,
        "fixtureId": fixture_id,
        "bookmakers": "pinnacle,bet365,singbet",  # max 3 per call
    },
)
hist = r.json()

# Walk the price history for the Dallas moneyline at Pinnacle
for snap in hist["bookmakers"]["pinnacle"]["markets"]["151"]["outcomes"]["151"]["players"]["0"]:
    print(f"{snap['createdAt']}  {snap['price']}")

# Sample output:
# 2026-04-17T14:12:03+00:00  1.92
# 2026-04-17T18:30:41+00:00  1.89
# 2026-04-18T09:02:15+00:00  1.85
# 2026-04-18T14:07:06+00:00  1.847   ← current

That’s steam money moving the Dallas line from 1.92 to 1.85 in 24 hours — exactly the kind of signal a line-movement alert bot looks for. For a full steam-move detector, see the steam move detector post.

Rate Limits & Production Notes

  • Cooldown: ~0.88s between calls to the same endpoint. Use time.sleep(0.2) between iterations as a safety buffer.
  • Date range: /fixtures accepts a max 10-day window. Loop across windows for season-long backfills.
  • Historical bookmakers: /historical-odds caps at 3 books per call. Loop with different combinations if you need more.
  • Active outcomes: check outcome.active == True — suspended markets ship with active: false and stale prices.
  • Exchange odds: Betfair Exchange and Polymarket are included. Their payloads add an exchangeMeta block with the lay side. Parse it defensively — the shape varies by book.

Why This Beats the Alternative

Every other NHL odds option forces a tradeoff:

  • The Odds API: 20 bookmakers, $119/mo for 5M calls, no player props on the base plan.
  • Sportradar: Enterprise-only, $3k+/month, 6-week contract cycle.
  • SportsGameOdds: 85 books, paywalls historical data, no sharp-book depth.
  • Scraping DraftKings directly: IP bans within hours, TOS violation.

OddsPapi is the only option that gives you Pinnacle + Bet365 + DraftKings + FanDuel + Betfair Exchange + 107 other books on one endpoint, on a free tier, with free historical data. That’s the pitch.

Stop Scraping. Start Building.

Grab a free API key and you’re pulling NHL puck lines, totals, and shots-on-goal props in the next 10 minutes. Historical data is included — backtest your model before you risk a dollar.

FAQ

Is there an official NHL odds API?

No. The NHL licenses official data partnerships for sports integrity and betting operators, but does not expose a public developer API for odds. OddsPapi aggregates odds from 350+ bookmakers covering every NHL regular-season and playoff game.

Does OddsPapi include DraftKings and FanDuel NHL odds?

Yes. Both DraftKings and FanDuel are covered on every NHL fixture we checked, alongside BetMGM, Caesars, BetRivers, Bovada, and the Betfair Exchange. Sharp books (Pinnacle, Bet365, Singbet) are also included on the free tier.

Are NHL player props available?

Yes. Goals, Assists, Points, Shots on Goal, Blocks, Power Play Points, Saves, and Goals Against are all native markets. Player names are embedded in the response — no extra lookup required.

Is historical NHL odds data free?

Yes. The /historical-odds endpoint is on the free tier. Every price change is timestamped, so you can compute Closing Line Value, track steam moves, or backtest any strategy against real NHL line history.

What’s the rate limit?

Approximately 0.88 seconds between calls to the same endpoint on the free tier. For production arb scanners or line monitors, OddsPapi also offers WebSocket push so you don’t have to poll.