An odds API gives developers programmatic access to live betting prices. Here's everything you need to know — types, providers, use cases, and code examples.
Every global odds API skips Australian racing. Here is what a complete racing data API actually needs — fields, fixed odds, exchange, tote, sectionals, connections — and who carries it.
Backtest your betting model against 232K+ historical odds records. Step-by-step Python tutorial with real data from the Krok Odds archive.
An odds API is the difference between a betting product you can build in a weekend and one you cannot build at all. Without one you are scraping 140+ bookmaker websites, each with its own DOM, its own bot protection, and its own habit of silently changing structure on a Tuesday afternoon. With one, you make an HTTP request and get normalised JSON.
This guide covers what odds APIs actually do, the categories they fall into, how they are priced, the design decisions that bite later, and working code for the common patterns.
At minimum, an odds API models four nested concepts:
The hard part is not serving that structure. It is normalisation. Sportsbet calls a team "Brisbane Lions", another book calls it "Brisbane", a third calls it "Bris Lions". A racing feed identifies a runner by saddlecloth number, another by name, another by an internal ID that changes daily. An odds API worth paying for resolves all of that to stable identifiers so you can join across bookmakers without writing fuzzy matching yourself.
{
"success": true,
"data": [
{
"event_id": "afl-2026-r14-coll-ess",
"sport": "aussierules_afl",
"commence_time": "2026-06-12T09:20:00Z",
"home_team": "Collingwood Magpies",
"away_team": "Essendon Bombers",
"markets": [
{
"key": "h2h",
"selections": [
{
"name": "Collingwood Magpies",
"prices": [
{ "bookmaker": "sportsbet", "price": 1.91, "captured_at": "2026-06-12T08:55:02Z" },
{ "bookmaker": "ladbrokes", "price": 1.95, "captured_at": "2026-06-12T08:55:04Z" },
{ "bookmaker": "bet365", "price": 1.93, "captured_at": "2026-06-12T08:54:58Z" }
]
}
]
}
]
}
],
"meta": { "count": 1, "tier": "api", "timestamp": "2026-06-12T08:55:10Z" }
}| Type | What it gives you | Typical latency | Good for |
|---|---|---|---|
| Pre-match odds | Prices for upcoming events across books | 30s – 5min | Comparison sites, EV screens, model inputs |
| Live / in-play odds | Prices during an event | 1s – 15s | Trading tools, live dashboards |
| Historical / archive | Time series of past prices and results | Batch | Backtesting, CLV analysis, model training |
| Derived / analytics | Arbs, +EV, middles, steam, projections | Depends on source | Products where you do not want to build the maths |
Most projects need at least two. A comparison dashboard needs pre-match plus historical for the movement chart. A model needs historical to train and pre-match to deploy against.
This distinction gets glossed over and it matters more than almost anything else.
Licensed feeds are purchased wholesale from aggregators who hold commercial arrangements with bookmakers. Coverage is broad, especially internationally, and the data is contractually clean. The trade-offs are cost, latency (the aggregator is an extra hop), and coverage gaps — Australian white-label bookmakers and smaller racing operators are frequently missing entirely.
Direct-scrape feeds are collected by the API provider from public bookmaker endpoints. Latency is lower because there is no intermediary, coverage of Australian books is dramatically better, and the provider controls the update cadence. The trade-offs are engineering burden — scrapers break — and the need to be honest about provenance.
Krok Odds runs 16 direct-scrape feeds across Australian corporate books alongside licensed coverage, and every /v1 response carries a provenance label of direct-scrape, licensed or derived so you always know which you are consuming. Full endpoint list is in the API documentation.
Authentication is almost always a header-based API key. Here is the pattern against the Krok Odds sports feed:
curl -s "https://krokodds.com.au/api/v1/odds-feed/sports/afl?limit=5" \
-H "X-API-Key: $KROK_API_KEY" | jq '.data[0]'TypeScript:
type Price = { bookmaker: string; price: number; captured_at: string };
type Selection = { name: string; prices: Price[] };
type Market = { key: string; selections: Selection[] };
type Event = {
event_id: string;
sport: string;
commence_time: string;
home_team: string;
away_team: string;
markets: Market[];
};
async function getOdds(sport: string): Promise<Event[]> {
const res = await fetch(
`https://krokodds.com.au/api/v1/odds-feed/sports/${sport}?limit=50`,
{
headers: { 'X-API-Key': process.env.KROK_API_KEY! },
next: { revalidate: 60 },
}
);
if (!res.ok) throw new Error(`odds feed ${res.status}`);
const json = await res.json();
return json.data as Event[];
}Python:
import os
import requests
BASE = "https://krokodds.com.au/api/v1"
HEADERS = {"X-API-Key": os.environ["KROK_API_KEY"]}
def get_odds(sport: str, limit: int = 50):
r = requests.get(f"{BASE}/odds-feed/sports/{sport}",
params={"limit": limit},
headers=HEADERS,
timeout=15)
r.raise_for_status()
return r.json()["data"]
for ev in get_odds("afl"):
h2h = next((m for m in ev["markets"] if m["key"] == "h2h"), None)
if not h2h:
continue
for sel in h2h["selections"]:
best = max(sel["prices"], key=lambda p: p["price"])
print(f'{ev["home_team"]} v {ev["away_team"]}: {sel["name"]} '
f'best {best["price"]} @ {best["bookmaker"]}')Raw prices are rarely the end goal. Three derivations cover most use cases.
function bestPrices(market: Market) {
return market.selections.map((s) => {
const best = s.prices.reduce((a, b) => (b.price > a.price ? b : a));
return { selection: s.name, price: best.price, bookmaker: best.bookmaker };
});
}function bookPercentage(prices: number[]): number {
return prices.reduce((sum, p) => sum + 1 / p, 0) * 100;
}
// > 100 means margin; < 100 across best available prices means arbitrage.
const pct = bookPercentage([1.95, 2.10]); // 98.9 → arb
const margin = pct > 100 ? pct - 100 : 0;function devigProportional(prices: number[]): number[] {
const implied = prices.map((p) => 1 / p);
const sum = implied.reduce((a, b) => a + b, 0);
return implied.map((p) => p / sum);
}If you would rather not maintain this logic, the derived endpoints (/v1/opportunities/arbitrage, /v1/opportunities/positive-ev, /v1/opportunities/low-holds, /v1/opportunities/middles) return the computed opportunities directly, with same-corporate-group pairings already suppressed.
Polling every 30 seconds is fine for a comparison table. It is useless for arbitrage, where the median opportunity lifetime is measured in tens of seconds. For that, use server-sent events:
import json
import requests
url = "https://krokodds.com.au/api/v1/stream/opportunities"
params = {"types": "arbs,middles", "sport_key": "aussierules_afl", "min_value": "1.0"}
with requests.get(url, params=params,
headers={"X-API-Key": KEY, "Accept": "text/event-stream"},
stream=True, timeout=None) as r:
event = None
for raw in r.iter_lines(decode_unicode=True):
if raw is None or raw == "":
continue
if raw.startswith("event: "):
event = raw[7:]
elif raw.startswith("data: "):
payload = json.loads(raw[6:])
if event == "snapshot":
print("initial", payload["type"], len(payload["rows"]))
elif event == "delta":
print("delta", payload)The stream emits ready, then a snapshot per requested type, thendelta events as opportunities appear and disappear. It caps at ten minutes of wall-clock, so implement reconnection — EventSource does this for you in the browser.
| Model | How it works | Watch for |
|---|---|---|
| Request quota | N calls per month | Whether a paginated sweep counts as one call or twenty |
| Weighted credits | Endpoints cost different amounts | Bulk and historical endpoints often cost 10-50x a list call |
| Per-market billing | Charged by markets returned | A single wide request can consume an entire monthly allowance |
| Seat / flat licence | Unlimited within fair use | "Fair use" is usually undefined until you exceed it |
Two questions to ask any provider before you commit: do empty responses consume quota (Krok Odds refunds the full credit cost on an empty result), and what happens at the limit — hard 429, soft throttle, or overage billing. The answers change your architecture.
revalidate, a Redis read-through, or a simple in-process TTL map all work.Retry-After, use exponential backoff with jitter, and never retry a 4xx other than 429.A short evaluation checklist that will save you a rewrite:
That last point catches more teams than any other. An API that returns{ "data": [] } on an internal failure will silently break your product, and you will not find out until a user asks why Saturday had no AFL games.
If you are building against the Australian market, start with a pre-match feed plus the historical archive, cache at your edge, and add streaming only once you have a latency requirement you can articulate. The Krok Odds API has a free tier that covers the first two, and the endpoint reference with request and response examples for every route is at /api-docs.
Pre-match, live, racing, historical archive and derived opportunity endpoints — all provenance-labelled. Free tier, no card required.
Get an API key →
David has been running advantage betting strategies across Australian bookmakers since 2023 and contributes long-form retrospectives, case studies, and operational pieces drawn from years of running real bets in AU markets. His writing focuses on the realities of running a sustainable AU advantage operation — what works, what fails, and the operational details most blogs gloss over.
Racing arbitrage between Betfair and fixed-odds bookmakers is real but underused. Here's how it works, the measured data, and how to do it.