Krok Odds
Guide

What is an Odds API? Complete Guide for Developers

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.

David Koval
David Koval
Senior Advantage Betting Contributor
12 min read·Published 26 Aug 2026

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.

What an odds API returns

At minimum, an odds API models four nested concepts:

  • Event — a match or race, with participants and a start time.
  • Market — a question about that event: head-to-head, line, total, a player prop.
  • Selection — a possible answer: a team, a runner, over/under.
  • Price — a bookmaker's odds for that selection, with a capture timestamp.

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.

A typical response shape

{
  "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" }
}

The four categories of odds API

TypeWhat it gives youTypical latencyGood for
Pre-match oddsPrices for upcoming events across books30s – 5minComparison sites, EV screens, model inputs
Live / in-play oddsPrices during an event1s – 15sTrading tools, live dashboards
Historical / archiveTime series of past prices and resultsBatchBacktesting, CLV analysis, model training
Derived / analyticsArbs, +EV, middles, steam, projectionsDepends on sourceProducts 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.

Licensed vs direct-scrape

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.

Making your first request

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"]}')

Computing the things you actually want

Raw prices are rarely the end goal. Three derivations cover most use cases.

Best price per selection

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 };
  });
}

Book percentage and margin

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;

De-vigged fair probability

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.

Streaming for latency-sensitive use cases

Polling once a minute 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.

Pricing models and the traps in them

ModelHow it worksWatch for
Request quotaN calls per monthWhether a paginated sweep counts as one call or twenty
Weighted creditsEndpoints cost different amountsBulk and historical endpoints often cost 10-50x a list call
Per-market billingCharged by markets returnedA single wide request can consume an entire monthly allowance
Seat / flat licenceUnlimited 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.

Design decisions that bite later

  • Cache aggressively at your edge. If ten users load the same AFL page, that should be one upstream call. Next.js revalidate, a Redis read-through, or a simple in-process TTL map all work.
  • Store what you fetch. Historical odds are far more expensive to buy later than to keep now. Append every payload to your own store from day one.
  • Never trust name matching implicitly. Log unmatched entities rather than silently dropping them. Most odds pipeline bugs are join failures presenting as missing data.
  • Treat a stale timestamp as a missing price. A price with a 40-minute-old capture time is not a price; it is a memory. Filter on freshness explicitly.
  • Handle same-corporate-group pairs. Ladbrokes and Neds are the same book. If you compute arbs yourself, suppress those pairs or you will publish opportunities that get voided.
  • Back off properly on 429. Respect Retry-After, use exponential backoff with jitter, and never retry a 4xx other than 429.

Choosing a provider

A short evaluation checklist that will save you a rewrite:

  1. Bookmaker coverage in your market. Global providers frequently carry three Australian books. Count them before you sign.
  2. Update cadence, measured not claimed. Poll a live market for an hour and diff timestamps.
  3. Historical depth. Can you backtest, and how far back?
  4. Entity resolution. Are teams and runners given stable IDs, or are you joining on strings?
  5. Data rights. Internal use, display, or redistribution?
  6. Provenance labelling. Do you know whether a given row was licensed or scraped?
  7. Failure behaviour. Does an upstream outage return an error, or an empty array that looks like "no games today"?

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.

Where to start

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.

Build on 140+ Australian bookmakers

Pre-match, live, racing, historical archive and derived opportunity endpoints — all provenance-labelled. Free tier, no card required.

Get an API key →
David Koval
About the author
David Koval
Senior Advantage Betting Contributor

David contributes long-form retrospectives, case studies, and operational pieces on AU advantage betting 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.