Response Shapes
Game-line odds, props, futures, prediction markets, and exchange markets each return a slightly different JSON shape. They diverged because they grew at different times in different parts of the system. This page is the explicit map: what each looks like, how they differ, when each was introduced, and how to handle the differences in client code.
Why three shapes (instead of one)
Game-line odds inherits the the-odds-api.com shape because we ship as a drop-in. Player props grew on top of that with extra fields for player and market. Futures came later for season-long markets and uses a flatter shape since there are no recurring "events". Prediction markets and exchanges add a few more fields to expose volume and bid/ask. We won't break v1 to merge these; the answer is to document the differences carefully and offer normalization helpers in the SDK.
Game-line odds (h2h, spreads, totals)
Endpoint: GET /v1/sports/{sport_key}/odds. Shape returned:
[
{
"id": "abc123",
"sport_key": "baseball_mlb",
"sport_title": "MLB",
"commence_time": "2026-05-01T19:00:00Z",
"home_team": "Boston Red Sox",
"away_team": "Houston Astros",
"bookmakers": [
{
"key": "draftkings",
"title": "DraftKings",
"last_update": "2026-05-01T18:56:55Z",
"markets": [
{
"key": "h2h",
"last_update": "2026-05-01T18:56:55Z",
"outcomes": [
{ "name": "Boston Red Sox", "price": -126 },
{ "name": "Houston Astros", "price": +108 }
]
},
{
"key": "spreads",
"last_update": "2026-05-01T18:56:55Z",
"outcomes": [
{ "name": "Boston Red Sox", "price": -110, "point": -1.5 },
{ "name": "Houston Astros", "price": -110, "point": +1.5 }
]
},
{
"key": "totals",
"last_update": "2026-05-01T18:56:55Z",
"outcomes": [
{ "name": "Over", "price": -110, "point": 8.5 },
{ "name": "Under", "price": -110, "point": 8.5 }
]
}
]
}
]
}
]
| Field | Notes |
|---|---|
id | Stable per (date, home, away). Use as a join key with /props and /scores. |
commence_time | ISO 8601, UTC, kickoff time. Pre-game window is everything before this. |
bookmakers[].last_update | Per-book freshness. We capture this directly from each book's response. Treat as upper bound on price age. |
markets[].last_update | Per-market freshness within a book. Usually equals bookmakers[].last_update; can lag if the book updates h2h faster than spreads. |
outcomes[].price | American odds by default. Use ?oddsFormat=decimal to flip. |
outcomes[].point | Spread number for spreads, total for totals. Absent on h2h. |
Player props
Endpoint: GET /v1/sports/{sport_key}/props. Different shape: instead of nesting markets inside bookmakers inside events, we flatten one row per (event, book, market, player, line) tuple. This makes filter-by-player and filter-by-market dramatically easier in clients.
[
{
"event_id": "abc123",
"sport_key": "baseball_mlb",
"commence_time": "2026-05-01T19:00:00Z",
"home_team": "Boston Red Sox",
"away_team": "Houston Astros",
"source": "draftkings",
"source_title": "DraftKings",
"player_name": "Aaron Judge",
"market_key": "batter_home_runs",
"market_label": "Home Runs",
"line": 0.5,
"over_price": +295,
"under_price": -380,
"over_implied_prob": 0.2532,
"under_implied_prob": 0.7917,
"snapshot_time": "2026-05-01T18:55:30Z",
"raw_json": "{ ... full source response ... }"
}
]
event_id in your code.
Implied probability fields (over_implied_prob, under_implied_prob) are derived from the raw American prices via the standard formula. We include them because every client computes them anyway. They're not different data from over_price / under_price; they're a convenience.
Futures
Endpoint: GET /v1/sports/{sport_key}/futures. Season-long markets (championship, MVP, totals). No recurring events, so we flatten further:
[
{
"sport_key": "basketball_nba",
"season": "2025-26",
"market_key": "nba_championship_winner",
"market_label": "NBA Championship Winner",
"selection": "Boston Celtics",
"source": "draftkings",
"source_title": "DraftKings",
"price": +450,
"implied_prob": 0.1818,
"snapshot_time": "2026-05-01T18:55:30Z",
"raw_json": "{ ... }"
}
]
One row per (season, market, selection, book). No nested arrays. commence_time is absent because futures don't have a single kickoff. line is absent because there's no over/under in most championship markets (use price).
Prediction markets
Endpoint: GET /v1/prediction-markets/{sport_key}. Kalshi and Polymarket prices normalized to American/decimal odds plus market-specific fields:
[
{
"event_id": "kalshi-MLBYANK-NEW-2026-05-01",
"sport_key": "baseball_mlb",
"commence_time": "2026-05-01T19:00:00Z",
"home_team": "Boston Red Sox",
"away_team": "Houston Astros",
"source": "kalshi",
"selection": "Boston Red Sox",
"yes_price": +112,
"no_price": -130,
"yes_implied_prob": 0.4717,
"no_implied_prob": 0.5652,
"volume_24h_usd": 4287.50,
"raw_json": "{ ... }"
}
]
Same general shape as game-line odds, with two additions:
yes_price/no_priceinstead of nestedbookmakers[].markets[].outcomes[]. Prediction-market contracts are binary; we expose them flat.volume_24h_usd: dollar volume traded on the contract in the last 24 hours. Absent for sportsbook responses since books don't disclose volume.
Exchange markets (Novig, ProphetX)
Endpoint: GET /v1/exchange/{sport_key}/markets. Exchanges expose bid/ask + matched volume, which sportsbooks don't:
[
{
"event_id": "abc123",
"sport_key": "baseball_mlb",
"commence_time": "2026-05-01T19:00:00Z",
"home_team": "Boston Red Sox",
"away_team": "Houston Astros",
"source": "novig",
"market_key": "h2h",
"selection": "Boston Red Sox",
"best_bid": -130,
"best_ask": -118,
"last_traded": -124,
"volume_usd": 1245.00,
"is_consensus": true,
"raw_json": "{ ... }"
}
]
Raw vs normalized
Every response includes a raw_json field on each row containing the full untouched response from the upstream source. The other fields are normalized: we extract, type-coerce, and present them in a consistent shape across sources. raw_json is your escape hatch when the normalized fields don't expose what you need.
You can opt out of raw_json per-request to slim payloads:
?include=normalized # (default) full payload incl. raw_json
?include=slim # drops raw_json from every row, ~40% smaller responses
?include=raw # ONLY raw_json + identifying fields (event_id, source, etc)
Use slim for production hot paths where you've validated you don't need the upstream blob. Use raw when you're debugging or want the source-of-truth representation.
Per-book timestamps
Three places to find freshness, in increasing specificity:
- Top-level response header
X-Data-As-Of: when this query was served from cache (or freshly computed if uncached). - Per-row
snapshot_time(props, futures, prediction markets) orbookmakers[].last_update(game-line odds): when we captured this specific row from the source. - Per-market
markets[].last_updatewithin a bookmaker: when this specific market within this book last changed.
For backtesting / model training, use the per-row timestamp. For "is this fresh enough to bet on", use the per-market timestamp.
Side-by-side: same game, three shapes
game-line odds
{
"id": "abc123",
"home_team": "...",
"away_team": "...",
"commence_time": "...",
"bookmakers": [
{
"key": "draftkings",
"markets": [
{
"key": "h2h",
"outcomes": [
{ "name": "...", "price": -126 }
]
}
]
}
]
}
player props
{
"event_id": "abc123",
"source": "draftkings",
"player_name": "Aaron Judge",
"market_key": "batter_home_runs",
"line": 0.5,
"over_price": +295,
"under_price": -380
}
futures
{
"season": "2025-26",
"market_key": "nba_championship_winner",
"selection": "Boston Celtics",
"source": "draftkings",
"price": +450
}
Schema stability policy
- We add fields without versioning. New optional fields on responses don't break clients.
- We never remove or rename existing fields under
/v1/. Anything we want to remove ships under/v2/as a new namespace. raw_jsonis upstream-defined and can change at any time. We don't promise its shape. Treat it as escape hatch.- Field semantics are documented here. If we change a semantic (extremely rare), we ship a per-endpoint deprecation header, give 30 days notice, and run both old and new in parallel during the window.
The detailed migration log lives in the changelog.