Keys a human can read
Markets are named, not numbered. No lookup catalogue to download and join before the first price makes sense.
Odds API · Fonbet · Winline live
Readable market keys, a change feed that never loses a price, and a live stream you can resume. One clean schema for sportsbook odds, built to carry many books.
One request
# every change since you last looked
curl -H "Authorization: Bearer $ODDSPRISM_KEY" \
"https://api.oddsprism.com/v1/odds?sport=29&since=5448040"
import os, requests
r = requests.get(
"https://api.oddsprism.com/v1/odds",
params={"sport": 29, "since": 5448040},
headers={"Authorization": f"Bearer {os.environ['ODDSPRISM_KEY']}"},
)
for c in r.json()["changes"]:
print(c["key"], c["selection"], c["price"])
const r = await fetch(
"https://api.oddsprism.com/v1/odds?sport=29&since=5448040",
{ headers: { Authorization: `Bearer ${process.env.ODDSPRISM_KEY}` } }
);
const { changes } = await r.json();
for (const c of changes) console.log(c.key, c.selection, c.price);
Or keep a stream open
# prices as they change; reconnect and continue
curl -N -H "Authorization: Bearer $ODDSPRISM_KEY" \
"https://api.oddsprism.com/v1/stream?sport=29&scope=live"One response · example
{
"as_of": "2026-10-09T19:45:42Z",
"seq": 5448041,
"changes": [{
"seq": 5448041,
"book": "pinnacle",
"event": "1637098295",
"period": 0,
"key": "handicap",
"line": -1.5,
"selection": "home",
"price": 2.04,
"previous": 2.12,
"first_seen": false
}],
"has_more": false
}Why teams switch
Markets are named, not numbered. No lookup catalogue to download and join before the first price makes sense.
Page a full snapshot, then ask for everything since its counter. Whatever moved while you were paging is still in the answer. Out of sync? You get a clear error, not silent gaps.
A server-sent event stream you can pick up where you left off after a drop. Every price carries its own timestamp, and openings are flagged with first_seen.
Responses are compressed with br, gzip or zstd, typically 8–9× smaller. Single events carry an ETag, so unchanged data costs a 304.
Per-key limits per minute and per day, concurrent-stream caps and optional IP allow-lists. Predictable errors with a retry hint.
Withdrawn prices are explicit (price: null), lines carry stable identifiers, and every price has a unique key across book, event, period, market, line and selection.
Freshness
Every price carries its own update time. This is what we commit to, by data class. No hidden averages.
| Data class | Typical age |
|---|---|
| Live events of the sport currently pushed over the socket | ≈ 1 s |
| Main markets: moneyline, handicap, totals — early and today events | 1–5 min |
| Special markets of distant events (correct score, margins, combinations) | up to 1 h |
| Outrights | up to 1 h |
A commitment about age, not a promise that every market exists at every moment: markets opened late by the source appear within 5 minutes of the next depth read. Current figures: status.
Coverage
We would rather ship a book with complete markets and honest freshness than a long list with gaps. Books join the same schema, so your integration does not change.
Sports today: soccer, basketball, tennis, hockey, baseball, American football, e-sports and more — prices for every sport the book offers, early, today and live.
Early access
We are onboarding a small number of teams. Tell us what you build; you will get a key with limits that fit.