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 }
            ]
          }
        ]
      }
    ]
  }
]
FieldNotes
idStable per (date, home, away). Use as a join key with /props and /scores.
commence_timeISO 8601, UTC, kickoff time. Pre-game window is everything before this.
bookmakers[].last_updatePer-book freshness. We capture this directly from each book's response. Treat as upper bound on price age.
markets[].last_updatePer-market freshness within a book. Usually equals bookmakers[].last_update; can lag if the book updates h2h faster than spreads.
outcomes[].priceAmerican odds by default. Use ?oddsFormat=decimal to flip.
outcomes[].pointSpread 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 ... }"
  }
]
Why flatter: when you query for "Aaron Judge home runs across all books", the natural shape is one row per book. Nesting books inside events forces clients to flatten anyway. The trade-off: comparing the same event across markets requires grouping by 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:

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:

  1. Top-level response header X-Data-As-Of: when this query was served from cache (or freshly computed if uncached).
  2. Per-row snapshot_time (props, futures, prediction markets) or bookmakers[].last_update (game-line odds): when we captured this specific row from the source.
  3. Per-market markets[].last_update within 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

The detailed migration log lives in the changelog.