An odds feed is a stream or endpoint that delivers prices from bookmakers in a fixed structure: a game, the books pricing it, each book's markets, and each market's outcomes with a price. If you understand that nesting, and how to convert the prices between formats, you can read almost any odds feed and merge it with another.
This guide takes one record apart field by field, then shows how to normalize the parts that differ between sources: odds formats, sides and lines. The example uses the shape ZippyOddsAPI returns, because it's documented in full, but the ideas apply to any json odds feed.
The four levels of a record
Every odds record nests the same way:
- Event. One real game: league, teams, start time, status.
- Bookmaker. One book's view of that game, with when it was read.
- Market. One kind of bet at one line: a total at 224.5, a spread at -3.5, a moneyline.
- Outcome. One side of that market, with its price.
Here's a single event trimmed to one book and one market, as /v1/odds returns it in decimal odds:
{
"id": "evt_0h7k2m9q4x1tzb",
"league": "nba",
"start_time": "2026-11-07T00:30:00.000Z",
"status": "scheduled",
"home": { "id": "nba_celtics", "name": "Boston Celtics" },
"away": { "id": "nba_knicks", "name": "New York Knicks" },
"bookmakers": [
{
"id": "prophetx",
"type": "exchange",
"updated_at": "2026-11-06T17:59:40.000Z",
"markets": [
{
"id": "mkt_0k2v9qa1x7m3pc",
"key": "total",
"period": "full_game",
"line": 224.5,
"suspended": false,
"outcomes": [
{ "id": "sel_1f8s2ld0q9w4ze", "side": "over", "point": 224.5, "price": 1.9259, "liquidity": 840 },
{ "id": "sel_0p3n7ty5h2c6vb", "side": "under", "point": 224.5, "price": 1.8929, "liquidity": 1120 }
]
}
]
}
]
}
The values are illustrative. What matters is where each fact lives.
Fields that do the work
A few fields carry most of the meaning:
| Field | Why it matters |
|---|---|
bookmaker.type | sportsbook, exchange or dfs. An exchange price may need its fee taken off |
bookmaker.updated_at | When this book's prices in the answer were observed. The oldest of what it wraps |
market.key, market.period | What the bet is, and over which part of the game |
market.line | The line the market is at, when it has one |
outcome.side | home, away, draw, over or under |
outcome.point | That side's own handicap on a spread, or the line on a total |
outcome.liquidity | Dollars available at the price, when the venue publishes it |
market.suspended | Whether the market is open. Suspended markets are left out unless you ask |
A good bookmaker data feed carries more than a price. Without the observation time and the suspended flag, you can't tell a live price from a closed or stale one.
Normalizing odds formats
Sources disagree on format. One odds data feed sends American odds, another decimal, a prediction market a probability. Convert everything to one internal form, usually decimal or probability, and convert back only for display.
The arithmetic:
- American to decimal:
1 + a/100for a positive price,1 + 100/|a|for a negative one. - Decimal to American:
(d - 1) × 100whendis 2.0 or more, otherwise-100 / (d - 1). - Decimal to implied probability:
1 / d.
Worked through on the record above:
- Over at 1.9259:
-100 / 0.9259= -108. Implied chance 1 / 1.9259 = 51.92%. - Under at 1.8929:
-100 / 0.8929= -112. Implied chance 52.83%. - Together they come to 104.75%, so this market carries a 4.75% margin.
The same in Python, as generic code with no API calls:
def american_to_decimal(a):
return 1 + a / 100 if a > 0 else 1 + 100 / abs(a)
def decimal_to_american(d):
return round((d - 1) * 100) if d >= 2 else round(-100 / (d - 1))
def implied(d):
return 1 / d
over, under = 1.9259, 1.8929
print(decimal_to_american(over), decimal_to_american(under)) # -108 -112
print(round((implied(over) + implied(under) - 1) * 100, 2)) # 4.75
If your odds feed api lets you pick the format, ask for the one you store. ZippyOddsAPI takes format=american, decimal or probability, with decimal to 4 places and probability to 6. Its stream snapshots are always in American odds.
Normalizing sides and lines
Prices are the easy part. The harder part is making two books' markets line up, because every bookmaker feed names teams, sides and periods its own way.
- Spreads pair by the home line. Home -3.5 at one book pairs with home -3.5 at another, and with away +3.5. Never pair by absolute value, or you'll match home -3.5 with home +3.5.
- Periods must match. A first-half total isn't a full-game total at a lower number. Keep
periodin every key you build. - Lines are part of identity. In ZippyOddsAPI a market whose line moves gets a new market id, while the selection id for the same side and point is shared at every book. Key your store on those ids rather than on names.
Choosing an odds feed provider
When you compare an odds feed provider with another, look past the list of books. Check that every price carries its observation time, that the record says what period and line it's for, and that coverage is reported from prices actually seen. The sports odds API guide compares markets and formats more broadly.
Two related questions come up next. If you only need a few leagues and are testing ideas, our guide to the free live sports API covers what free tiers usually hold back. If you need results as well as prices, see the sports scores API guide. For the full record shape, including the fields only sent when they apply, see the ZippyOddsAPI docs.