Endpoints
Get Odds
Every bookmaker's price for a sport in one call, with per-book freshness.
/v1/sports/{sport_key}/oddsAPI keyAll events for a sport with each source's prices attached. Events are matched across books, so one entry carries every book's line for the same fixture.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
sport_key | string | required | One of ufc, boxing, tennis, nfl, golf. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
markets | string | optional | moneyline, spread, total, outright. Comma-separated; defaults to all. |
books | string | optional | Comma-separated sources to include, e.g. pinnacle,fanduel. |
exclude_books | string | optional | Sources to leave out, e.g. kalshi,polymarket. |
min_books | integer | optional | Only events priced by at least this many sources. |
min_limit | number | optional | Only outcomes with a published max stake at or above this. |
unknown_limit | string | optional | What to do with outcomes whose source publishes no limit: exclude (default) or include. |
min_liquidity | number | optional | Only outcomes with at least this much depth behind them. |
commence_before | string | optional | ISO timestamp, or a relative window like 24h / 3d. |
commence_after | string | optional | Same formats as commence_before. |
fresh_only | boolean | optional | Drop any source currently flagged stale. |
include | string | optional | Computed extras: best, novig, arb. Comma-separated. |
arb_only | boolean | optional | With include=arb, return only events where an arbitrage exists. |
odds_format | string | optional | american (default), decimal or probability. |
sort | string | optional | commence_time, books or arb. |
limit | integer | optional | Page size. total_matching reports the unpaged count. |
offset | integer | optional | Pagination offset. |
{
"sport": "ufc",
"title": "UFC",
"generated_at": "2026-08-09T23:21:37.714Z",
"sources": [
{
"book": "bovada",
"name": "Bovada",
"last_update": "2026-08-09T23:21:34.849Z",
"stale": false,
"latency_ms": 39
},
{
"book": "pinnacle",
"name": "Pinnacle",
"last_update": "2026-08-09T23:21:24.662Z",
"stale": false,
"latency_ms": 76
},
{
"book": "unibet",
"name": "Unibet",
"last_update": "2026-08-09T23:21:22.444Z",
"stale": false,
"latency_ms": 157
}
],
"bout_count": 29,
"bouts": [
{
"home": "Jeremiah Wells",
"away": "Myktybek Orolbai",
"commence_time": "2026-08-15T21:30:00.000Z",
"books": [
{
"book": "bovada",
"last_update": "2026-08-09T23:21:34.849Z",
"outcomes": [
{
"name": "Jeremiah Wells",
"price": 500
},
{
"name": "Myktybek Orolbai",
"price": -700
}
]
},
{
"book": "pinnacle",
"last_update": "2026-08-09T23:21:24.662Z",
"outcomes": [
{
"name": "Myktybek Orolbai",
"price": -650
},
{
"name": "Jeremiah Wells",
"price": 482
}
]
},
{
"book": "unibet",
"last_update": "2026-08-09T23:21:22.444Z",
"outcomes": [
{
"name": "Jeremiah Wells",
"price": 430
},
{
"name": "Myktybek Orolbai",
"price": -625
}
]
}
]
}
]
}Market keys
Every outcome carries a market field. Filter with ?markets=.
| Key | Shape | Sports | Sources |
|---|---|---|---|
moneyline | Two outcomes, one per side | UFC, boxing, tennis, NFL | All |
spread | Handicap with a point per side | NFL, tennis | Pinnacle, Bovada |
total | Over / Under with a shared point | NFL, tennis | Pinnacle, Bovada |
outright | One outcome per competitor in the field | Golf | Bovada, Pinnacle, Kambi, FanDuel |
Filters that return nothing
A filter that empties the response explains itself. min_limit excludes outcomes whose source publishes no maximum stake — right for arbitrage, surprising otherwise — so the response carries filter_notes naming which sources publish limits, and filter_report counting what was dropped. Pass unknown_limit=include to keep them.
{
"bout_count": 32,
"filter_notes": [
"204 outcome(s) excluded because their source does not publish a max stake. Sources that do: pinnacle. Pass unknown_limit=include to keep them."
],
"filter_report": {
"unknown_limit_outcomes": 204,
"below_limit_outcomes": 0,
"unknown_handling": "exclude",
"limit_publishers": ["pinnacle"]
}
}Tradeability
A price you cannot actually take is worse than no price. Where a source publishes it, each outcome carries bid and ask alongside price, plus limit (maximum stake) and liquidity (depth behind the quote). Prediction-market prices are the ask, not the mid — the mid is carried separately as mid.
| Field | Meaning | Sources that publish it |
|---|---|---|
price | The price you would take right now | All |
bid / ask | Both sides of the book | Kalshi, Polymarket |
mid | Midpoint, for reference only | Polymarket |
limit | Maximum stake the book will accept | Pinnacle |
liquidity | Depth available at that price | Kalshi, Polymarket, SX Bet |
book_name | The competitor label the source printed | All |
sources carries a fees model — Kalshi charges per contract, SX Bet takes commission on winnings, bookmakers price margin into the line. include=arb applies these for you.Computed extras
Pass include= to have the API do the arithmetic that every consumer would otherwise write themselves.
| Value | Adds |
|---|---|
best | Best available price per outcome and the source offering it |
novig | Fair probabilities with the margin removed, from the tightest book |
arb | Margin, margin after fees, stake split per leg, and the ceiling imposed by limits and depth |
{
"arbitrage": {
"is_arbitrage": true,
"survives_fees": true,
"margin": 1.958,
"margin_after_fees": 0.387,
"max_stake": 1394.79,
"stake_ceiling_unknown": false,
"legs": [
{ "name": "Jonathan Kunneman", "book": "polymarket", "price": -178,
"stake": 653.07, "liquidity": 2830.18, "max_total_stake": 4333.63,
"fee_model": "spread_only" },
{ "name": "Joseph Kropschot", "book": "kalshi", "price": 194,
"stake": 346.93, "liquidity": 483.89, "max_total_stake": 1394.79,
"fee_model": "per_contract" }
]
}
}max_stake is set by the binding leg, so the same 1.96% edge can be worth $1,394 or $26,000 depending on which side runs out first. stake_ceiling_unknown is true when a source publishes neither a limit nor depth — unknown, which is not the same as unlimited.
Freshness
Each entry in sources carries last_update, stale and latency_ms. A source is marked stale once we have not had a good read from it in 90 seconds, and stale sources are excluded from the merged prices.
Caching
Responses carry an ETag and Cache-Control: max-age=2. Send If-None-Match and you will get a 304 when nothing has changed, which does not count against your quota differently but does save you the payload.