---
title: "NHL player prop hit-rate + latest line across books"
method: GET
path: "/v1/nhl/players/{player_id}/props"
tags: ["nerd"]
---

# NHL player prop hit-rate + latest line across books

`GET /v1/nhl/players/{player_id}/props`

## Request

- Base URL: `https://api.bigballsdata.com`
- URL: `https://api.bigballsdata.com/v1/nhl/players/{player_id}/props`
- Auth: one of:
  - API key in header `x-api-key`
  - HTTP bearer

**Requires the Edge plan or higher.** Player props are part of the
odds and model layer; Free, Solo and Trio are refused with `403
forbidden`. Moved to Edge on 2026-09-25; previously free.

For one market (shots_on_goal/goals/assists/points; saves and
power_play_points have no game-log field, so they return a real
current_line with a null hit_rate), returns the latest line we hold picked by tightest
juice plus rolling-last-N hit rate from player_stats (box
stats). 60s cache. current_line null when no nhl_player_props row
exists; hit_rate null when the player has no stats bridge or no
history.

**Check `freshness` before treating a line as a current price.** Every
response carries a `freshness` object DERIVED from the returned line's
own `fetched_at`, never from the clock: `data_as_of` (the line's real
timestamp), `age_days`, and `status` (`live` ≤2d, `recent` ≤14d,
`stale` >14d, or `no_data` when no line exists). The top-level `as_of`
is when the RESPONSE was built and says nothing about the line's age.
Between seasons the newest lines can be months old and will report
`stale`; the historical lines and the hit rate computed over them
remain valid.

**`milestones`: yes/no prop prices, separate from `current_line`.**
A milestone prop asks whether a player reaches a threshold at all, so it
is priced as ONE American `odds` integer, not an over/under pair. Each
entry is `{ sportsbook, display, line, odds, fetched_at }`, one per
(book, threshold): `goals` with `line: 0.5` is the Anytime Goal Scorer
price, `1.5` is 2+ goals, and so on.

These are strictly ADDITIVE. `current_line`, `current_line.all_books`
and the `hit_rate` line are computed from over/under rows ALONE, so a
milestone price can never appear as a line: the query keys on
`market_type` rather than leaving it to a tiebreak. The array is empty
until the milestone half of the board is being stored.

## Path parameters

- `player_id` string, uuid, required

## Query parameters

- `market` 'shots_on_goal' | 'goals' | 'assists' | 'points' | 'saves' | 'power_play_points', required
- `lookback` integer
- `book` string: Sportsbook short-name (e.g. betrivers). Omit for tightest-juice pick. Selects the over/under line only; `milestones` always returns every book.

## Response `200`

ok

- object

## Other responses

- `400`: invalid query or player_id
- `403`: plan_required (Edge or higher)
- `404`: player not found
- `503`: Service temporarily unavailable (`503`). Returned when a backing query exceeds the endpoint's `statement_timeout` (Postgres cancels it) instead of holding the connection for seconds. Transient under heavy load; retry after a short delay. Standard error envelope with `error.code = upstream_unavailable`.

---

[API](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api.md) · [All operations](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/llms.txt) · [OpenAPI document](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/revisions/31fb54ac414e?raw)
