GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing parlay-api.com — ParlayAPI Docs - Sports odds API documentation ParlayAPI DocumentationReal-time sports odds API with 30+ sources, player props, prediction-market exchanges, and WebSocket streaming. Some endpoints have similar request patterns to the-odds-api; validate integration behavior, coverage, and current plan costs before switching. Quick StartStart with a request below, or use the step-by-step guide for key setup, response checks, and troubleshooting. For complete runnable examples, see the language quickstarts. pip install parlay-api
from parlay_api import ParlayAPI
client = ParlayAPI(api_key="YOUR_KEY")
odds = client.odds("baseball_mlb", regions="us")curl "https://parlay-api.com/v1/sports/baseball_mlb/odds?regions=us" \ -H "X-API-Key: YOUR_KEY" const r = await fetch(
"https://parlay-api.com/v1/sports/baseball_mlb/odds?regions=us",
{ headers: { "X-API-Key": "YOUR_KEY" } }
);
const odds = await r.json();import requests
r = requests.get(
"https://parlay-api.com/v1/sports/baseball_mlb/odds",
headers={"X-API-Key": "YOUR_KEY"},
params={"regions": "us"},
)
odds = r.json()You'll need a key for the calls above. The free tier includes 1,000 credits a month, no card required. Get a free API keyAuthenticationPass your API key one of two ways:
WebSocket connections use the query param: Never commit your key. If you accidentally publish one, rotate it from the dashboard immediately.
Reveal and rotate mints the replacement immediately and leaves the previous secret working for a 14-day grace period. Once that period ends, a request still carrying the old secret answers Credits & PricingMost paid endpoints deduct a fixed number of credits per call. Multi-market endpoints such as
Tiers
Concurrent SSE/WS = max simultaneous push-stream connections per API key. Polling endpoints are subject to the plan's rate limits and monthly credit allowance. Hit a stream connection limit and new SSE / WS connections return 429 / WS close 4002 with a clear reason; existing connections aren't affected. The full odds feed ( Sandbox (no auth, fake data)Hit Source health diagnosticLive-betting bots need to know when a source goes stale so they don't trade on dead data. Postman + OpenAPI specFull machine-readable OpenAPI spec at SDKs and language quickstartsPython: JavaScript / Node: start with the native fetch quickstart, which needs no package install. The JavaScript SDK source is public; source availability does not establish an npm release. All eight language quickstarts include setup, a first request, and empty-response handling. Check the published SDK version's features before relying on retry helpers or streaming support. API stabilityRead the full versioning + deprecation policy. Short version: paths under Every response includes Python SDKPure-Python single-file SDK on PyPI: The SDK provides a Python interface for ParlayAPI endpoints, with methods for ParlayAPI extensions and devig helpers. Check its documentation and validate your required endpoints and response handling before migrating an existing client. from parlay_api import ParlayAPI
client = ParlayAPI(api_key="YOUR_KEY")
# TOA-compatible methods
sports = client.sports()
events = client.events("baseball_mlb")
odds = client.odds("baseball_mlb", regions="us", markets=["h2h", "spreads"])
historical = client.historical_odds("baseball_mlb", date="2024-10-15")
# Extensions
props = client.props("baseball_mlb", markets=["player_total_bases"])
arbs = client.arbitrage("baseball_mlb", limit=20)
consensus = client.consensus("baseball_mlb")
# Devig math (no network call)
fair_over, fair_under = ParlayAPI.devig(over_price=-110, under_price=-110)
edge_pct = ParlayAPI.edge(book_price=-105, fair_prob=fair_over)
# WebSocket URL builder
ws_url = client.websocket_url("baseball_mlb")SportsGET/v1/sports0 credits
List all sport keys with at least one event in the last 24 hours. Response[
{
"key": "baseball_mlb",
"group": "Baseball",
"title": "MLB",
"description": "Major League Baseball",
"active": true,
"has_outrights": false
},
...
]See Sport Keys reference for the full list. EventsGET/v1/sports/{sport_key}/events0 credits
List events for a sport. Deduped by ParameterscommenceTimeFromISO 8601 string Filter to events starting after this time commenceTimeToISO 8601 string Filter to events ending before this time dateFormatenum iso (default) or unixResponse[
{
"id": "8b1f3a2c0e9d4...",
"canonical_event_id": "ee78855a3bdd1019",
"sport_key": "baseball_mlb",
"sport_title": "MLB",
"commence_time": "2026-05-01T19:35:00Z",
"home_team": "New York Yankees",
"away_team": "Kansas City Royals"
},
...
]OddsGET/v1/sports/{sport_key}/oddsmarkets x regions credits, or markets x bookmakers/10 rounded up when
bookmakers= is givenTOA-compatible game-line odds. Returns moneyline, spread, totals (and player_* markets if you ask for them) across every book that publishes them. You are only charged for markets this endpoint can serve. The price is
Both sides of a two-way price always come from the same fixture, and from the same phase of it. When the same two teams or players meet twice in a day, each meeting is priced only from what a book posted for that meeting. A side that book has not posted for this fixture comes back with a null price rather than being filled from another match, so a null price is an absent quote and never a value we worked out for you. ParametersRepeating a list parameter is the same as the comma form. regionscomma-separated global, us, us2, uk, eu, au, fr, ca, br, mx, latam, asia. Default: us. Each region maps to an allowlist of books, so a book we serve can still be absent from a region you did not ask for; /regions and GET /v1/meta/regions publish the per-region book lists. Those lists separate the two questions: the active_books field is every book the region carries that has actually written prices in the last 24 hours, and books is the wider set this filter lets through, which includes books that currently return nothing. Every book carries a status (active, paused, retired, not_yet_integrated) and its last write age.Region filters select book keys. In particular, regions=ca does not establish an Ontario source feed, Ontario-specific prices, licensing, account eligibility or whether an offer can be placed. Per-book activity is separate from market completeness and price parity.marketscomma-separated Servable here, and this is the whole list: h2h (moneyline), spreads, totals, alternate_spreads, alternate_totals, outrights, and any player_* / batter_* / pitcher_* / anytime_* / futures_* prop key (e.g. player_total_bases).Any other catalogued key is accepted and answered, returns no odds here, and costs nothing: it is named in x-markets-unservable and routed by x-markets-served-elsewhere. The period keys h2h_1st_half, spreads_1st_half, totals_1st_half, h2h_1st_quarter, h2h_1st_period, h2h_1st_5_innings, spreads_1st_5_innings and totals_1st_5_innings are served by /v1/sports/{sport_key}/live/period_markets (?period=1H&market=h2h) and /v1/historical/sports/{sport_key}/period_markets. team_totals, btts, correct_score, double_chance, draw_no_bet, the MMA specials and the horse_* racing keys are served by /v1/sports/{sport_key}/props?markets=<key>. Every catalogued key carries a served_by field in GET /v1/markets.bookmakerscomma-separated Filter to specific books: draftkings,fanduel. See Bookmaker Keys.oddsFormatenum american (default) or decimaldateFormatenum iso (default) or unixeventIdscomma-separated Limit response to specific event IDs Exampleodds = client.odds(
"baseball_mlb",
regions="us",
markets=["h2h", "spreads", "totals"],
bookmakers=["draftkings", "fanduel"],
)curl "https://parlay-api.com/v1/sports/baseball_mlb/odds?regions=us&markets=h2h,spreads,totals&bookmakers=draftkings,fanduel" \ -H "X-API-Key: YOUR_KEY" Every event and row that carries Every bookmaker block carries Player PropsGET/v1/sports/{sport_key}/props3 credits
Player prop odds from the books available for your query. Broad queries can return a limited board; check the completeness headers below. Each row has Parametersmarketscomma-separated Filter to specific market keys, e.g. player_total_bases,player_hits_runs_rbis. See Market Keys.bookmakerscomma-separated Filter to specific books playerstring Partial-match player name (e.g. ?player=Judge)eventIdstring Limit to one game dfsOddsenum midpoint (default, +100/-100 zero-vig) or effective (-137/-137 reflecting actual 2-pick payout)limitint 1-10000 Max rows returned, default 5000 offsetint 0-10000 Page offset within the result set, default 0. Counts rows in both flat and grouped mode. Follow x-next-offset while x-result-has-more: true. Past 10000 rows, narrow with markets or bookmakers instead of paging further.maxAgeSecint Drop rows whose latest observation is older than N seconds. /props serves the latest row per book from the last 60 minutes, so a quiet market can be several minutes old; each row carries age_seconds (real write age) and this filter bounds it.groupedbool Return one entry per prop with a books[] array instead of one row per book. Per-book fields (prices, links, freshness, and the DFS tags below) live inside books[]; limit and offset still count rows in this mode.Pagination and completeness headersSuccessful
Fetching a board across booksFor cross-book coverage comparisons, start with one request per bookmaker and the market you need, for example
Degraded empty pages:
|
| Plan | Historical window | Oldest readable |
|---|---|---|
| Free | 48 hours | 2 days back |
| Starter | 168 hours | 7 days back |
| Pro | 720 hours | 30 days back |
| Business | 2,160 hours | 90 days back |
| Enterprise | 8,760 hours | 1 year back |
| Scale | Up to 87,600 hours | Records available within that window |
The plan window is a maximum lookback, not a promise that records exist throughout it. Historical coverage varies by sport, league, market, bookmaker, and date. Check a representative sample for your exact scope and completeness before planning a backtest or purchase. Every historical response carries x-historical-window-hours and
x-historical-window-from headers, so you can read your own limit at runtime
instead of inferring it from a 403. If you are backtesting a full season,
size your plan by this window first - the credit cost is rarely the binding constraint.
| Product | Endpoint | Use case | Important distinction |
|---|---|---|---|
| Point-in-time odds | /odds | TOA-compatible historical snapshots | Requires date; limited by tier window. |
| Closing odds | /closing-odds | Backtests at final pregame price | Game lines and prop closing rows where real prices exist. |
| Match/results archive | /matches | Schedules, teams, scores, esports results | Rows include has_odds; result-only rows are not price history. |
| Forward line movement | /line-movement | CLV and price-change tracking | Starts when ParlayAPI began capturing that market. |
/matches, return only rows that include real odds.Exchange-specific data including order book depth where available. Currently novig.
Free-text discovery across Kalshi, Polymarket, and Novig event markets. Built for Specials, next-team markets, coach-out markets, trade deadline markets, and other non-standard contracts that do not fit a fixed sport/event schema.
Try the live demo at /event-markets.
AJ Brown next team or Mike Vrabel out before September.kalshi,polymarket,novig. Default checks all three.0.balanced shows a mix of venues. match sorts by match confidence and volume.curl 'https://parlay-api.com/v1/event-markets/search?q=AJ%20Brown%20next%20team&sources=kalshi,novig,polymarket&min_volume=1000'
{
"query": "AJ Brown next team",
"credits_charged": 0,
"source_summary": {
"kalshi": {"count": 10, "max_volume": 283989.29},
"novig": {"count": 4, "max_volume": 9458.94}
},
"markets": [
{
"source": "kalshi",
"event_title": "A.J. Brown's Next Team",
"outcome": "New England",
"prices": {"yes_bid": 0.79, "yes_ask": 0.81}
}
],
"clusters": [
{
"cluster_key": "aj brown next team",
"sources": ["kalshi", "novig"],
"note": "Candidate text match only. Prices remain source-native and are not blended."
}
]
}Real-time odds streaming for one sport. Business / Enterprise / Scale tier required. Receives eligible stored odds updates after collection, processing and server-side coalescing.
Every WebSocket route takes the key in any of these three forms. /ws/odds and /ws/live share one auth path, so a key that opens one opens the other, and both refuse with the same close codes.
| Form | Example | When to use it |
|---|---|---|
X-API-Key header | X-API-Key: YOUR_KEY | Recommended. Keeps the key out of URLs and access logs. |
?apiKey= | wss://parlay-api.com/v1/ws/odds/baseball_mlb?apiKey=YOUR_KEY | Browser WebSocket, which cannot set headers. |
?api_key= | wss://parlay-api.com/v1/ws/live/tennis_wta?api_key=YOUR_KEY | Identical to ?apiKey=. Both spellings are accepted. |
A logged-in dashboard session cookie is a credential on /ws/live only, which is the free live board's own feed. The metered sockets (/ws/odds, /ws/odds-fast) need a key in one of the forms above, whatever browser they are opened from: the credit allowance is metered per key, and a cookie carries none. When both a key and a session cookie are present, the key wins.
Close codes on a refused connect are the same on every WebSocket route: 4003 no key on the request, 1008 key not valid, 4001 tier below Business (or an expired session), 4002 concurrent-connection cap, 4004 monthly credits spent. A refused connect is never billed.
| param | example | meaning |
|---|---|---|
markets | player_points,player_rebounds | Prop market keys |
kinds | game,prop | Game lines, props, or both |
event_id | ee78855a3bdd1019 | Single event filter, without the subscribe frame |
since | 1757280000000 | Resume cursor in epoch ms |
diff | true | Frames after initial_state carry only changed fields |
limit | 1000 | Rows in the initial_state frame. 1 to 1000, default 500. Same name, bounds and default as /v1/sse/odds. Any value that is not an integer in range closes the socket 4005, carrying the same sentence the SSE endpoint's 422 gives. |
max_age_s | 600 | Maximum retained observation age for game lines, including game selections stored as prop rows. Checked on initial snapshots and again before sending updates; 1 to 3600 seconds, default 600. Ordinary player props retain their existing window. A missing observation timestamp is not evidence of freshness. |
To seed a complete current game-line board, explicitly name the books and request ?kinds=game&markets=h2h,spreads,totals&limit=1000. Accept the frame as a replacement only when it has snapshot_scope="current_game_board", snapshot_complete=true, and truncated=false. It reports resume_mode="replace"; a supplied since is intentionally ignored and reported as since_honored=false.
Without snapshot=complete, other initial frames are bounded recent_rows samples. In that scope, since is a best-effort retained-observation filter, not an exactly-once replay cursor or lossless removal log. diff defaults to false and changes subsequent update frames only. max_age_s defaults to 600 seconds for game lines; it limits observation age but does not make a sample complete or prove that an unchanged price is still offered upstream.
Business tier and above can explicitly request snapshot=complete on /v1/ws/odds/americanfootball_nfl or /v1/sse/odds/americanfootball_nfl, with bookmakers=novig,pick6 (or either source alone), kinds=prop and markets=player_pass_yds,player_receiving_yards. An event filter is optional. Other books, sports, kinds and markets are refused in this mode. Completeness covers requested eligible observed native selections, not every upstream offering or other props streams.
/v1/sse/odds/americanfootball_nfl?bookmakers=novig,pick6&kinds=prop&markets=player_pass_yds,player_receiving_yards&snapshot=complete&limit=500
limit is rows per frame in this mode (1 to 1000, default 500). Configure clients for up to 512 KiB per frame, 64 frames, 16 MiB aggregate, 10,000 matching identities and a 20-second initial transfer. Timeout, unsupported scope, identity or marshal omission, and count or byte limits refuse the complete transfer rather than certify a partial board.
After connected, read initial_state and any snapshot_chunk parts in order. They share snapshot_id and snapshot_generation, with consecutive zero-based snapshot_part, snapshot_parts, snapshot_total_count, count and data. Parts have snapshot_in_progress=true and snapshot_complete=false. No live updates precede baseline completion.
Replace client state atomically only after the terminal snapshot_complete message: same ID and generation, snapshot_part=snapshot_parts, equal matching_count and delivered_count, snapshot_complete=true, snapshot_in_progress=false, unique identities, every part received, and no partial or truncation flags. An empty board still has one zero-row initial part and the terminal. Silence, heartbeat or an interrupted transfer never certifies completion.
A supplied since does not reduce this baseline: resume_mode=replace and since_honored=false. Reconnect performs a fresh complete enumeration. WebSocket subscription replacements start a fresh generation and independent 20-second deadline; retain the last completed baseline until the new terminal and discard older generation chunks. Queued updates follow the baseline; older native observations are rejected while newer observations preserve original quote clocks, literal prices and Pick6 payout metadata. This is not a durable change log. Omit snapshot=complete to retain the default single recent-row initial frame and change-event protocol.
initial_state frame is filled per bookThe row budget set by limit is a fair allocation among the books the fetch returned, not a newest-first fill. Books draw rows in turn until the budget is spent, game lines before props within each book and freshest fixture first. High-volume books publish on their own cadence; before per-book allocation, a ten-book subscription could receive 500 rows of one book and meet the other nine only in later odds_update frames.
It is an allocation, not a coverage guarantee. The fill shares out what this connect's fetch came back with, so a book can still be thin or absent from the frame. The per-book numbers below describe this build only: they are what this one snapshot held after its own filters and what it served, and they say nothing about your account, the sport, or your next connect.
The frame says what it contains and what it left out:
| field | meaning |
|---|---|
books | Every bookmaker key present in data, sorted. This is the universe this frame carries; a book the budget reached no rows for is not in it. |
snapshot_fill | per_book, the allocation policy above. |
truncated_books | Present only when the budget ran out. Keyed by bookmaker, with the served and omitted row counts for this build. served can be 0, which means this snapshot held rows for that book and the budget reached none of them. Raise limit, or narrow your filters, to see more. |
const es = new EventSource(
"https://parlay-api.com/v1/sse/hot/baseball_mlb?apiKey=YOUR_KEY&bookmakers=fanduel&kinds=game&heartbeat_s=5"
);
es.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
if (msg.type === "odds_update") console.log(msg.data);
};A book listed in truncated_books with a non-zero served is in data; one listed with served 0 is not, and does not appear in books either. A book in missing_books is a different case again: that field only appears on a partial frame, where a book's snapshot could not be read at all and the stream was served without it rather than refused.
There is no guarantee of eventual delivery of the omitted rows. An odds_update frame is sent when a row changes, so a line that does not move may never be pushed on this connection. If you need those rows in hand, raise limit, narrow bookmakers / markets / kinds, or inspect the available REST view at /v1/sports/{sport_key}/odds. That REST view is not a complete-baseline continuation protocol for DraftKings or FanDuel.
What the fill does not touch: the merged selector, the native current page and stream lifecycle behaviour are unchanged. Your limit carries into the re-attach, so a connection served a partial frame and re-attached later is filled at the same budget it asked for on connect.
For DraftKings and FanDuel pregame game lines, the supported REST request is /v1/sports/americanfootball_ncaaf/odds?bookmakers=draftkings,fanduel®ions=us&markets=h2h,spreads,totals&oddsFormat=american&dateFormat=iso&include=normalized,verification. /v1/odds is not a supported route.
On these bookmaker blocks, verified_at is a source/fixture poll heartbeat. Its key does not bind a market, selection, line or displayed price. is_current reports whether that heartbeat is present and within the five-second threshold at response assembly; it does not certify every outcome. topped_up=true means the view retained older price rows. A fresh heartbeat does not independently reverify those rows or prove that none was suspended or withdrawn. line_changed_at is the retained row timestamp used by this legacy view, not a universal upstream price-change clock.
Stream last_update retains the stored price-row or source timestamp; it is not guaranteed to advance when a poll sees an unchanged price. The frame timestamp is a send clock, and price_age_s tracks retained price-change history. Neither a frame heartbeat nor a bookmaker heartbeat is authoritative per-outcome verification. The requested game-line age bound is rechecked on cached initial rows and before sending updates; dropping an expired row does not emit a source withdrawal.
DraftKings/FanDuel initial frames use snapshot_scope="recent_rows" and snapshot_complete=false. An initial_state frame ends that bounded initial response, not a complete current-board enumeration. There is no supported pagination cursor or end-of-snapshot signal that turns these views into a guaranteed complete current baseline. since filters retained observations; it is not an exactly-once replay cursor.
truncated=false does not override snapshot_complete=false. truncated_books counts only known rows omitted by the final row budget. A fetch cap or another partial read can set truncated=true with snapshot_partial_reasons and no exact omitted counts.
These DraftKings/FanDuel feeds do not provide a guaranteed withdrawal stream or an authoritative complete replacement on reconnect. Missing rows and unchanged prices omitted from a snapshot cannot be classified as withdrawn from absence alone. Apply your own observation-age rejection, but do not treat it as proof of upstream suspension. Integrations requiring verified per-outcome current status, complete replacement and reliable removals cannot obtain those guarantees from this contract.
| type | When | Payload |
|---|---|---|
initial_state | On connect | Up to limit rows for the sport, shared across the requested books |
odds_update | Every change | Array of changed rows |
arb_flagged | New arb detected | The arb opportunity (5s scanner) |
heartbeat | Every 30s | Connection health |
Send a subscribe frame after connect:
{"type": "subscribe", "event_id": "ee78855a3bdd1019"}To unsubscribe and receive sport-wide updates again:
{"type": "unsubscribe"}from parlay_api import ParlayAPI
import asyncio, json
import websockets
async def stream():
client = ParlayAPI(api_key="YOUR_KEY")
url = client.websocket_url("baseball_mlb")
async with websockets.connect(url) as ws:
async for raw in ws:
frame = json.loads(raw)
if frame["type"] == "odds_update":
for row in frame["data"]:
print(row["bookmaker"], row["player"],
row["over_price"], row["under_price"])
asyncio.run(stream())WebSocket minimum push intervals are Business 1.0 s, Enterprise 0.5 s and Scale 0.0 s. These are server-side coalescing settings, not book-publication-to-client latency bounds. Source polling, observation age, database-to-client delivery and HTTP request duration measure different stages; see timing and freshness.
last_update, price_age_s and line_changed_at_msEvery streamed row carries last_update (epoch milliseconds we last wrote or re-verified that price) and price_age_s, a server-computed convenience field with no client clock-skew guesswork. price_age_s is the seconds since the price last actually MOVED, not since the last write: when a book re-emits an unchanged price as a verification write, last_update advances but price_age_s keeps counting from the real move, so a frozen line never reads as fresh. The row also carries line_changed_at_ms, the epoch milliseconds of that last real move, if you want to compute the age yourself.
To protect live in-play consumers, a fast book's line that has stayed frozen for more than about 150 seconds during a commenced game is dropped from the live feed rather than streamed with a fresh-looking timestamp, so the socket agrees with /live. Read price_age_s if you want to enforce your own tighter or looser staleness cutoff.
EventSource-compatible HTTP stream for enterprise hot paths. It sends a connection frame, source freshness, initial state, then live updates with a 5-second heartbeat.
| param | example | meaning |
|---|---|---|
kinds | game,prop | Game lines, props, or both |
markets | player_points,player_rebounds | Prop market keys |
event_id | 2026-05-07_Team_A_Team_B | Single event filter |
heartbeat_s | 5 | 1 to 30 seconds |
limit | 1000 | Rows in the initial_state frame. 1 to 1000, default 500. Identical to /ws/odds, and shared across the requested books the same way. |
max_age_s | 600 | Maximum retained game-line observation age on initial snapshots and before sending updates, including game selections stored as prop rows. 1 to 3600 seconds, default 600; ordinary player props retain their existing window. |
The initial_state frame here is the same frame /ws/odds sends, built by the same code and filled per book: see The initial_state frame is filled per book above for books, snapshot_fill and truncated_books. Requesting the same params on either transport returns the same rows.
# Python quickstart
import json, requests
url = "https://parlay-api.com/v1/sse/hot/baseball_mlb"
params = {
"apiKey": "YOUR_KEY",
"bookmakers": "fanduel",
"kinds": "game",
"heartbeat_s": 5,
}
with requests.get(url, params=params, stream=True, timeout=60) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if line and line.startswith("data: "):
msg = json.loads(line[6:])
if msg["type"] in ("hot_feed_status", "odds_update"):
print(msg)Hot feed means fast delivery once a book update lands in our pipeline. It does not invent prices or promise that every external book publishes a new price every 5 seconds.
Operational check: admins run python3 scripts/verify_book_coverage.py before broad outreach or deploys to prove active books survive REST and SSE visibility.
The feed behind our own live board: game lines for one sport, plus the moneylines the board overlays onto each game card. It is a narrower feed than /ws/odds, not a live-only view of it. For spreads, totals, player props and alternate lines, use /ws/odds and read commence_time to pick out the games already under way.
Auth is identical to /ws/odds: the X-API-Key header, ?apiKey=, or ?api_key=, on a Business / Enterprise / Scale key. See Authentication above. The live dashboard opens this same socket with its session cookie, which is why a logged-in browser needs no key; this is the one route where the cookie is a credential.
Three protocol differences from /ws/odds, worth knowing before porting a client:
initial_state frame. You get {"type":"connected", ...} and then odds_update diffs, so inspect GET /v1/sports/{sport_key}/odds or use /ws/odds for a bounded initial view. Neither supplies a guaranteed complete current DraftKings/FanDuel baseline.bookmakers, markets, kinds, diff, since, event_id and max_age_s belong to /ws/odds; on this route they are ignored rather than applied.{"type":"subscribe","event_id":"..."} to lock to one game, and that game arrives in full, which is what the board's game modal uses.This route used to accept the dashboard cookie and nothing else, so an API key was closed with 4001 "Not logged in" no matter which form it arrived in. Fixed 2026-09-05. If you worked around it by polling, you can go back to the socket.
| Code | Meaning | Action |
|---|---|---|
200 | OK | Use response body |
400 | Invalid sport_key or param | Check spelling and Sport Keys |
401 | Missing or invalid API key | Pass X-API-Key header or ?apiKey= |
403 | Credit limit exceeded | Wait for monthly reset or upgrade tier |
404 | Resource not found | Endpoint or event_id doesn't exist |
422 | Missing required param | Check the param table for that endpoint |
429 | Rate limited | Slow down or retry with backoff |
500 | Server error | Retry. If it persists, email [email protected] |
The Python SDK raises typed exceptions: InvalidAPIKeyError, CreditLimitExceededError, RateLimitedError, TierGatedError, all subclasses of ParlayAPIError.
Live keys (those with active events in the last 24h). The /v1/sports endpoint returns the current authoritative list.
| Key | Sport |
|---|---|
baseball_mlb | MLB |
basketball_nba | NBA |
basketball_wnba | WNBA |
basketball_ncaab | NCAAB |
icehockey_nhl | NHL |
americanfootball_nfl | NFL |
americanfootball_ncaaf | NCAAF |
mma_mixed_martial_arts | MMA / UFC |
tennis_atp | ATP |
tennis_wta | WTA |
soccer_epl | English Premier League |
soccer_spain_la_liga | La Liga |
soccer_germany_bundesliga | Bundesliga |
soccer_italy_serie_a | Serie A |
soccer_france_ligue_one | Ligue 1 |
soccer_usa_mls | MLS |
golf_pga_championship | PGA Championship |
disc_golf | Disc Golf |
esports_lol | League of Legends |
esports_cs2 | Counter-Strike 2 |
esports_valorant | Valorant |
Smaller leagues rotate in and out of the upstream offer, so a key is live only while that league has active events. GET /v1/sports is the authoritative list of what is live right now, and the umbrella basketball key aggregates every child league in a single call. Any key is queryable at /v1/sports/{sport_key}/odds, /props, /ev, /arbitrage, /consensus and the other per-sport endpoints.
GET /v1/sports serves 90+ keys in total. Check current per-book coverage before selecting a sport.
| Key | Book | Type |
|---|---|---|
draftkings | DraftKings | Sportsbook |
fanduel | FanDuel | Sportsbook |
caesars | Caesars | Sportsbook |
bovada | Bovada | Sportsbook |
betmgm | BetMGM | Sportsbook |
fanatics | Fanatics | Sportsbook |
fliff | Fliff | Sportsbook |
bet365 | bet365 | Sportsbook |
betrivers | BetRivers | Sportsbook |
hardrock | Hard Rock | Sportsbook |
parx | Parx | Sportsbook |
pmu | PMU | Sportsbook (FR) |
unibet | Unibet | Sportsbook (EU) |
betrivers_ca | BetRivers (CA) | Sportsbook (CA) |
sportsbet_au | Sportsbet (AU) | Sportsbook (AU) |
rushbet | RushBet | Sportsbook (LATAM) |
novig | Novig | Exchange |
kalshi | Kalshi | Prediction market |
polymarket | Polymarket | Prediction market |
robinhood | Robinhood Event Contracts | Prediction market |
prizepicks | PrizePicks | DFS pick'em |
underdog | Underdog | DFS pick'em |
betr | Betr | DFS pick'em |
sleeper | Sleeper | DFS pick'em |
pick6 | Pick6 (DraftKings) | DFS pick'em |
This table is a subset. GET /v1/bookmakers is the integration registry; ?all=true also includes non-active entries and their status. status: active and endpoints describe configured integrations and request families, not current data availability.
For a props source such as Betr or ParlayPlay, follow its freshness_path to /v1/bookmakers/{key}/freshness. The top-level live_props flag means the latest stored prop observation is at most 300 seconds old. It does not establish priced coverage or delivery for your requested sport and markets. Check the actual /v1/sports/{sport_key}/props?bookmakers={key} response, including row timestamps and source-specific price semantics; neither registry status nor a recent write guarantees a non-empty or complete result.
The most-used market keys per sport. Hit /v1/sports/{sport_key}/props/markets for the full live list.
player_total_bases, player_hits, player_home_runs, player_rbis, player_runs, player_singles, player_doubles, player_triples, player_walks, player_strikeouts, player_pitcher_outs, player_hits_allowed, player_earned_runs, player_hits_runs_rbis, player_first_hit, player_first_home_run
player_points, player_rebounds, player_assists, player_threes, player_steals, player_blocks, player_turnovers, player_pra (pts+reb+ast), player_pts_rebs, player_pts_asts, player_rebs_asts, player_double_double, player_triple_double
player_goals, player_assists, player_points_nhl, player_shots_on_goal, player_saves, player_anytime_goal, player_powerplay_points, player_first_goal_scorer, player_anytime_goal_scorer
player_pass_yds, player_pass_tds, player_pass_completions, player_rush_yds, player_rec_yds, player_receptions, player_anytime_td, player_first_td, player_longest_rec, player_interceptions
player_anytime_goalscorer, player_shots_on_target, player_assists, player_goals_assists, player_fouls, player_total_sets, plus the standard h2h, spreads, totals at the game level.
DFS boards publish first-quarter and first-half player lines alongside the full-game
line for the same player and stat. They are separate markets and they carry their own
market keys, using the same _1st_quarter / _1st_half suffixes as the
game-line period markets: player_pass_yds_1st_quarter,
player_pass_yds_1st_half, player_rush_yds_1st_quarter,
player_rush_yds_1st_half, player_reception_yds_1st_quarter,
player_reception_yds_1st_half, player_receptions_1st_quarter,
player_receptions_1st_half, player_fantasy_points_1st_quarter,
player_fantasy_points_1st_half. GET /v1/markets is the authoritative
list.
Every /v1/sports/{sport_key}/props row carries a period field, so
you never have to parse the key yourself: "FULL" for a confirmed full-game line,
"Q1" for first quarter, "1H" for first half (and "P1" /
"F5" where a sport uses them). Legacy unsuffixed PrizePicks rows whose segment
was not retained say "UNKNOWN" instead of being guessed as full game. It sits on
the prop, not on each book, because the segment is a property of the market rather than of
who priced it.
Cutover note. Until this change was deployed, first-quarter and
first-half player projections were written under the full-game key, so one
player/market/snapshot group could hold several different lines with nothing to tell them
apart. Rows written from the deploy onward carry the suffixed key and the correct
period. There is no single cutover instant: the PrizePicks collection paths
are deployed separately, so each one changes over when it ships. Rows written before a path
changed over keep the old mixed key and report "UNKNOWN": the segment was never
stored, so it cannot be recovered from the archived row. Until those legacy rows age out, a
filter on market_key=player_pass_yds can still include them alongside confirmed
full-game lines.
If you're already using TOA, some moneyline, spread, and total endpoints have compatible request patterns. A host change alone does not complete a migration: create a ParlayAPI key, map your event IDs, and validate response fields, credit costs, and coverage for the exact sports, markets, books, and dates your application uses.
# Was: TOA_BASE = "https://api.the-odds-api.com/v4" # Now: TOA_BASE = "https://parlay-api.com/v1"
Existing clients may need changes for authentication, identifiers, response handling, or endpoint-specific parameters. Compare the returned events, bookmakers, markets, timestamps, errors, and actual credits charged on a representative sample before switching production traffic. Player props use a different response shape; see Player Props. For customer-facing display, resale or redistribution, sublicensing, commercial derived products, or an SLA, see the terms and contact support about a separate signed agreement with express scope.
Detailed comparison: parlay-api.com vs the-odds-api
Need live odds on a page without writing any code? One script tag renders a compact moneyline table (next games, up to 4 major US books) into any page. Keyless and free; the visible "Live odds by ParlayAPI" footer link is the license. Odds are cached server-side for about a minute per sport.
<div data-parlayapi-widget data-sport="americanfootball_nfl"></div> <script async src="https://parlay-api.com/widget.js"></script>
Pick a sport and preview it live at parlay-api.com/widget. The backing
feed is GET /v1/widget/odds?sport={sport_key} (keyless, h2h only, 60 requests/hour
per IP, honest cache_age_seconds in the response). For full data in your own UI,
use GET /v1/sports/{sport_key}/odds with an API key.
Get a free API key in under a minute: 1,000 credits a month, no credit card required.
Get a free API keyPrefer to read more first? Try the answers hub, the cookbook, or the free betting calculators.