API Documentation

Everything you need to integrate PropLine into your app.

Building with an AI assistant? Pre-load the full PropLine reference into your model of choice:

Quick Start

Get up and running in 30 seconds:

  1. Get your API key from the signup form
  2. Make your first request:
curl "https://api.prop-line.com/v1/sports?apiKey=YOUR_API_KEY"

Or skip the curl boilerplate — PropLine ships official SDKs and tools for every common workflow:

Python SDK
pip install propline
Node / TS SDK
npm install propline
CLI new
npm install -g propline-cli
Tables in your terminal — GitHub
MCP server
npx -y propline-mcp
Claude Desktop / Code · GitHub

Authentication

Pass your API key via query parameter or header:

# Query parameter
curl "https://api.prop-line.com/v1/sports?apiKey=YOUR_API_KEY"

# Header
curl -H "X-API-Key: YOUR_API_KEY" "https://api.prop-line.com/v1/sports"

Tracking your usage

Every authenticated response carries your live quota state in headers — no separate call or dashboard visit needed:

X-Daily-Limit: 1000        # your tier's daily request cap
X-Daily-Used: 563          # requests used today (including this one)
X-Daily-Remaining: 437     # requests left before the cap
X-Daily-Reset: 1785542400  # unix seconds when the quota resets (00:00 UTC)

The same numbers are on the dashboard, with history. Quotas reset at 00:00 UTC — a hard reset, not a rolling window.

Standard rate-limit headers

The same quota is also published in the IETF RateLimit-* form and the de-facto X-RateLimit-* form, so a generic HTTP client or an AI agent can self-throttle without knowing anything PropLine-specific:

RateLimit-Limit: 5000
RateLimit-Remaining: 4437
RateLimit-Reset: 30840        # SECONDS until reset (per the RFC)
RateLimit-Policy: 5000;w=86400

X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4437
X-RateLimit-Reset: 1785542400 # unix timestamp

Note the two Reset spellings differ on purpose: the RFC header is a delta in seconds, the X- header is an absolute timestamp. Unauthenticated /v1/* responses advertise the free-tier policy only — remaining is unknowable without a key.

Both kinds of 429 (daily cap and per-second burst) carry Retry-After in seconds. Honour it instead of retrying immediately.

Versioning & deprecation

The API version is the first path segment (/v1/). New fields, books, markets and endpoints land inside v1 continuously — so ignore unknown fields, and never assume the bookmakers array holds an exact set of books. Removals and renames ship as a new version path, never in place.

Anything deprecated keeps serving for at least 12 months and carries RFC 8594 Deprecation and Sunset headers for the whole window, so an automated client can detect it without polling a page. Full policy: versioning & deprecation.

Bookmakers

Every odds response returns a bookmakers array so you can compare lines across books in a single request. Iterate the array to line-shop between sources.

KeyBookCoverage
bovadaBovadaAll 54 sports — game lines + full player props
draftkingsDraftKingsMLB, NBA, NHL, NFL, NCAAF, tennis + 9 soccer leagues — game lines + player props (NFL/NCAAF game lines now, player props at preseason; tennis includes match lines, total sets/aces + per-player aces and games-won props across active ATP/WTA tournaments)
fanduelFanDuelMLB, NBA, NHL, NFL, NCAAF + 9 soccer leagues — game lines + player props (NFL/NCAAF game lines now; player props at preseason)
betmgmBetMGMMLB, NBA, 17 soccer leagues — game lines + MLB player props (pitcher/batter). NJ skin (www.nj.betmgm.com)
betriversBetRiversMLB, NBA, NHL, 6 soccer leagues — full prop suite incl. NHL Player Points tiers, NBA double/triple-double YES, soccer To Score or Assist
pinnaclePinnacleMLB (game lines + props), NBA/NHL/NFL + 28 soccer leagues (game lines, goalie saves) — sharpest US-facing book
unibetUnibetMLB/NBA/NHL + 6 soccer leagues — game lines; NBA + NHL + soccer player props (points, rebounds, assists, threes, steals, blocks, PRA, shots on goal, goalscorer, cards, BTTS, total corners)
onexbet1xBetMLB, NBA, WNBA, NHL + 13 soccer leagues — game lines (h2h, spreads, totals)
tab_auTAB (Australia)MLB, NBA, WNBA, NHL + 13 soccer leagues — game lines (h2h, spreads, totals). Australia's largest licensed wagering operator.
prizepicksPrizePicks (DFS)MLB, NBA, NHL, 27 soccer leagues, tennis, UFC — player projections at synthetic +100/+100 even-money pricing (DFS payouts scale with parlay correct-count)
underdogUnderdog Fantasy (DFS)MLB, NBA, NHL, tennis, UFC + 9 soccer leagues — player props with real two-way American prices (e.g. -113/+101) and an optional payout multiplier on boosted picks
kalshiKalshi (event-contract exchange)MLB, NBA, NHL, NFL, NCAAF + EPL/La Liga/Serie A/Bundesliga/Ligue 1/MLS/Scottish Premiership — game-line h2h moneylines (NFL/NCAAF series load year-round; CFTC-regulated US exchange; binary YES/NO contracts converted to American odds)
polymarketPolymarket (prediction-market exchange)MLB, NBA, NHL + 13 soccer leagues — game-line h2h, multi-line totals, and spreads (US sports bundle all three; soccer is 3-way h2h re-assembled from binary YES/NO markets)
matchbookMatchbook (back/lay exchange)MLB, NBA, NHL + 17 soccer leagues + tennis + boxing — game lines (h2h, totals, spreads). Best back price per runner.
smarketsSmarkets (back/lay exchange)MLB, NBA, NHL + 13 soccer leagues — Match Winner + alt-total + alt-handicap lines. UK exchange with best back price per contract.
novigNovig (P2P exchange)MLB, NBA, WNBA, NHL + 13 soccer leagues — deepest per-event surface (MLB ~40 markets/event w/ full batter & pitcher prop suite, NBA ~140 markets/event incl. DOUBLE_DOUBLE / TRIPLE_DOUBLE, NHL player goals + saves). US peer-to-peer exchange; `last` field is implied probability converted to American odds.
prophetxProphetX (P2P exchange)MLB, NFL, WNBA, NBA, NHL, NCAAF, UFC + soccer — game lines, period lines (1st 5 innings, 1st inning, 1st half), team totals, and the full player-prop suite (MLB batter & pitcher props; NBA/WNBA points, rebounds, assists, threes + combos). US peer-to-peer exchange; we publish the best takeable price on each side of the order book.
betusBetUSMLB, NBA, WNBA, NHL, NFL (incl. preseason), NCAAF, CFL, UFC + 10 soccer leagues — game lines (h2h incl. 3-way soccer, spreads, totals, team totals; total rounds on UFC) + props: MLB pitcher outs, soccer anytime goalscorer / BTTS / double chance / correct score, UFC fight-to-go-the-distance + round betting. Offshore US-facing sportsbook.
betonlineagBetOnline.agMLB, NBA, WNBA, NHL, NFL, NCAAF, CFL, UFC, boxing + 24 soccer leagues — game lines (h2h incl. 3-way soccer, spreads, totals; total rounds on fights) + props: MLB pitcher strikeouts / outs + batter H+R+RBI, soccer BTTS / double chance / draw no bet / correct score, UFC fight-to-go-the-distance + round betting. Offshore US-facing sportsbook.
lowvigLowVig.agSame coverage as BetOnline.ag (same platform and fixtures) at reduced-juice prices — e.g. -108/-108 where BetOnline shows -110/-110.
mybookieagMyBookie.agMLB, NBA, WNBA, NHL, NFL, NCAAF, CFL, UFC, boxing + 23 soccer leagues — game lines (h2h incl. 3-way soccer, spreads, totals; total rounds on fights). Offshore US-facing sportsbook.
fanaticsFanaticsMLB, NBA, WNBA, NHL, NFL (incl. preseason), NCAAF, UFC, boxing, tennis + EPL, La Liga, Serie A, Ligue 1, MLS — game lines (h2h, spreads, totals, team totals, first 3/5/7-innings periods; total rounds and fight-to-go-the-distance on fights) + props: MLB batter hits / home runs / RBIs / runs / total bases / H+R+RBI + pitcher strikeouts, NBA & WNBA points / rebounds / assists / threes and every combo, soccer anytime + first goalscorer / score-or-assist / BTTS / double chance / correct score. Full US sportsbook; deep alt-line ladders.

More books are actively being added. You don't need to specify a bookmaker in requests — every response includes every book that carries lines for the requested market.

Timestamps & Data Guarantees

For CLV tracking, line-movement analysis, and any time-sensitive modeling, the meaning of every timestamp matters as much as the timestamp itself. This section lays out exactly what each field represents, what we guarantee, and where the underlying book APIs limit what's possible.

Two timestamps per snapshot

Every snapshot in /odds/history and every current outcome in /odds can carry up to two timestamps:

  • recorded_at (system-seen) — when our scraper observed and persisted this odds. Always populated. Sub-second wall-clock precision relative to our database.
  • book_updated_at (book-published)— when the book itself reports this odds was last set. Populated only for books that expose a publish-time signal in their API. Today that's Bovada (per-event lastModified); BetRivers, DraftKings, FanDuel, PrizePicks, Underdog, Kalshi, and Polymarket all return null.
  • last_change_at (our observed last move) — when PropLine last saw this price change. Unlike book_updated_at (the book's own publish-time, Bovada-only) this is populated for every book — Pinnacle and PrizePicks included — because we derive it ourselves. We only advance it when price_american actually moves, so it is a true "this line last changed at T" signal. Compare it across books on a single /odds call to detect repricing lag — e.g. Pinnacle just moved but a slower book's last_change_at is older, meaning it hasn't caught up yet — without a separate /odds/history call per event. It is also the relist signal: /odds never serves two generations of a relisted board at once (withdrawn selections leave the response, and the staler side of a self-contradicting ladder is withheld, judged by this timestamp) — and on the record endpoints, which keep every generation, a relist shows up as a sharp last_change_at break between adjacent rungs. A "main line" flag or a line count cannot carry that signal: some books flag every rung main, and DraftKings legitimately serves two live main lines on every MLB run line.
  • book_version (market version counter) — a monotonic integer the book bumps on every market update. Today only Pinnacle exposes one (`market.version`); other books return null. Captured at the moment of the snapshot/price change — comparing versions across two snapshots tells you how many distinct market updates the book recorded between them, even if only some changed the visible price.
  • payout_multiplier (DFS boost/discount) — the payout scaling factor on a daily-fantasy pick. Populated only for Underdog Fantasy, where every outcome carries a value; null means the book is not Underdog. A standard pick is 1.0 — its quoted price carries the full payout. A value like 0.75 (discount) or 1.5 (boost) scales the effective payout. When comparing DFS lines against sportsbook consensus, keep only payout_multiplier === 1.0 so a scaled "special" doesn't read as a mispriced edge. Filtering on non-null would drop every Underdog line.
  • dfs_odds_type (PrizePicks flavor) — PrizePicks posts up to three flavors of the same player prop: standard (the true market line), goblin (easier line, lower payout) and demon (harder line, higher payout). null for every traditional sportsbook. Filter to standard to get PrizePicks's market line; goblin/demon arrive as their own per-line markets (e.g. Points (demon 27.5)) so they never overwrite the standard line. PrizePicks publishes no numeric multiplier for these, so only the flavor is surfaced. The same dfs_odds_type field is present on /results (for DFS calibration audits), /odds/history, /odds/closing, and /movement.
  • line_gap (PrizePicks goblin/demon line delta)— the signed difference between a goblin/demon line and that player+stat's standard line (point − standard_point): positive on a harder demon line, negative on an easier goblin line. null unless the outcome is a PrizePicks goblin/demon with a standard counterpart on the slate. Since PrizePicks publishes no numeric per-pick multiplier, the flavor + this line gap are the modelable signals for fitting per-pick payout adjustments.
  • liquidity (exchange resting size) — the dollars a bettor can actually stake at the quoted price. Populated for exchange books that publish resting-offer size — ProphetX and Novig today; null for every other book, and for an exchange quote whose size the feed omitted (never coerced to 0). On a peer-to-peer exchange the best price is often a thin dangling offer with only a few dollars behind it, so filter or discount rows where liquidity is small before treating the price as bettable. Present on both /odds endpoints and on every price row of /best-line (where a thin exchange quote often wins the best slot on price alone). Refreshed every poll cycle independently of price movement; liquidity changes do not appear in /odds/history and do not fire line_movement webhooks. Pair it with liquidity_updated_at — the exchange's OWN timestamp for the resting order behind that size (for Novig, when the backing order last changed). ProphetX does not continuously re-publish resting size, so a rung keeps its last-known amount until something happens to it (measured 2026-08-23: median age 137s, but 27% of served rungs older than 20 minutes); Novig's size is re-read from its live order book every cycle. Drop or discount sizes whose backing order is older than your threshold — without the timestamp the number is untestable.
  • player_id (stable cross-book player id)— join the SAME player across books without name matching. It is the league's own permanent id, namespaced: mlb:592450 (MLBAM), nba:/wnba: (CDN personId), nhl: (playerId), and espn:8439 (ESPN athlete id — soccer, NFL, NCAAF). A real league id rather than a name-hash, so it distinguishes two players with the same name, is stable across trades and seasons, and cross-references to the league's own API. Present on player-prop markets only (always null on game lines and futures), unconditional (no query param), on both /odds endpoints and /results. It is null whenever we do not have a confirmed, unambiguous id — and never guessed, because a wrong join is worse than a missed one: a sport with no stable-id stats feed (tennis, golf, UFC, cricket, esports… — null forever), a player who has never graded, a book spelling that diverges from the league's (“Elmer Rodríguez” gets the id, “Elmer Rodriguez Cruz” stays null), or a name two players share. Coverage warms as games grade after launch.
  • GET /v1/dfs/payouts (DFS payout schedule + breakeven) — PrizePicks Power/Flex entry payout schedule (2–6 legs) plus the per-leg breakeven win probability for each play. Add ?leg_win_prob=0.58 to also get expected_return and is_plus_ev per play — turning a slip into the hit rate it actually needs to clear. Free reference math; standard published payouts only (demon/goblin per-pick modifiers aren't in PrizePicks's feed — see the disclaimer field).

The gap between the two — when populated on both sides — is your scraper-side latency for that book. We don't fabricatebook_updated_atfor books that don't expose it, because a fake value would be indistinguishable from a real one to your model.

Sequence preservation

Snapshots are stored with a monotonic recorded_at per outcome and read back in time order via the (outcome_id, recorded_at) index. Within an outcome, you will not see line movements out of order. Across outcomes within the same event, ordering is consistent because all markets ingested in a single poll cycle share that cycle's transaction commit time as their lower bound.

Snapshots are full states, not deltas

Every snapshot is a complete record of the price/point at that moment — never a delta against a prior snapshot. You can replay any subset of an outcome's history without needing other snapshots for context. Snapshots are written only on change: if a poll cycle returns the same price + point as the prior one, no new row is created. This keeps storage tight without losing information.

Polling cadence & missed ticks

We sweep each sport's books on a deadline-paced cycle. Any line move within a single cycle is collapsed into one snapshot at our next observation — we cannot recover intra-poll micro-moves the book may have flashed and reverted.

  • Pre-game multi-book sweep: 60 seconds on most sports. The cycle is max(60s, sweep time), so on the widest slates it is longer — MLB across 27 books measures ~90 seconds.
  • Live in-progress movement: 30 seconds target. Same rule — the cycle is max(30s, sweep time), and the live sweep across every in-play book measures ~40 seconds per book on a full MLB slate (60s on the slowest books).
  • Long-cycle low-volume sports: 60s sweep, less frequent per book

For most CLV and steam-detection workflows this is sufficient — the books themselves don't change pre-game lines faster than every 30–60 seconds in calm windows, and our ~40s live cadence covers most in-play movement. See /freshness for live per-book staleness. Webhooks remove the polling delay on top of that — we push the moment a change is ingested, so you learn about it without waiting for your next request — but they cannot beat the capture cadence itself. Our floor for seeing a move is one scrape cycle, so push gets you a 30–90s signal sooner, not a sub-second one.

Normalization vs raw

Prices pass through unchanged from the book — American odds are what the book quoted, decimal odds are computed deterministically from the American value. The fields we normalize across books arestructural, not numeric: team names, market keys, player names. The raw forms aren't exposed because the structural normalization is what makes cross-book comparison meaningful in the first place; if you need a specific book's raw shape, hit that book's feed directly.

Event IDs can change

The same fixture is often posted by different sportsbooks under different team-name spellings (“Paris Saint-Germain” vs “Paris St-G” vs “PSG”). We continuously merge those duplicates into one canonical event, which means an event_id you cached can be superseded by the id it was merged into.

Requests for a merged id are resolved automatically — you get the canonical event back with a 200, and the id in the response body is the canonical one. If you store event ids, update what you have to the returned id rather than re-sending the old one indefinitely.

A 404 on an id that previously worked means the same thing — re-fetch the event from /v1/sports/{sport}/events and use the current id. This matters most for long-running pollers: an id that no longer resolves will never start returning results on its own, so treat a persistent 404 as “re-discover the event” rather than “keep waiting.”

Uptime & status

Live uptime and incident history are public on the status page, on every tier including free. UptimeRobot polls api.prop-line.com/health every 5 minutes from infrastructure independent of ours, so the page stays reachable when the API is not.

Published uptime is not an SLA. A contractual availability guarantee with a financial remedy is an Enterprise term. On the self-serve tiers we publish the measurements continuously instead, so you can judge reliability from the record rather than a promise. Per-bookmaker data staleness is a separate signal — see Data Freshness.

Event Identity & Team Orientation

One fixture is one id, across every bookmaker. Books disagree about a great deal — how they spell a club, what time they say kickoff is, and, on some feeds, which side is at home — so this section states exactly which parts of an event are stable and which can legitimately change under a stable id. (Ids themselves are covered under Event IDs can change — an id you cached keeps resolving after a merge, but it then returns the surviving row's spelling and start time.)

Team names and start times can change

home_team and away_team are display names, not identifiers — do not key on them. They can be corrected (a short form replaced by the full club name, an accent restored) and they change on a merge. commence_time tracks the book's own scheduled start and moves when the fixture is re-timed. Note that each bookmaker block carries its book's OWN spelling in its outcome names, which is frequently not the event-level spelling. To join our rows onto a book's native feed, use ?includeBookIds=true and match on ids rather than names.

Orientation is normalized once, at the event level

home_team / away_team on the event is the single source of truth for which side is at home, and it applies to every bookmaker in the response. Individual books do not each get their own orientation: exchanges such as Kalshi and Polymarket publish no home/away signal at all, and some sportsbook feeds list the home side first, so we resolve one orientation per fixture and confirm it against the league's own scores feed once the game is played.

Outcome order is deterministic

Within a market, outcomes are returned in a fixed order that is a function of the data, never of storage:

  • Team markets (h2h, spreads, draw-no-bet…) — home first, then away, then Draw. This matches the-odds-api's convention.
  • Player and total markets — grouped by subject (the player), with Over/Yes before Under/No.

Reading a price by position is therefore safe. Matching by outcome name is safe too, but remember the name is the book's spelling.

Endpoints

Base URL: https://api.prop-line.com/v1

MethodEndpointDescription
GET/sportsList available sports
GET/sports/{sport}/eventsList upcoming events
GET/sports/{sport}/oddsBulk odds (game lines)
GET/sports/{sport}/events/{id}/oddsEvent odds + player props
GET/sports/{sport}/events/{id}/odds/historyHistorical line movement — period filters, downsample, change-only (Hobby+)
GET/sports/{sport}/events/{id}/odds/closingOpening + closing line per (book, market, outcome) — CLV helper (Hobby+)
GET/sports/{sport}/scoresGame scores & status
GET/sports/baseball_mlb/grand-salamiSynthetic daily Grand Salami (total runs + per-book line)
GET/sports/hockey_nhl/daily-goals-totalSynthetic daily NHL goals total (total goals + per-book line)
GET/sports/{sport}/events/{id}/resultsResolved prop outcomes (Hobby+)
GET/sports/{sport}/players/{name}/historyPlayer prop history with resolution (Hobby+)
GET/sports/{sport}/players/{name}/trendsPlayer hit-rate trends (L5/L10/L20/L50) (Hobby+)
GET/sports/{sport}/events/{id}/evCross-book +EV with no-vig fair lines (Hobby+)
GET/sports/{sport}/events/{id}/best-lineBest price per (market, player, line) across all books (Hobby+)
GET/sports/{sport}/futuresFutures markets — championship winner, MVP, etc.
GET/sports/{sport}/events/{id}/statsRaw player/team stats from box scores
GET/sports/{sport}/events/{id}/contextScores, stats and market list for one event in a single call
GET/sports/{sport}/events/{id}/movementLine movement + cross-book steam detection (Hobby+)
GET/sports/{sport}/events/{id}/marketsMarket keys available on one event — cheap discovery before a full odds pull
GET/sports/{sport}/events/{id}/ev/calcInteractive +EV calculator — your own price/stake against our fair line (Hobby+)
GET/dfs/payoutsDFS Power/Flex payout schedule + per-leg breakeven win probability
GET/markets/hit-ratesPer-market daily graded-Over counts over the last N days (free)
GET/markets/resolution-summaryGraded-prop volume per sport/market over the last N days (free)
GET/freshnessPer-book data-freshness report, split game lines vs props (free)
GET/exports/resolved-propsBulk CSV export of resolved props + closing lines (Pro)
GET/exports/odds-historyBulk CSV of the full line-movement tick history (Backfill / Enterprise)
GET/exports/sampleFree public CSV sample — last 7 days MLB K props
POST/webhooksCreate a push subscription — returns the signing secret once (Streaming)
GET/webhooksList subscriptions (secret masked) (Streaming)
PATCH/webhooks/{id}Update filters, URL or active flag (Streaming)
DELETE/webhooks/{id}Remove a subscription (Streaming)
POST/webhooks/{id}/testEnqueue a test payload (Streaming)
GET/webhooks/{id}/deliveriesLast 50 delivery attempts with status + response code (Streaming)
GET/webhooks/{id}/replayRe-read missed events from a cursor (Streaming)
WS/streamStream events over a websocket (Streaming Lite+)

List Sports

GET /v1/sports

Returns all available sports with their current status.

Response:

[
  {
    "key": "baseball_mlb",
    "title": "MLB",
    "active": true
  }
]

Migrating from the-odds-api?

Their sport key names work as aliases, so you only need to change the base URL — americanfootball_nfl, icehockey_nhl, soccer_spain_la_liga, mma_mixed_martial_arts and the rest resolve to our equivalents automatically.

Aliases exist only where the competition is genuinely the same. Where it isn't, you get a 404 that explains why rather than a silently-different feed — tennis_atp, for example, because PropLine covers tennis under a single tennis key spanning ATP, WTA and ITF/Challenger together.

{
  "error": "unknown_sport",
  "message": "Sport 'tennis_atp' is not available on PropLine.",
  "reason": "PropLine covers tennis under the single key 'tennis', which includes ATP, WTA and ITF/Challenger matches together...",
  "not_equivalent": true,
  "did_you_mean": ["tennis"],
  "sports_url": "https://api.prop-line.com/v1/sports"
}

List Events

GET /v1/sports/{sport_key}/events

Returns upcoming events for a sport. No odds included (use the odds endpoint for that).

Response:

[
  {
    "id": "10",
    "sport_key": "baseball_mlb",
    "home_team": "Colorado Rockies",
    "away_team": "Philadelphia Phillies",
    "commence_time": "2026-04-05T19:10:00Z",
    "home_team_key": "rockies",
    "away_team_key": "phillies",
    "home_team_id": "mlb:115",
    "away_team_id": "mlb:143",
    "merged_from_event_ids": ["7", "9"]
  }
]

home_team_key / away_team_key

A stable, permanent join key per team. Team namesare display strings — books spell them differently and the shown spelling can change — so if you store data per team, key it on these instead. Every spelling of a club resolves to the same key ("St Mirren FC", "St. Mirren" and "St Mirren Fc" are all st_mirren), and a key is never renamed once published.

Null when we can't identify the team with certainty (individual sports like tennis and golf, and a small tail of team sports) — fall back to the name there. Present on /events, both /odds endpoints and /scores.

home_team_id / away_team_id

The league's own permanent team id, namespaced by source — mlb:147 (MLBAM), espn.soccer:363, espn.nfl:12. Use home_team_key to key data inside PropLine; use this to join PropLine rows against external datasets keyed on the same league ids. Sourced from the stats feeds we grade against, never guessed — null where no confirmed id exists (individual sports, and team sports without a results feed).

merged_from_event_ids

When we discover that two of our rows are the same fixture we merge them into one, and the retired ids keep resolving (see Event IDs can change). This field is the other direction: it lists the ids that were merged INTO this event, so a client holding a stored id can reconcile it from the response it was already fetching rather than re-requesting every saved id one at a time.

Present on /events and both /odds endpoints, always — no flag to set. Omitted (null) for the large majority of events, which have never been merged. Ids are strings, matching id.

Get Odds (Bulk)

GET /v1/sports/{sport_key}/odds?markets=h2h,spreads,totals
    &bookmakers=draftkings,fanduel    # optional — omit for all books
    &includeLinks=true                # optional — event-page URLs per book
    &includeBookIds=true              # optional — each book's own event/selection ids

Returns odds for all upcoming events. Use the markets parameter to filter by market type (comma-separated). The optional bookmakers parameter (comma-separated book keys, same name as the-odds-api) restricts the response to specific books. Pass includeLinks=true (same name as the-odds-api) to add a link field on each bookmaker block — that book's public event-page URL, so your UI can click out to the book. Links ship for Bovada, DraftKings, FanDuel, BetMGM, Kalshi, Polymarket and Smarkets; other books return null. The same flag also adds an app_link — a mobile app-open deep link that opens the book's native app on that fixture (app-store fallback otherwise), vs link which is the desktop web page. ProphetX only today; null for other books.

Pass includeBookIds=true to get each book's own identifiers alongside our canonical ones: book_event_id on every bookmaker block and book_outcome_id on every outcome. This is the join key if you already hold a book's data — most commonly Kalshi, where you get the event ticker and the per-contract market ticker (e.g. KXMLBGAME-26AUG08NYYBOS-NYY) — so you can match on ids instead of fuzzing team names, player names and lines. Books that don't publish a stable id return null.

On PrizePicks, book_outcome_id is the projection id — the token a populated entry URL is built from. The direction and line are appended separately, and multiple picks are comma-separated: app.prizepicks.com/board?projections=14306846-o-23.5. Use -o- for the Over outcome and -u- for the Under, with the outcome's own point as the line.

Note that a two-sided market can share one book_outcome_id across both legs: a Kalshi contract is binary, so the Over and Under are the YES and NO sides of the same contract, and a PrizePicks projection is one pick whose direction lives in the URL rather than the id. The id identifies the contract or projection; the outcome's name tells you which side.

That is not uniform across books, so don't assume it either way. Underdog, Sleeper and Dabble ship the Over and Under as separate objects with separate ids, so each leg carries its own. Group a pair by (market, player, point) rather than by book_outcome_id.

Player Props (Per Event)

GET /v1/sports/{sport_key}/events/{event_id}/odds
    ?markets=pitcher_strikeouts,batter_hits,batter_home_runs
    &bookmakers=draftkings    # optional — omit for all books

This is the primary endpoint for player props. Pass one or more prop market keys via the markets parameter. Filter to specific books with bookmakers — also supported on /odds/history, /odds/closing and /movement. Add includeLinks=true for per-book event-page URLs (also on /best-line, where each price row carries the link — the click-out for “go bet this”). Add includeBookIds=true for each book's own event and selection ids (see Get Odds) — the fastest way to join prop legs onto Kalshi contract tickers.

Response:

{
  "id": "10",
  "sport_key": "baseball_mlb",
  "home_team": "Colorado Rockies",
  "away_team": "Philadelphia Phillies",
  "commence_time": "2026-04-05T19:10:00Z",
  "bookmakers": [
    {
      "key": "bovada",
      "title": "Bovada",
      "markets": [{
        "key": "pitcher_strikeouts",
        "last_update": "2026-04-05T18:46:30Z",
        "outcomes": [
          { "name": "Over",  "description": "Zack Wheeler",
            "price": -130, "point": 6.5 },
          { "name": "Under", "description": "Zack Wheeler",
            "price": 100,  "point": 6.5 }
        ]
      }]
    },
    {
      "key": "draftkings",
      "title": "DraftKings",
      "markets": [{
        "key": "pitcher_strikeouts",
        "last_update": "2026-04-05T18:46:42Z",
        "outcomes": [
          { "name": "Over",  "description": "Zack Wheeler",
            "price": -125, "point": 6.5 },
          { "name": "Under", "description": "Zack Wheeler",
            "price": 105,  "point": 6.5 }
        ]
      }]
    },
    {
      "key": "fanduel",
      "title": "FanDuel",
      "markets": [{
        "key": "pitcher_strikeouts",
        "last_update": "2026-04-05T18:46:38Z",
        "outcomes": [
          { "name": "Over",  "description": "Zack Wheeler",
            "price": -135, "point": 6.5 },
          { "name": "Under", "description": "Zack Wheeler",
            "price": 110,  "point": 6.5 }
        ]
      }]
    },
    {
      "key": "pinnacle",
      "title": "Pinnacle",
      "markets": [{
        "key": "pitcher_strikeouts",
        "last_update": "2026-04-05T18:46:45Z",
        "outcomes": [
          { "name": "Over",  "description": "Zack Wheeler",
            "price": -128, "point": 6.5 },
          { "name": "Under", "description": "Zack Wheeler",
            "price": 108,  "point": 6.5 }
        ]
      }]
    },
    {
      "key": "prizepicks",
      "title": "PrizePicks",
      "markets": [{
        "key": "pitcher_strikeouts",
        "last_update": "2026-04-05T18:46:50Z",
        "outcomes": [
          // DFS projection — synthetic +100 pricing on both sides.
          // Payout scales with parlay correct-count, not per-pick odds.
          { "name": "Over",  "description": "Zack Wheeler",
            "price": 100, "point": 6.5 },
          { "name": "Under", "description": "Zack Wheeler",
            "price": 100, "point": 6.5 }
        ]
      }]
    },
    {
      "key": "underdog",
      "title": "Underdog Fantasy",
      "markets": [{
        "key": "pitcher_strikeouts",
        // The book's own name for this market row.
        "description": "Strikeouts",
        // Which team this market is scoped to, or null for a game market.
        // On a `totals` market this is what separates the GAME total
        // (team: null) from a TEAM total (team: "Arsenal") — both ride the
        // `totals` key. Always null outside `totals`.
        "team": null,
        "last_update": "2026-04-05T18:46:52Z",
        // Null while the market is on the board. Set the moment the book
        // pulls it pregame (late scratch, a dropped market type) — the
        // outcomes are then the last quoted legs, not a live price. Clears
        // when the book lists it again. Push version: the market_suspended
        // webhook (Streaming).
        "suspended_at": null,
        "outcomes": [
          // Real two-way DFS prices. Every Underdog outcome carries a
          // payout_multiplier: 1.0 is a standard pick, anything else is a
          // boost/discount "special" — here the Over is a 1.5x boost, so
          // its effective payout is scaled.
          { "name": "Over",  "description": "Zack Wheeler",
            "price": -113, "point": 6.5, "payout_multiplier": 1.5 },
          { "name": "Under", "description": "Zack Wheeler",
            "price": 101,  "point": 6.5, "payout_multiplier": 1.0 }
        ]
      }]
    }
  ]
}

Historical Line Movement (Hobby+)

GET /v1/sports/{sport_key}/events/{event_id}/odds/history
    ?markets=pitcher_strikeouts        # default h2h,spreads,totals
    &relative_from=-3h                 # 3h before commence_time
    &relative_to=0                     # commence_time itself
    &interval=1m                       # one snapshot per minute per outcome
    &changes_only=true                 # drop unchanged buckets

Full snapshot history per outcome. Paid tiers get the raw stream; free tier sees market structure with redacted: true and a count of available snapshots.

Period-historical query params

All optional. Combine to scope, downsample, and de-noise — the same data you'd post-process today, computed server-side in one call.

  • from, to — absolute ISO timestamps. Bound the snapshot window directly.
  • relative_from, relative_to — offsets relative to commence_time. Forms: -3h, -30m, -90s, 0. Mutually exclusive with the absolute counterpart.
  • interval — downsample to one snapshot per bucket. One of 30s, 1m, 5m, 15m, 30m, 1h. We pick the last snapshot in each bucket.
  • changes_only — when true, drop rows whose (price, point) matches the previous row. The opening line is always kept.

Tier-gated depth by event age: Hobby = 30 days, Pro = 90 days, Streaming Lite = 180 days, Streaming = 365 days, Enterprise = unlimited. Older events return the redacted shape with an upgrade_url.

Opening & Closing Lines / CLV (Hobby+)

GET /v1/sports/{sport_key}/events/{event_id}/odds/closing
    ?markets=h2h,spreads,totals,pitcher_strikeouts

Both ends of the move in one call. Closing is the last snapshot per outcome at or before commence_time — the canonical line CLV-tracking tools measure against. opening_* is the first snapshot in the same 14-day pre-kickoff window. One call replaces the “fetch full history → find the first and last pre-game rows” pattern.

Response:

{
  "id": "5885",
  "sport_key": "baseball_mlb",
  "home_team": "Seattle Mariners",
  "away_team": "Texas Rangers",
  "commence_time": "2026-04-19T20:10:00Z",
  "bookmakers": [
    {
      "key": "draftkings",
      "title": "DraftKings",
      "markets": [
        {
          "key": "pitcher_strikeouts",
          "description": "Pitcher Strikeouts",
          "outcomes": [
            {
              "name": "Over",
              "description": "Bryan Woo",
              "price": 116,
              "point": 6.5,
              "closing_at": "2026-04-19T20:08:14Z",
              "closing_age_seconds": 106,
              "is_stale": false,
              "opening_price": 104,
              "opening_point": 6.5,
              "opening_at": "2026-04-17T13:02:51Z",
              "opening_age_seconds": 198429,
              "book_updated_at": null,
              "book_version": null
            },
            {
              "name": "Under",
              "description": "Bryan Woo",
              "price": -148,
              "point": 6.5,
              "closing_at": "2026-04-19T20:08:14Z",
              "opening_price": -128,
              "opening_point": 6.5,
              "opening_at": "2026-04-17T13:02:51Z"
            }
          ]
        }
      ]
    }
  ]
}

closing_at is the recorded_at of the snapshot we picked — useful for confirming the data point came from right before tip rather than hours earlier. Same per-tier event-age cap as /odds/history.

opening_age_seconds is how long before kickoff we first recorded the outcome. Read it before trusting an open: our archive starts April 2026, so for any book/sport we began polling after a line was posted, opening_* means first observed by us, not the book’s true open. A value of minutes rather than hours or days is the tell.

Grade Your Bets vs the Close (Hobby+)

POST /v1/clv/grade
Content-Type: application/json

[
  {"ref": "b1", "sport_key": "baseball_mlb", "event_id": 150791,
   "market": "batter_hits_runs_rbis", "bookmaker": "lowvig",
   "selection": "Drake Baldwin", "side": "Under", "point": 0.5,
   "price": 145, "stake": 1}
]

Send the bets you actually placed; get each back with its closing line, the de-vigged closing fair probability, CLV, and the graded result. Closing line value is the only durable proxy for whether a bettor has edge — did the price you took beat the number the market settled on? Stateless: nothing is stored.

Response:

{
  "summary": {
    "bets": 1, "matched": 1, "unmatched": 0, "graded": 1, "pending": 0,
    "avg_clv_pct": 6.52, "avg_ev_vs_close_pct": 0.08,
    "beat_close_pct": 100.0, "profit_units": -1.0
  },
  "bets": [
    {
      "ref": "b1",
      "matched": true,
      "unmatched_reason": null,
      "closing_price": 130,
      "closing_point": 0.5,
      "closing_at": "2026-08-21T20:09:12Z",
      "closing_is_stale": false,
      "closing_is_final": true,
      "fair_source": "pinnacle",
      "closing_fair_prob": 0.4085,
      "clv_pct": 6.52,
      "ev_vs_close_pct": 0.08,
      "beat_close": true,
      "resolution": "lost",
      "actual_value": 1.0
    }
  ]
}

Two CLV numbers, deliberately. clv_pct is price-vs-price — familiar and quotable, but vig-blind, so it flatters a bet taken on the juicy side of a wide market. ev_vs_close_pct scores your price against the de-vigged close and is the honest one: a -110 taken into a -105/-115 close beat the price but not the fair line, and only the second number says so.

The de-vig uses the sharpest book quoting that line at close (Pinnacle first, then Polymarket / Kalshi / Bovada / Smarkets, falling back to your own book), reported as fair_source. De-vigging the book you bet at always returns a negative number — you paid its hold — which says nothing about whether you found value.

Matching is fail-closed. A bet we cannot pin to exactly one stored outcome returns matched: false with an unmatched_reason rather than a guess — a confident wrong match would report a real-looking CLV for a different bet. Lines match by equality, never nearest-value.

Bets on events that have not started carry closing_is_final: false and are excluded from the summary averages — a pre-kickoff “closing” price is just the latest price. Max 500 bets per request. Free tier sees the full structure with every number nulled.

Period Markets

Markets bucketed by game period — 1st quarter spread, 2nd half total, 1st period puck line, 6th inning run line. Every odds endpoint accepts an optional period query param. Omitted = full game (default; backwards-compatible). Pass canonical codes or all to opt in.

# Last 30 min of 1st-half spread movement before tip
GET /v1/sports/basketball_nba/events/{event_id}/odds/history
    ?markets=spreads&period=h1&relative_from=-30m

# Closing line for the 1st-quarter total
GET /v1/sports/basketball_nba/events/{event_id}/odds/closing
    ?markets=totals&period=q1

# Every period (quarters + halves + full game) in one response
GET /v1/sports/basketball_nba/events/{event_id}/odds?period=all

Canonical period codes

CodeMeaningApplies to
q1, q2, q3, q4QuartersNBA, WNBA, NCAAB, NFL, NCAAF
h1, h2HalvesBasketball, football, soccer
p1, p2, p3PeriodsNHL
i1 … i9InningsMLB
f3, f5, f7First N inningsMLB
map1 … map7MapsEsports (per-map game lines + total_kills / kills_handicap / odd_even_total_kills / first_team_to_n_kills)
s1 … s5SetsTennis, volleyball (per-set h2h / spreads / totals)
g1 … g7GamesTable tennis, badminton (per-game h2h / spreads / totals)

Every market row in the response carries a period field (string or null for full game), so consumers can branch on it without re-parsing the URL.

Coverage today: Bovada, DraftKings, FanDuel, and Pinnacle all carry period markets across NBA / NHL / MLB / soccer. Football (NFL / NCAAF) period markets land alongside the September player-prop rollout.

Game Scores

GET /v1/sports/{sport_key}/scores?days_from=3

Returns game scores and status for recent events. Free tier. Use days_from to control how many days back to include (default: 3).

Response:

[
  {
    "id": "16",
    "sport_key": "baseball_mlb",
    "home_team": "Detroit Tigers",
    "away_team": "St. Louis Cardinals",
    "commence_time": "2026-04-05T23:20:00Z",
    "status": "final",
    "home_score": 3,
    "away_score": 5
  }
]

MLB Grand Salami

GET /v1/sports/baseball_mlb/grand-salami?date=2026-05-22

Synthetic daily Grand Salami for MLB — total runs scored across every game on a given UTC date, plus each book's implied Grand Salami line (sum of their primary game totals). No retail book quotes this as a single market, so cross-book historical data isn't available elsewhere. Free tier. Default date is today (UTC).

Response:

{
  "sport_key": "baseball_mlb",
  "date": "2026-05-22",
  "games_total": 15,
  "games_completed": 15,
  "games_in_progress": 0,
  "games_upcoming": 0,
  "actual_total_runs": 142,
  "bookmakers": [
    { "key": "bovada", "title": "Bovada", "games_priced": 15, "line": 134.5, "result": "over" },
    { "key": "draftkings", "title": "DraftKings", "games_priced": 15, "line": 135.0, "result": "over" },
    { "key": "pinnacle", "title": "Pinnacle", "games_priced": 15, "line": 133.5, "result": "over" }
  ]
}

NHL Daily Goals Total

GET /v1/sports/hockey_nhl/daily-goals-total?date=2026-05-24

Synthetic daily goals total for NHL — total goals scored (incl. OT/SO) across every NHL game on a given UTC date, plus each book's implied Daily Goals Total line (sum of their primary game totals). Hockey's equivalent of the MLB Grand Salami; no retail book quotes it as a single market. Free tier. Default date is today (UTC).

Response:

{
  "sport_key": "hockey_nhl",
  "date": "2026-05-24",
  "games_total": 4,
  "games_completed": 4,
  "games_in_progress": 0,
  "games_upcoming": 0,
  "actual_total_goals": 23,
  "bookmakers": [
    { "key": "bovada", "title": "Bovada", "games_priced": 4, "line": 24.5, "result": "under" },
    { "key": "draftkings", "title": "DraftKings", "games_priced": 4, "line": 24.0, "result": "under" },
    { "key": "pinnacle", "title": "Pinnacle", "games_priced": 4, "line": 24.5, "result": "under" }
  ]
}

Prop Results (Hobby+)

GET /v1/sports/{sport_key}/events/{event_id}/results
    ?markets=pitcher_strikeouts,batter_hits

Returns resolved prop outcomes with actual player stats. Hobby+ (all paid tiers). Each outcome includes whether it won, lost, or pushed, plus the actual stat value.

Response:

{
  "id": "16",
  "sport_key": "baseball_mlb",
  "home_team": "Detroit Tigers",
  "away_team": "St. Louis Cardinals",
  "status": "final",
  "home_score": 3,
  "away_score": 5,
  "bookmakers": [{
    "key": "bovada",
    "title": "Bovada",
    "markets": [{
      "key": "pitcher_strikeouts",
      "description": "Total Strikeouts - Tarik Skubal (DET)",
      "outcomes": [
        {
          "name": "Over",
          "description": "Tarik Skubal (DET)",
          "price": -150,
          "point": 6.5,
          "resolution": "won",
          "actual_value": 7.0,
          "resolved_at": "2026-04-06T03:15:00Z"
        },
        {
          "name": "Under",
          "description": "Tarik Skubal (DET)",
          "price": 120,
          "point": 6.5,
          "resolution": "lost",
          "actual_value": 7.0,
          "resolved_at": "2026-04-06T03:15:00Z"
        }
      ]
    }]
  }]
}

Resolution values: won, lost, push, void (player scratched)

Cross-book +EV (Hobby+)

GET /v1/sports/{sport_key}/events/{event_id}/ev
    ?markets=pitcher_strikeouts,batter_hits   # optional
    &bookmakers=draftkings,fanduel            # optional

For each (market, player, line) on an event, derives a no-vig fair line from a sharp anchor (Pinnacle preferred, Bovada fallback) and returns the expected value (EV%) for every other book's price at the same line. Outcomes are sorted with +EV plays floated to the top. Hobby+ (all paid tiers).PrizePicks is excluded — its synthetic +100/+100 prices aren't payout odds.

bookmakers narrows the prices to the books you hold accounts at (the-odds-api-compatible, same as on the odds endpoints). It never narrows the anchor. bookmakers=draftkings still returns DraftKings EV% measured against Pinnacle — that comparison is the point of the endpoint. Lines where none of your books quote a price are omitted.

Which book anchored a line is never hidden. Every line carries fair_source naming the book the no-vig price came from, and the response carries fair_source_default with the full fallback order. The anchor is chosen per line, so one response routinely mixes several — read fair_source per line rather than assuming Pinnacle anchored all of them.

Response:

{
  "id": "12345",
  "sport_key": "baseball_mlb",
  "home_team": "Yankees",
  "away_team": "Red Sox",
  "commence_time": "2026-04-25T23:05:00Z",
  "fair_source_default": "pinnacle → polymarket → kalshi → bovada (first available per market)",
  "lines": [
    {
      "market_key": "pitcher_strikeouts",
      "description": "Gerrit Cole",
      "point": 6.5,
      "fair_source": "pinnacle",
      "fair_probs": { "Over": 0.52, "Under": 0.48 },
      "outcomes": [
        { "book": "fanduel", "book_title": "FanDuel",
          "name": "Over", "price": 110, "ev_pct": 9.20,
          "is_plus_ev": true },
        { "book": "draftkings", "book_title": "DraftKings",
          "name": "Over", "price": -105, "ev_pct": 1.30,
          "is_plus_ev": true },
        { "book": "pinnacle", "book_title": "Pinnacle",
          "name": "Over", "price": -110, "ev_pct": -3.80,
          "is_plus_ev": false }
      ]
    }
  ]
}

Lines without sharp-anchor coverage on this event (or single-tier YES markets like “10+ Strikeouts” that can't be no-vig fair-derived from one price) are dropped from the response.

Score your own price against the same fair line:

GET /v1/sports/{sport_key}/events/{event_id}/ev/calc
    ?market=pitcher_strikeouts
    &name=Over                    # team name for h2h/spreads; Over/Under otherwise
    &point=6.5                    # line; omit for h2h
    &description=Tarik Skubal     # player name; omit for game lines
    &price=-105                   # American odds at YOUR book
{
  "market": "pitcher_strikeouts",
  "name": "Over",
  "point": 6.5,
  "description": "Tarik Skubal",
  "price": -105,
  "fair_source": "pinnacle",
  "fair_prob": 0.5432,
  "implied_prob": 0.5122,
  "ev_pct": 6.05,
  "is_plus_ev": true
}

Same Pinnacle-preferred no-vig anchor as /ev, scored against a price you supply instead of the books we carry — for a number quoted at a book we don't poll (Caesars, Hard Rock, bet365…). Full-game markets only. A (market, point, name) tuple with no fair-anchored line returns 404 listing the outcome names that do exist. There is a UI at /ev-calculator.

Market-Implied Projections (Hobby+)

GET /v1/sports/{sport_key}/events/{event_id}/projections
    ?markets=player_pass_yds,player_receptions   # optional

One row per (market, player): the statistical value the betting market collectively implies — the line where the no-vig probability of Over crosses 50%, taken as the median across contributing sportsbooks. Built for validating your own projections against the live market. Hobby+ (all paid tiers). Free tier sees the structure with values redacted.

These are market-implied values, not forecasts — pure arithmetic over sportsbook prices (the same de-vig math as /ev). DFS pick'em pricing is excluded; only genuine two-way Over/Under pairs contribute. Where a book posts a ladder of two-way alt lines, the 50% crossing is interpolated; a single line contributes the line with a bounded juice-skew nudge.

Response:

{
  "id": "25070",
  "sport_key": "football_nfl",
  "home_team": "Seattle Seahawks",
  "away_team": "New England Patriots",
  "commence_time": "2026-09-10T00:20:00Z",
  "method": "market-implied: per book, the line where the no-vig P(over) crosses 50% ...",
  "projections": [
    { "market_key": "player_pass_yds", "player": "Drake Maye",
      "player_id": "espn:4431452",
      "projected_value": 246.9, "consensus_over_prob": 0.512,
      "books_contributing": 4,
      "last_update": "2026-08-27T14:59:02Z" },
    { "market_key": "player_receptions", "player": "Jaxon Smith-Njigba",
      "player_id": "espn:4430878",
      "projected_value": 6.4, "consensus_over_prob": 0.487,
      "books_contributing": 3,
      "last_update": "2026-08-27T14:58:41Z" }
  ]
}

player_id is the same stable cross-book id served on /odds prop outcomes (null until the player has graded at least once). Players are grouped by exact-normalized name, so a divergent cross-book spelling appears as a separate row with fewer contributing books rather than a guessed merge.

Best Line — Cross-book Line Shopping (Hobby+)

GET /v1/sports/{sport_key}/events/{event_id}/best-line
    ?markets=pitcher_strikeouts,h2h      # optional
    &bookmakers=draftkings,fanduel,bovada  # optional — shop only your books

The companion to /ev: once you've decided what to bet, this tells you which book pays the most. For every (market, player, line) tuple on the event, returns the single best American price across every comparable book, plus an all_prices array sorted best-first (one row per book) so you can render runner-ups without a second request. Hobby+ (all paid tiers) for prices. Free tier sees the full structure — every line, side, book identity, and the best-first ranking — with prices nulled and redacted: true. DFS pick'em books (PrizePicks, Sleeper, Dabble) are excluded — their quotes aren't independently bettable payouts; Underdog is included only at its clean two-way lines.

Response:

{
  "id": "12345",
  "sport_key": "baseball_mlb",
  "home_team": "Yankees",
  "away_team": "Red Sox",
  "commence_time": "2026-07-19T23:05:00Z",
  "books_considered": ["bovada", "draftkings", "fanduel", "pinnacle"],
  "lines": [
    {
      "market_key": "pitcher_strikeouts",
      "description": "Gerrit Cole",
      "point": 6.5,
      "sides": {
        "Over": {
          "best": { "book": "fanduel", "book_title": "FanDuel",
                    "price": 110, "last_update": "2026-07-19T21:04:11Z" },
          "all_prices": [
            { "book": "fanduel", "book_title": "FanDuel", "price": 110,
              "last_update": "2026-07-19T21:04:11Z" },
            { "book": "draftkings", "book_title": "DraftKings", "price": -105,
              "last_update": "2026-07-19T21:03:58Z" },
            { "book": "bovada", "book_title": "Bovada", "price": -115,
              "last_update": "2026-07-19T21:04:02Z" }
          ]
        },
        "Under": { "...": "same shape" }
      }
    }
  ]
}

Books quoting a different line land in separate entries — FanDuel's Cole 6.5 isn't directly comparable to DK's 7.5. Each price carries last_update so you can discount quotes from a book that went stale mid-slate. Full-game markets only.

Player Prop History (Hobby+)

GET /v1/sports/{sport_key}/players/{player_name}/history
    ?market=pitcher_strikeouts
    &bookmaker=draftkings   # optional
    &limit=20               # default 20, max 100

Returns a player's recent resolved props for a given market — line, Over/Under prices, and whether each side won or lost. Hobby+ (all paid tiers) for resolution data. Free tier sees event structure with redacted: true.

Response:

{
  "player_name": "Bryan Woo",
  "sport_key": "baseball_mlb",
  "market": "pitcher_strikeouts",
  "entries": [
    {
      "event_id": "5885",
      "commence_time": "2026-04-19T20:10:00Z",
      "home_team": "Seattle Mariners",
      "away_team": "Texas Rangers",
      "bookmaker": "draftkings",
      "bookmaker_title": "DraftKings",
      "line": 6.5,
      "over_price": 116,
      "under_price": -148,
      "actual_value": 6.0,
      "over_result": "lost",
      "under_result": "won",
      "resolved_at": "2026-04-19T23:18:45Z",
      "redacted": false
    }
  ]
}

One entry per (event, bookmaker) pair. Name match is case-insensitive prefix, so both Bryan Woo and bryan woo work.

Player Game Log & H2H

GET /v1/sports/{sport_key}/players/{player_name}/games
    ?limit=20            # optional — 1..100, default 20
    ?opponent=Red Sox    # optional — head-to-head; team name or abbreviation
    ?stat_type=hits,rbis # optional — comma-separated, omit for all

A player's recent games with every raw box-score stat per game — one call instead of one request per event. Build L5 / L10 / L20, season splits, charts and head-to-head from the raw rows. Free tier.

This is the raw-stat archive, not graded-prop history. It covers every game we hold a box score for, including games no sportsbook posted a market on — so unlike a window built from /history or /trends, a "last 10 games" here is genuinely the last 10 games. It carries no line, price or grade; use /trends for hit rates against a posted line.

Response:

{
  "player_name": "Aaron Judge",
  "sport_key": "baseball_mlb",
  "opponent": "Boston Red Sox",
  "games": [
    {
      "event_id": "31882",
      "commence_time": "2026-04-23T22:10:00Z",
      "status": "final",
      "home_team": "Boston Red Sox",
      "away_team": "New York Yankees",
      "home_score": 3,
      "away_score": 5,
      "team_abbr": "NYY",
      "player_team": "New York Yankees",
      "opponent": "Boston Red Sox",
      "is_home": false,
      "stats": {
        "hits": 1.0,
        "total_bases": 1.0,
        "walks": 1.0,
        "rbis": 1.0,
        "home_runs": 0.0,
        "at_bats": 3.0
      }
    }
  ]
}

opponent= accepts a full name, a nickname or an abbreviation ("Boston Red Sox", "Red Sox", "BOS"), and the limit applies after the filter — so ?opponent=BOS&limit=10 is the last 10 meetings, not the Boston games among the last 10 games. H2H is not capped to the current season; it reaches as far back as the archive holds.

player_team, opponent and is_home are null when the player's side can't be identified from the box score's team abbreviation, and always for individual sports (tennis, golf, UFC) which have no home side. They are left null rather than guessed — a wrong home/away flag would corrupt every split built on it. Stat names are per-sport; see Player Stats.

Resolution Coverage (Free)

GET /v1/markets/resolution-summary
    ?days=30   # 1-90, default 30

Aggregated counts only — the factual volume of player props we've graded against real box scores over the window. Free tier. A coverage proof, not a profitability claim: the-odds-api grades nothing, and OddsJam only grades single bets on request — neither ships a pre-graded dataset like this.

Response:

{
  "days": 30,
  "total_graded": 712043,
  "total_settled": 698811,
  "events_graded": 4120,
  "sports_covered": 33,
  "by_sport": [
    { "sport_key": "baseball_mlb", "title": "MLB",
      "graded": 386415, "events": 383 }
  ],
  "top_markets": [
    { "market_key": "batter_total_bases", "graded": 48210 }
  ]
}

total_graded includes voids; total_settled is won/lost/push only. top_markets is capped at 12.

Futures Markets

GET /v1/sports/{sport_key}/futures

Season-long markets — World Series winner, NBA championship, Stanley Cup, league MVP, division winners, etc. One entry per (futures event, book, market), with every team or player priced. Free tier. Polled hourly.

Response:

[
  {
    "id": "12345",
    "sport_key": "baseball_mlb",
    "title": "World Series 2026",
    "commence_time": "2026-10-31T00:00:00Z",
    "markets": [
      {
        "key": "world_series_winner",
        "description": "World Series Winner",
        "bookmaker": "bovada",
        "bookmaker_title": "Bovada",
        "last_update": "2026-04-27T15:30:51Z",
        "book_updated_at": "2026-04-27T15:14:11Z",
        "outcomes": [
          { "name": "Los Angeles Dodgers", "price": 180, "price_decimal": 2.8 },
          { "name": "New York Yankees", "price": 725, "price_decimal": 8.25 },
          { "name": "Seattle Mariners", "price": 1200, "price_decimal": 13.0 }
        ]
      },
      {
        "key": "regular_season_wins",
        "description": "Seattle Mariners Total Regular Season Wins",
        "bookmaker": "pinnacle",
        "bookmaker_title": "Pinnacle",
        "last_update": "2026-09-28T15:30:51Z",
        "book_updated_at": "2026-09-28T15:14:11Z",
        "outcomes": [
          { "name": "Over", "description": "Seattle Mariners", "point": 85.5,
            "price": -110, "price_decimal": 1.91,
            "resolution": "won", "actual_value": 90.0,
            "settled_at": "2026-09-30T09:12:04Z" },
          { "name": "Under", "description": "Seattle Mariners", "point": 85.5,
            "price": -110, "price_decimal": 1.91,
            "resolution": "lost", "actual_value": 90.0,
            "settled_at": "2026-09-30T09:12:04Z" }
        ]
      }
    ]
  }
]

Available sports: MLB, NBA, NHL, NFL, NCAAB, NCAAF. Market keys are slugified from the book's description with year suffixes dropped, so they're stable across seasons — world_series_winner, stanley_cup_winner, nba_mvp. Filter client-side on market.key to find the one you want.

Settlement. The futures a final regular-season table decides — team win totals, division winners and the conference #1 seed on NFL, NBA and MLB — settle in the weeks after that regular season ends. Those outcomes gain resolution (won/lost/push), actual_value (the figure settled against — a team's season wins for a win total, 1/0 for a yes-style outright) and settled_at.

Everything else stays unsettled and those three fields arenull. A championship, a pennant or a conference title is not in any standings table, and awards (MVP, Coach of the Year) are published by no free feed — we would rather leave those honestly ungraded than guess. College-football win totals are excluded for a specific reason: the available feed's win count includes the conference championship game, which the books' markets exclude.

Optional ?bookmakers= (comma-separated book keys, e.g. bookmakers=bovada,draftkings) narrows the per-book market rows — same the-odds-api-compatible filter as every odds endpoint: omitted returns all books, unknown keys match nothing. A futures event left with no matching market is dropped.

Player Stats

GET /v1/sports/{sport_key}/events/{event_id}/stats

Returns raw player and team stats from official box scores. This data is book-agnostic — use it to resolve props from any sportsbook, build your own models, or power research dashboards. Free tier.

Live during games (all major US sports): for in-progress MLB and WNBA games today — plus NFL, NCAAF, NBA, and NHL as their seasons start — stats update roughly every 90 seconds with cumulative in-game values — a strikeout or a hit shows up within about a minute and a half of it happening. When status is in_progress, treat the numbers as partial; once it flips to final, they are the official box score. Other sports populate stats when the game completes.

Response:

{
  "id": "16",
  "sport_key": "baseball_mlb",
  "home_team": "Detroit Tigers",
  "away_team": "St. Louis Cardinals",
  "status": "final",
  "home_score": 3,
  "away_score": 5,
  "players": [
    {
      "name": "Tarik Skubal",
      "team": "Detroit Tigers",
      "stats": {
        "strikeouts": 7,
        "earned_runs": 2,
        "hits_allowed": 5,
        "outs": 18
      }
    },
    {
      "name": "Masyn Winn",
      "team": "St. Louis Cardinals",
      "stats": {
        "hits": 2,
        "home_runs": 1,
        "rbis": 3,
        "total_bases": 5
      }
    }
  ]
}

Stats are sourced from official league APIs (MLB, NBA, NHL, NCAAB, ESPN) and are available once a game reaches final status. Use this endpoint alongside odds from any sportsbook to build your own prop resolution engine.

Which stats each sport returns

Pass ?stat_type=hits,home_runs to narrow the response to the stats you need (comma-separated, omit for all). The stat names below are the complete vocabulary per sport.

This list is wider than our market list. We store a stat whenever the box score carries it, not only when a sportsbook prices it — so several of these have no betting market anywhere and are still returned. NFL passing_completions and tennis sets_won are examples. Don't infer what we store from what we quote.

Sportstat_type values
MLBhits, total_bases, singles, doubles, triples, home_runs, runs, rbis, walks, stolen_bases, at_bats, hits_runs_rbis, batter_strikeouts · pitching: strikeouts, earned_runs, hits_allowed, outs, pitcher_started (1 = started, 0 = relieved)
NBA · WNBA · NCAABpoints, rebounds, assists, threes, steals, blocks, turnovers, points_rebounds, points_assists, rebounds_assists, points_rebounds_assists, minutes
NHLgoals, points_nhl, shots_on_goal, blocked_shots, power_play_points, saves
NFL · NCAAFpassing_yards, passing_tds, passing_attempts, passing_completions, interceptions, longest_completion, rushing_yards, rushing_tds, rushing_attempts, longest_rush, receiving_yards, receiving_tds, receptions, longest_reception, pass_rush_yds, rush_reception_yds, anytime_td, first_td, sacks, fumbles, fumbles_lost, fumbles_recovered, field_goals_made, field_goals_attempted, longest_field_goal, extra_points_made, extra_points_attempted, kicking_points
Soccer (all leagues)goals, assists, shots, shots_on_target, yellow_cards, red_cards, cards, first_goal, fouls_committed, fouls_suffered, offsides, saves, shots_faced, goals_conceded, own_goals · team: corners
Tennistotal_games, sets_won, set_1_games … set_5_games · serve detail (thinner coverage): aces, dblfaults, breakpts, breakpts_w, tiebreaks, tiebreaks_w, games_w, sets_w, matches_w, plus opp_* mirrors
Golfstrokes, score, total_score, birdies, eagles, pars, bogeys_ow, holes, rank, golf_position, golf_made_cut, tournament_winner
UFCsignificant_strikes, takedowns, fight_winner, fight_method, fight_round, completed_rounds, went_distance
Esports (CS2)kills_map_1, kills_maps_1_2, kills_maps_1_2_3, headshots_map_1, headshots_maps_1_2, headshots_maps_1_2_3

Team rows also carry per-period scores as period_{code}_home / period_{code}_away — for example period_q1_home, period_i3_away, period_f5_home — using the same period codes as the ?period= odds filter.

Coverage depth varies by how long we have carried a sport. Our box-score archive starts April 2026, so MLB, WNBA, tennis, soccer and golf hold a full season while NBA, NHL and NFL begin accumulating at their 2026-27 openers.

Available Markets (Per Event)

GET /v1/sports/{sport_key}/events/{event_id}/markets

Which market keys this event actually has, and how many outcomes sit under each. Cheap discovery before a full odds pull: a busy MLB game carries 100+ markets across 27 books, and asking this first lets you request only the markets= you care about instead of downloading the whole board.

[
  { "key": "batter_home_runs", "outcomes_count": 54 },
  { "key": "h2h", "outcomes_count": 38 },
  { "key": "pitcher_strikeouts", "outcomes_count": 46 },
  { "key": "spreads", "outcomes_count": 40 },
  { "key": "totals", "outcomes_count": 44 }
]

Full-game markets only, so the counts match what a default /odds?markets= call returns. Period markets are excluded — reach them with ?period=.

Game Context

GET /v1/sports/{sport_key}/events/{event_id}/context

The conditions a game is played under — for MLB, the probable starting pitchers and their throwing hand (platoon-split context for every batter prop), whether the lineup is confirmed, the home-plate umpire, and first-pitch weather at outdoor (or open-roof) venues. For NFL & NCAAF, the venue and kickoff weather (wind and cold drive passing, kicking, and totals). The same context is also embedded in the /results response, so every graded prop carries the conditions it settled against — something no other odds API offers. Free tier.

Response:

{
  "event_id": "16",
  "sport_key": "baseball_mlb",
  "home_team": "Detroit Tigers",
  "away_team": "Seattle Mariners",
  "commence_time": "2026-06-06T17:10:00Z",
  "venue": "Comerica Park",
  "roof_type": "Open",
  "is_indoor": false,
  "home_probable_pitcher": "Keider Montero",
  "away_probable_pitcher": "Bryce Miller",
  "home_probable_pitcher_hand": "R",
  "away_probable_pitcher_hand": "R",
  "lineup_confirmed": true,
  "home_plate_umpire": "Chris Segal",
  "weather": {
    "temperature_f": 82.7,
    "wind_speed_mph": 7.4,
    "wind_direction": "WNW",
    "precip_probability_pct": 10,
    "conditions": "Light drizzle"
  },
  "updated_at": "2026-06-06T15:40:00Z"
}

Returns 404 when no context is on file yet (before the context loop has reached the event, or for sports without a context source). Indoor / domed venues return weather: null with is_indoor: true.

Line Movement & Steam (Hobby+)

GET /v1/sports/{sport_key}/events/{event_id}/movement
    ?markets=h2h,spreads,totals    # optional, comma-separated
    &period=q1                     # optional game-period filter

Line movement derived from our snapshot tick history. For each (book, market, outcome) it returns the opening line, the latest line, and the implied-probability / point shift between them. The top-level steam array flags outcomes that multiple books moved in the same direction — the classic sharp-money signal, computed across all 27books we poll. Because we own the tick history, this is something pull-only odds APIs can't produce. Hobby+ full; free tier redacted.

Response:

{
  "id": "37464",
  "sport_key": "baseball_mlb",
  "home_team": "Texas Rangers",
  "away_team": "Cleveland Guardians",
  "steam": [
    {
      "market": "totals",
      "name": "Over",
      "books_quoting": 9,
      "books_moved": 6,
      "consensus_direction": "shortening",
      "avg_prob_shift": 0.041,
      "consensus_point_shift": 0.5,
      "steam_score": 24.6
    }
  ],
  "bookmakers": [
    {
      "key": "pinnacle",
      "title": "Pinnacle",
      "markets": [
        {
          "key": "totals",
          "outcomes": [
            {
              "name": "Over",
              "open_price": -105, "open_point": 8.5, "open_at": "2026-06-05T22:00:00Z",
              "latest_price": -130, "latest_point": 9.0, "latest_at": "2026-06-06T23:10:00Z",
              "prob_shift": 0.054, "point_shift": 0.5,
              "direction": "shortening", "num_snapshots": 42
            }
          ]
        }
      ]
    }
  ]
}

prob_shift is the signed implied-probability change (open → latest); positive means the book shortened the outcome (money moved toward it). steam_score is a 0–100 composite of how many books agreed and how far they moved. Steam comes in two flavors: price steam (consensus_direction = shortening/lengthening) when books move the price at a stable line, and point steam (point_up/point_down) when books move the line/number itself the same way (e.g. a total 8.5 → 9 or a run line +1.5 → −1.5) — common on totals and spreads, where sharp money moves the number rather than the price. The two are disjoint: a moved line has prob_shift = null (and per-book direction = line_moved), so it can't double-count. For point steam avg_prob_shift is 0 and the magnitude is in consensus_point_shift. PrizePicks is excluded from steam (synthetic DFS pricing).

Webhooks — Push Delivery (Streaming)

The push half of the API. Instead of polling, subscribe a URL and we POST to it when a line moves, a prop grades, the steam detector fires, or a book pulls a market. Every delivery is HMAC-signed and retried with exponential backoff (10s → 30s → 2m → 10m → 30m → 1h, six attempts). Streaming Lite ($39, 5 hooks) and Streaming ($79, 10 hooks).

Create a subscription:

POST /v1/webhooks
X-API-Key: YOUR_KEY
Content-Type: application/json

{
  "url": "https://your-app.com/hooks/propline",
  "events": ["line_movement", "resolution", "steam", "market_suspended"],
  "filter_sport_key": "baseball_mlb",       # optional
  "filter_market_key": "pitcher_strikeouts", # optional
  "filter_player_name": "Skubal",            # optional, substring
  "filter_bookmaker_key": "draftkings,fanduel", # optional, comma-separated books
  "filter_event_id": 12649,                  # optional
  "min_price_change_pct": 5,                 # line_movement only
  "min_steam_score": 25,                     # steam only, 0-100
  "min_books_agreeing": 3,                   # market_suspended only
  "format": "json",                          # or "discord"
  "batch_max": 100                           # optional: batched delivery (see below)
}

The response carries the signing secret in full — this is the only time it is ever shown; every later read masks it. Filters are AND-ed, and at least one is required below Enterprise (a filter-less firehose is an Enterprise contract). filter_bookmaker_key takes comma-separated book keys — the same vocabulary as the ?bookmakers= query param — and applies to line_movement, resolution and market_suspended; steam is a cross-book consensus signal with no single book, so it is unaffected. A three-book filter on a sport-wide line_movement subscription typically cuts delivery volume ~85%. Set format: "discord" to have us rewrite the payload as a Discord embed — see Discord webhooks.

Batched delivery (batch_max):

# With "batch_max": 100, up to 100 events arrive in ONE signed POST:
{
  "batch": true,
  "event_type": "line_movement",
  "count": 3,
  "events": [
    {"delivery_id": 25901221, "data": { ...exactly the per-event payload... }},
    {"delivery_id": 25901222, "data": { ... }},
    {"delivery_id": 25901223, "data": { ... }}
  ]
}
# Headers: X-PropLine-Batch: 3, X-PropLine-Event as usual, and the
# signature covers the whole envelope with the same HMAC scheme.
# Dedupe on the delivery_id INSIDE each element.

Strongly recommended for high-volume subscriptions (sport-wide line_movement can exceed 1,000 events/min during a full slate). One POST per event means your endpoint's response time caps your delivery rate — an endpoint answering in 2s simply cannot receive 1,000 individual POSTs a minute, and events queue behind each other. With batching, delivery latency is your response time, full stop. Events within a batch are ordered oldest-first and only batch with others of the same event type. Set batch_max (1–500) at create time or via PATCH; 0 switches back to per-event. JSON format only — Discord subscriptions always deliver per-event. When your queue is short, batches are naturally small or single-event.

Auto-pause: a webhook whose endpoint fails every delivery for several days straight is paused automatically (active: false, paused_reason: "auto_failure") and the account owner is emailed a one-click re-enable link. Re-enable any time via the link, the dashboard, or PATCH /v1/webhooks/{id} with {"active": true}. Deliveries resume with new events; the paused period is not replayed. A brief outage never triggers this — only days of 100% failure.

market_suspended — a book took a market off the board:

{
  "event_type": "market_suspended",
  "sport_key": "baseball_mlb",
  "event": {"id": 138811, "home_team": "Pittsburgh Pirates",
            "away_team": "Boston Red Sox", "commence_time": "2026-08-16T17:35:00+00:00"},
  "bookmaker_key": "draftkings",
  "bookmaker_title": "DraftKings",
  "subject": "Willson Contreras",          # the player; null for a game line
  "reason": "off_the_board",               # or "no_offers" on an exchange
  "markets": [
    {"key": "batter_hits", "description": "Willson Contreras Hits O/U", "period": null,
     "last_seen": "2026-08-16T14:03:45+00:00",
     "last_price": [{"name": "Over", "price": -115, "point": 0.5},
                    {"name": "Under", "price": -105, "point": 0.5}]},
    {"key": "batter_total_bases", ...}     # every key the book pulled for this player
  ],
  "books_agreeing": 7,                     # books that dropped the same subject
  "books": ["betmgm", "betrivers", "draftkings", "novig", "pinnacle", "prophetx", "underdog"],
  "suspended_at": "2026-08-16T14:07:30+00:00"
}

Fires pregame only, once per (book, event, player) withdrawal — one late scratch is one delivery, not one per market key. It is detected by absence: a market the book delivered on the last cycle and did not deliver on the next two, while the rest of that event's board kept flowing. Line moves, alt-ladder reshuffles, a fetch we skipped, and the last 30 minutes before kickoff (when every book tears its prop board down) are all filtered out. books_agreeing is how many books have pulled the same subject on the same event; set min_books_agreeing on the subscription to hear only corroborated drops (3+ is a late scratch), or leave it unset to hear every one — if you price off one book, its single-book drop is the one you care about. There is no restore event: when the market comes back the existing line_movement fires on the returning price, and suspended_at on the market in /odds clears. That same field is the pull-side view of this signal on every tier.

A related but different staleness signal: pregame_only on each bookmaker block in /odds is true when the event is live and that book does not price it in play — the prices shown are its last pregame quote and will not move again until the game ends. This is the case suspended_at cannot show you: a withdrawal is detected by a market going missing from a poll, and a book with no in-play feed is never polled for the fixture once it starts, so nothing goes missing. We still serve the rows rather than hiding them, because on the DFS books that frozen pregame line is the number the bet settles against. Read it as “a real price, but not a live one”, and drop those books yourself if you are pricing in play.

Managing subscriptions:

GET    /v1/webhooks                  # list (secret masked)
GET    /v1/webhooks/{id}             # one subscription
PATCH  /v1/webhooks/{id}             # change url / events / filters / active
DELETE /v1/webhooks/{id}             # remove
POST   /v1/webhooks/{id}/test        # enqueue a test payload
GET    /v1/webhooks/{id}/deliveries  # recent attempts + response codes
       ?limit=50                     # up to 200 per page
       &before_id={id}               # page backwards: pass the smallest id
                                     # from the previous page
GET    /v1/webhooks/{id}/replay      # re-read missed events, oldest-first
       ?since_seq=4180               # your last processed X-PropLine-Sequence
       &limit=100                    # up to 500 per page

/deliveries is the debugging surface — it returns the status, HTTP response code, attempt count and full payload of each recent delivery, so a failing integration is diagnosable without contacting us. Pages are newest-first; a page shorter than limit is the last one. Test deliveries are prioritised ahead of the live queue.

Catching up after an outage

If your endpoint goes down, those deliveries are gone — and nothing used to tell you that you had missed any, so a quiet stream and a broken one looked identical. This is how you tell them apart and get the missing events back.

Every delivery carries X-PropLine-Sequence — a counter that is monotonic within your subscription. Store the highest one you have processed. Do not use X-PropLine-Delivery for this: that id is global across every subscription, so gaps in it are other customers’ traffic and say nothing about your own stream.

After downtime, read forward from that cursor. Events come back oldest-first (the opposite of /deliveries) so you can replay them in order. Page by passing next_seq back as since_seq while has_more is true.

GET /v1/webhooks/12/replay?since_seq=4180

{
  "webhook_id": 12,
  "since_seq": 4180,
  "events": [
    { "seq": 4181,
      "delivery_id": 99312,
      "event_type": "line_movement",
      "created_at": "2026-08-31T14:02:11Z",
      "data": { } }
  ],
  "next_seq": 4181,
  "has_more": false,
  "oldest_available_seq": 3902,
  "latest_seq": 4181,
  "truncated": false
}

truncated: true is the field that matters: it means events after your cursor have already aged out and are gone, so you should resync from the REST endpoints rather than assume you are current. Without it, a short list would be indistinguishable from “nothing to catch up on”. Replay is bounded by delivery retention — 2 days, and at most 5,000 deliveries per subscription. latest_seq is not subject to retention, so latest_seq - next_seq is an honest “how far behind am I” even when the rows themselves are gone. Sequence numbers always increase and never repeat, but are not guaranteed to be dense — treat a skipped number as normal, and read truncated for actual loss. Neither /replay nor /deliveries counts against your daily quota.

Websocket streaming

If your system already speaks websockets, or you simply can’t host a public HTTPS endpoint, connect a socket instead of receiving POSTs. Same events, same filters, same sequence numbers — a stream and a webhook are the same subscription with a different transport, so they cannot deliver you different things.

A socket changes how you receiveupdates, not how fast we see them. Events are pushed the moment we detect a change, but detection is bounded by our polling cycle — roughly 30s per book in-play and 60s pre-match. This is push delivery of polled data, not a sub-second feed, so it is not suited to pricing live in-play bets.

Create the subscription with transport: "websocket" (no url — there is nowhere to POST), then connect and authenticate:

POST /v1/webhooks
{ "transport": "websocket",
  "events": ["line_movement"],
  "filter_sport_key": "baseball_mlb" }        → { "id": 12, ... }

# then, over the socket:
wss://ws.prop-line.com/v1/stream

→ {"type":"auth","api_key":"YOUR_KEY","webhook_id":12,"since_seq":4180}
← {"type":"ready","webhook_id":12,"latest_seq":4213,"truncated":false}
← {"type":"event","seq":4181,"event_type":"line_movement","data":{...}}
← {"type":"event","seq":4182,...}
← {"type":"ping"}

Send since_seq to resume exactly where you left off — it is the same cursor /replay uses, so a reconnect never silently skips an event. The ready frame carries the same truncated flag: true means events after your cursor aged out of retention and are gone, so resync from REST rather than assume you are current.

Concurrent connections are capped per plan — Streaming Lite 2, Streaming 5. Delivered events do notcount against your daily request quota, exactly as webhook deliveries don’t. Refusals close the socket with a distinct code: 4401 bad key, 4403 tier or paused, 4404 no such subscription on your account, 4429 at your connection limit. Expect a ping every 25s; reconnect with your last seq if it stops.

line_movement and resolution payloads carry the same DFS pricing signals as /odds outcomes: market_description (for DFS alt markets this holds the flavor + line, e.g. Rebounds (demon 12.5)), dfs_odds_type (PrizePicks standard / goblin / demon; null for traditional books) and payout_multiplier (Underdog's numeric boost; PrizePicks publishes no numeric multiplier — the flavor is the signal). So a webhook consumer can tell a goblin/demon variant from the standard line without a REST join.

Verifying a delivery:

X-PropLine-Event      line_movement | resolution | steam | market_suspended | test
X-PropLine-Timestamp  unix seconds
X-PropLine-Signature  hex(HMAC-SHA256(secret, f"{timestamp}." + raw_body))
X-PropLine-Delivery   delivery row id — use as your dedupe key
import hmac, hashlib

def verify(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Sign over the raw body, not a re-serialised dict — key order would differ. Both SDKs ship this as a helper (verify_signature / PropLine.verifySignature). Discord-format hooks are unsigned — Discord does not verify them.

Bulk Data Export (Pro)

Want the archive without a subscription? The one-time $99 Historical Backfill pass grants a 7-day full-archive pull on any tier: a trailing 2-year lookback, uncapped export calls, and the only access to /exports/odds-history — the raw tick-by-tick line-movement firehose, which no subscription tier can pull.

GET /v1/exports/resolved-props
    ?sport=baseball_mlb
    &market=pitcher_strikeouts    # optional
    &bookmaker=draftkings         # optional
    &since=2026-04-01T00:00:00Z   # optional, ISO datetime
    &until=2026-04-30T23:59:59Z   # optional, ISO datetime

Streams every resolved prop outcome as a CSV download — ideal for backtesting, model training, and statistical research. Pro tier only. One row per (event, market, bookmaker, outcome) with the line, price, resolution (won/lost/push/void), and actual stat value — plus the opening and closing line on every row, so a full CLV study is one download rather than one history call per event. PropLine is the only API that offers this — other odds APIs don't resolve props.

Sample row:

event_id,sport_key,commence_time,home_team,away_team,home_score,away_score,market,bookmaker,player_name,outcome_name,line,price_american,price_decimal,resolution,actual_value,resolved_at,closing_price,closing_at,dfs_odds_type,closing_point,opening_price,opening_point,opening_at,customer_token
5885,baseball_mlb,2026-04-19T20:10:00+00:00,Seattle Mariners,Texas Rangers,3,2,pitcher_strikeouts,draftkings,Bryan Woo,Over,6.5,116,2.16,lost,6.0,2026-04-19T23:18:45Z,110,2026-04-19T20:08:12Z,,6.5,104,6.5,2026-04-17T13:02:51Z,xxxxxxxxxxxx

opening_price/opening_point/opening_at are the first snapshot in the 14 days before kickoff; closing_point is the line that went with closing_price. Both points matter on spreads and totals, where the number moves as much as the price (−3 −110 → −3.5 −105) — note this is distinct from line, which is the outcome’s own current point. Blank when we recorded no pre-game snapshot for that outcome.

New columns are always appended immediately before customer_token, so positional parsers written against an earlier column set keep working.

dfs_odds_type marks the PrizePicks projection tier — standard (the true market line), goblin, or demon. It's blank for every traditional sportsbook.

Response is text/csv with Content-Disposition: attachment, so curl -O saves straight to a file. Full-season exports can be tens of megabytes — the stream writes as it goes.

Historical depth by tier:

  • Pro — last 90 days of resolved outcomes
  • Streaming — last 365 days
  • Enterprise — full archive, no lookback cap

The effective floor is reflected in the X-PropLine-Export-Window-Start response header. A since value older than your tier's window is silently clamped to the floor. Need deeper history for backtesting? support@prop-line.com.

Coverage note:the archive begins April 2026 (PropLine's launch) and depth varies by sport — a sport's graded history starts when its games are played on our watch. NFL & NCAAF player-prop grading begins with the 2026 season (September) — no NFL games have been played since launch, so there are no graded NFL rows yet, though live NFL odds are flowing today. Every export response states the sport's boundary in-band: the X-PropLine-Archive-Starts header carries the earliest date we hold for that sport (none if the sport has no data yet), and if your requested window ends before the archive begins, an X-PropLine-Archive-Notice header says outright that the export will be empty — check those two headers before assuming a thin file is a bug.

Daily call cap by tier:

  • Pro — 50 export calls per day
  • Streaming Lite — 100 export calls per day
  • Streaming — 200 export calls per day
  • Enterprise — uncapped

The cap counts whole calls, not rows — and sport is required, so a multi-sport pull is one call per sport. The most efficient pattern is one call per sport over the widest date range you need; slicing into smaller date ranges burns more calls for the same data. The cap and remaining budget are surfaced on every successful response in the X-PropLine-Export-Daily-Cap and X-PropLine-Export-Daily-Remaining response headers — read those instead of waiting for a 429. Quotas reset at 00:00 UTC (a hard reset, not a rolling window). A nightly per-sport reload across our entire catalog fits comfortably under the Pro cap; if your workflow needs more, that is the Enterprise conversation.

Watermarking:

Every row carries a stable customer_token derived from your API key — the same value across all your exports, distinct from any other customer's. The same value is also returned in the X-PropLine-Customer-Token response header. Bulk redistribution of exported data is prohibited under our terms of service; the watermark is how we trace leaks back to their source.

Canary rows:

Every export also appends two synthetic rows whose team and player-name fields begin with the literal prefix (Watermark). The 8-hex token in player_name is HMAC-derived from your API key and the ISO week, so any leaked CSV remains attributable even after the customer_token column is stripped. They appear at the tail of the stream and are easy to filter:

-- Drop canary rows in SQL or pandas
WHERE player_name NOT LIKE '(Watermark)%'

Canary rows do not affect any aggregate statistic that excludes them. Full mechanism is disclosed in our watermarking terms.

Full line-movement history (backfill / Enterprise):

GET /v1/exports/odds-history
    ?sport=baseball_mlb
    &market=pitcher_strikeouts    # optional
    &bookmaker=draftkings         # optional
    &since=2026-04-01T00:00:00Z   # optional, ISO datetime (recorded_at)
    &until=2026-05-01T00:00:00Z   # optional, ISO datetime (recorded_at)

Streams the raw odds tick history — every recorded snapshot (price + line, per book, including period markets), one row per (outcome, snapshot) — not just the closing line. This is the bulk firehose that no subscription tier can pull: Pro and Streaming get per-event /odds/history, but the bulk export is exclusive to the one-time Historical Backfill pass and Enterprise. The pass covers a trailing 2-year lookback (Enterprise unbounded); uncapped calls. Same customer_token watermark + canary rows as the resolved-props export.

Columns:

event_id,sport_key,commence_time,home_team,away_team,market,period,bookmaker,player_name,outcome_name,recorded_at,price_american,price_decimal,point,book_updated_at,dfs_odds_type,customer_token

A full archive runs to gigabytes per sport — page month by month with since/until so each download stays manageable.

Free sample (no API key):

GET /v1/exports/sample
# Returns the last 7 days of MLB pitcher_strikeouts props as CSV.
# Up to 1000 rows. No auth required.

The sample endpoint is open to everyone — useful for evaluating the data shape before upgrading.

DFS Payouts (Free)

GET /v1/dfs/payouts
    ?platform=prizepicks       # only prizepicks today
    &leg_win_prob=0.58         # optional

The PrizePicks Power and Flex payout schedule (2–6 legs) with the per-leg breakeven win probability for each play — the hit rate you need before an entry is worth taking. Pass ?leg_win_prob= and every play also returns expected_return (per $1) and is_plus_ev at that rate. Pair it with /best-line to see whether a DFS slip beats the sportsbook price on the same legs.

Read the disclaimer field on the response: these are the standard published payouts (per-pick demon/goblin modifiers are not in the PrizePicks feed), and breakeven assumes independent legs.

Market Hit Rates (Free)

GET /v1/markets/hit-rates
    ?days=28                   # 1-60, default 28
    &bookmaker=bovada          # default bovada

Per-market daily counts of graded Overoutcomes over the look-back window — how often each market type actually went Over, straight off resolved props. Useful as a sanity baseline for a model, and as a calibration check on a book's lines.

{
  "days": 28,
  "bookmaker": "bovada",
  "markets": {
    "pitcher_strikeouts": [
      { "date": "2026-07-13", "total": 14, "won": 6 },
      { "date": "2026-07-14", "total": 18, "won": 10 }
    ],
    "batter_total_bases": [ ... ]
  }
}

total excludes voids; won is the subset where the actual stat beat the line. Counts only — no individual props — so this is free-tier.

Data Freshness (Free, no auth)

GET /v1/freshness

Per-bookmaker staleness across every book we carry, split into game lines vs props. Poll it to decide whether a book is worth reading right now, or to alert your own system when a book you depend on goes quiet. No API key required.

{
  "as_of": "2026-08-09T18:22:04Z",
  "stale_threshold_seconds": 900,
  "bookmakers": [
    {
      "key": "pinnacle",
      "market_count": 41208,
      "latest_update": "2026-08-09T18:21:47Z",
      "staleness_seconds": 17,
      "is_stale": false,
      "worst_staleness_seconds": 3820,
      "suspended_market_count": 24,
      "market_classes": {
        "game_lines": { "market_count": 9140, "latest_update": "...", "staleness_seconds": 17,
                        "active_market_count": 480, "oldest_market_update": "...",
                        "worst_staleness_seconds": 95, "suspended_market_count": 0 },
        "props":      { "market_count": 32068, "latest_update": "...", "staleness_seconds": 44,
                        "active_market_count": 2210, "oldest_market_update": "...",
                        "worst_staleness_seconds": 3820, "suspended_market_count": 24 }
      }
    }
  ]
}

The split matters: a book's prop board can go dark behind perfectly fresh game lines, and a single top-level number hides that. Top-level staleness_seconds is time since the most recent write on any market for the book, so it is optimistic per-event. worst_staleness_seconds is the pessimistic twin: the age of the oldest non-suspended market on the active board (events from 6h ago to 48h out), so a single silently-stale market is visible without walking every event. Markets the book has withdrawn are excluded from it — they are flagged with suspended_at on /odds and counted here as suspended_market_count instead, because a flagged withdrawal is not a capture hole. Same data as the human-readable /freshness page.

Available Markets

Market KeyDescriptionSport
h2hMoneylineAll
spreadsPoint Spread / Run LineAll
totalsOver/Under (Game Total)All
pitcher_strikeoutsPitcher Total StrikeoutsMLB
pitcher_earned_runsPitcher Earned Runs AllowedMLB
pitcher_hits_allowedPitcher Hits AllowedMLB
batter_hitsBatter Total Hits (Over/Under)MLB
batter_home_runsBatter Home RunsMLB
batter_rbisBatter RBIs (Over/Under)MLB
batter_total_basesBatter Total BasesMLB
batter_stolen_basesBatter Stolen BasesMLB
batter_walksBatter WalksMLB
batter_singlesBatter SinglesMLB
batter_doublesBatter DoublesMLB
batter_runsBatter Runs ScoredMLB
pitcher_outsPitcher Outs (Innings Proxy)MLB
batter_1plus_hitsPlayer to Record 1+ HitsMLB
batter_2plus_hitsPlayer to Record 2+ HitsMLB
batter_3plus_hitsPlayer to Record 3+ HitsMLB
batter_4plus_hitsPlayer to Record 4+ HitsMLB
batter_2plus_home_runsPlayer to Hit 2+ Home RunsMLB
batter_1plus_rbisPlayer to Record 1+ RBIsMLB
batter_2plus_rbisPlayer to Record 2+ RBIsMLB
batter_3plus_rbisPlayer to Record 3+ RBIsMLB
player_pointsPlayer Total PointsNBA
player_reboundsPlayer ReboundsNBA
player_assistsPlayer AssistsNBA
player_threesPlayer Three-Pointers MadeNBA
player_stealsPlayer StealsNBA
player_blocksPlayer BlocksNBA
player_turnoversPlayer TurnoversNBA
player_points_rebounds_assistsPoints + Rebounds + AssistsNBA
player_double_doublePlayer Double-DoubleNBA
player_goalsPlayer GoalsNHL
player_shots_on_goalPlayer Shots on GoalNHL
goalie_savesGoalie SavesNHL
player_blocked_shotsPlayer Blocked ShotsNHL
player_pass_ydsPassing YardsNFL / NCAAF
player_pass_tdsPassing TouchdownsNFL / NCAAF
player_pass_interceptionsInterceptions ThrownNFL / NCAAF
player_pass_completionsPass CompletionsNFL / NCAAF
player_pass_attemptsPass AttemptsNFL / NCAAF
player_longest_completionLongest CompletionNFL / NCAAF
player_rush_ydsRushing YardsNFL / NCAAF
player_rush_tdsRushing TouchdownsNFL / NCAAF
player_rush_attemptsRush AttemptsNFL / NCAAF
player_rush_longestLongest RushNFL / NCAAF
player_reception_ydsReceiving YardsNFL / NCAAF
player_receptionsReceptionsNFL / NCAAF
player_reception_tdsReceiving TouchdownsNFL / NCAAF
player_reception_longestLongest ReceptionNFL / NCAAF
player_pass_rush_ydsPassing + Rushing Yards (combo)NFL / NCAAF
player_rush_reception_ydsRushing + Receiving Yards (combo)NFL / NCAAF
player_anytime_tdAnytime Touchdown ScorerNFL / NCAAF
player_1st_tdFirst Touchdown ScorerNFL / NCAAF
player_2plus_tdPlayer to Score 2+ TouchdownsNFL / NCAAF
player_3plus_tdPlayer to Score 3+ TouchdownsNFL / NCAAF
player_sacksDefensive Sacks MadeNFL / NCAAF
player_field_goals_madeField Goals MadeNFL / NCAAF
player_extra_points_madeExtra Points MadeNFL / NCAAF
player_kicking_pointsKicking PointsNFL / NCAAF
player_fumbles_lostFumbles LostNFL / NCAAF
anytime_goal_scorerAnytime Goal ScorerSoccer
first_goal_scorerFirst Goal ScorerSoccer
both_teams_to_scoreBoth Teams to ScoreSoccer
double_chanceDouble ChanceSoccer
draw_no_betDraw No BetSoccer
correct_scoreCorrect ScoreSoccer
total_cornersTotal Corners (O/U)Soccer
corners_spreadCorners Handicap (team spread on corner count)Soccer
team_cornersTeam Corners O/U (per-team corner total)Soccer
total_cardsTotal CardsSoccer
team_cardsTeam Cards O/U (per-team card total)Soccer
2plus_goalsPlayer to Score 2+ GoalsSoccer
player_assistsPlayer to Assist a GoalSoccer
player_2plus_assistsPlayer to Assist 2+ GoalsSoccer
player_cardsPlayer to Be Shown a CardSoccer
goal_or_assistPlayer to Score or AssistSoccer
overtimeWill the Game Go to OvertimeWNBA / NFL
total_roundsTotal RoundsUFC / Boxing
fight_distanceFight Goes the DistanceUFC / Boxing
round_bettingRound BettingUFC
fight_winnerFight WinnerBoxing
fight_outcomeFight Outcome (KO/TKO/Decision)Boxing

Alt lines included: Player prop markets include alternate lines automatically. Query pitcher_strikeouts to get both the primary Over/Under line and alt lines (3+, 5+, 6+ strikeouts etc.) in a single response. Alt spreads, alt totals, and team totals are also included.

Read the market team field to tell them apart: a team total rides the same totals key as the game total, so a single book can return three totals markets on one soccer match — e.g. "Total" at 2.5 plus "Team Total - Independiente del Valle" at 1.5 and "Team Total - Deportes Tolima" at 0.5. Every book words that string differently, which is why team exists: it carries the canonical event team name (matching home_team / away_team exactly) and is null on the game total, so you never have to parse a book's wording. It is on /odds, /odds/history, /odds/closing and /movement, and is always null outside totals. Alt lines each get their own market row, with the line in the description — Bovada writes "Total Goals O/U (line 3.5)" and "Spread (Fulham -0.5)", Pinnacle "Total 3.25". Within one row, every outcome always shares one line.

Error Codes

StatusDescription
401Missing or invalid API key
403Endpoint requires a paid tier
404Sport or event not found. On an event id that previously worked, this usually means the event was merged into a canonical duplicate — re-fetch from /events and use the current id. See Event IDs can change.
429Daily quota exceeded, or per-key burst limit hit
500Internal server error

Structured error bodies

Gated and throttled responses return detail as a JSON object with a stable error code your code (or your AI agent) can branch on, plus the URL that unlocks the feature — missing_api_key, invalid_api_key, upgrade_required (with required_tier + upgrade_url), burst_limit_exceeded (with retry_after_seconds), and daily_limit_exceeded (with a pre-filled one-click upgrade URL).

{
  "detail": {
    "error": "upgrade_required",
    "message": "Prop resolution is a paid feature — start at $9/mo Hobby. Upgrade at https://prop-line.com/pricing",
    "required_tier": "hobby",
    "upgrade_url": "https://prop-line.com/pricing",
    "docs_url": "https://prop-line.com/docs"
  }
}

The X-Daily-Limit / X-Daily-Used / X-Daily-Remaining / X-Daily-Reset headers from the Authentication section appear on every authenticated response, so you can see the cap coming; daily-cap 429s additionally carry Retry-After (reset is unix seconds at the top of the next UTC day) — back off until the reset instead of retrying. Free-tier calls to paid analytical endpoints don't 403 at all: they return 200 with the structure intact, values redacted, and an upgrade_url in the body.

All markets, every sport →

One page per market: the live line across every book, the request that returns it, and recent outcomes graded against the official box score. Ranked by settled volume; generated from the data.