RapidOddsAPI bookmaker odds API logoRapidOddsAPI
Home/Blog/Guide

Compare horse racing odds across every Australian bookmaker in Python

Find the best win and place price on every runner, see which bookmaker has the sharpest market, and watch for price changes live.

By Joe, Founder of RapidOddsAPI ·

Punters who always take the best price on a horse are ahead of the ones who don't, and the gap between Australian bookmakers on the same runner is often bigger than people think. This guide uses the RapidOddsAPI horse racing odds API to pull fixed odds from 18 Australian bookmakers in one request, find the best win and place price on every runner, work out each bookmaker's market percentage, and then follow it all live over WebSocket.

You need Python 3 and the requests package, plus an API key on a plan with racing. All the prices for a race come back in one object, with runners keyed by their number, so there is no matching of horse names or track spellings between bookmakers. That is usually the painful part of comparing racing odds, and here it is already done.

Step 1: see what's racing today

The /racing/meetings endpoint lists every track racing today and the races at each one. It has no prices, so it is cheap to call whenever you need the day's card.

import requests API_KEY = "your_api_key" BASE = "https://api.rapidoddsapi.com/racing" def get_meetings(code="thoroughbred"): resp = requests.get(f"{BASE}/meetings", params={"api_key": API_KEY, "code": code}, timeout=30) resp.raise_for_status() return resp.json()["meetings"] for m in get_meetings(): print(m["track"], m["country"], len(m["races"]), "races")

On a Monday morning that printed:

Te Rapa NZ 8 races Wangaratta AU 9 races Queanbeyan AU 7 races Newcastle AU 7 races

Step 2: the best price on every runner

Now get the odds for a meeting with /racing/races and a track name. Each race has a bookmakers list, and each bookmaker has its own runners with win and place prices. Go through every bookmaker and keep the highest price for each runner number. Skip scratched runners, which come through with scratched: true and no price.

def get_races(track, code="thoroughbred"): resp = requests.get(f"{BASE}/races", params={"api_key": API_KEY, "track": track, "code": code}, timeout=30) resp.raise_for_status() return resp.json()["races"] def best_prices(race): best = {} for book in race["bookmakers"]: for r in book["runners"]: if r["scratched"] or r["win"] is None: continue b = best.setdefault(r["number"], {"name": r["name"], "win": 0, "win_book": None, "place": 0, "place_book": None}) if r["win"] > b["win"]: b["win"], b["win_book"] = r["win"], book["bookmaker"] if r["place"] and r["place"] > b["place"]: b["place"], b["place_book"] = r["place"], book["bookmaker"] return best race = get_races("Wangaratta")[0] print(f"{race['track']} R{race['race_number']} ({len(race['bookmakers'])} bookmakers)") for num, b in sorted(best_prices(race).items()): print(f" {num:>2} {b['name']:<18} win {b['win']:>6.2f} @ {b['win_book']:<10} " f"place {b['place']:>5.2f} @ {b['place_book']}")

Here is what came back for race 1 at Wangaratta:

Wangaratta R1 (16 bookmakers) 1 Bon Over win 31.00 @ BetRight place 5.75 @ Sportsbet 2 Hellofa Gamble win 81.00 @ Amused place 15.75 @ Sportsbet 3 Kensington City win 34.00 @ BetRight place 6.00 @ BetRight 5 Fiore Darancio win 26.00 @ Amused place 6.00 @ Sportsbet 6 Kinky Mistress win 3.60 @ Amused place 1.57 @ PointsBet 7 Super Scholar win 3.80 @ Amused place 1.62 @ PointsBet 8 Ubique win 26.00 @ TAB place 5.00 @ TAB 9 Valour Vino win 8.00 @ Amused place 2.65 @ Amused 11 Obsidian Arrow win 10.00 @ Amused place 3.10 @ SwiftBet 14 Snowy Mountains win 3.25 @ Picklebet place 1.52 @ PointsBet 15 Full Of Knowledge win 41.00 @ Amused place 7.75 @ Sportsbet

Numbers 4, 10, 12 and 13 are missing because they were scratched. Notice that the best win price and the best place price on the same horse are often with different bookmakers. That's worth knowing if you back each way, since you can split the two bets.

Step 3: which bookmaker has the best market?

A bookmaker's market percentage is the sum of 100 divided by each runner's win price. A fair market adds up to 100%, and anything over that is the bookmaker's margin. The lower the number, the better that bookmaker is pricing the race overall.

def market_percentages(race): out = {} for book in race["bookmakers"]: prices = [r["win"] for r in book["runners"] if not r["scratched"] and r["win"]] if prices: out[book["bookmaker"]] = round(sum(100 / p for p in prices), 1) return out for name, pct in sorted(market_percentages(race).items(), key=lambda x: x[1]): print(f" {name:<12} {pct}%") best = best_prices(race) print(f" best of all books: {round(sum(100 / b['win'] for b in best.values()), 1)}%")

For the same race, the five tightest markets were:

PointsBet 127.0% TAB 127.0% Amused 127.4% Picklebet 127.4% Bet365 127.8% ... best of all books: 124.9%

The last line is the interesting one. Every single bookmaker was at 127% or more, but taking the best price on each runner from wherever it was gets you down to 124.9%. That's the margin you save just by shopping around, on every race. If that best-of-all-books number ever drops under 100%, backing every runner at its best price would lock in a profit. It is rare, and it doesn't last long when it happens.

Market percentage only means something when every runner is priced, so be careful with races a long way out. Smaller bookmakers often post their prices later than the big ones.

Step 4: watch prices move live

Polling every few seconds works, but racing prices move most in the last few minutes before the jump. The racing WebSocket pushes each change to you as it happens. The first message after you subscribe has every upcoming race, and from then on each race_update is one bookmaker's current runners for one race. Keep the latest copy per bookmaker and work out the best price again whenever one arrives.

# pip install websockets import asyncio import json import websockets URL = f"wss://api.rapidoddsapi.com/racing/ws?api_key={API_KEY}" races = {} # race_id -> {bookmaker: runners} best = {} # (race_id, number) -> best win price def check(race_id, track, race_number): top = {} for book, runners in races[race_id].items(): for r in runners: if r["scratched"] or not r["win"]: continue if r["win"] > top.get(r["number"], (0,))[0]: top[r["number"]] = (r["win"], book, r["name"]) for number, (win, book, name) in top.items(): old = best.get((race_id, number)) if old is not None and win != old: print(f"{track} R{race_number} #{number} {name}: best win {old} -> {win} @ {book}") best[(race_id, number)] = win async def main(): async with websockets.connect(URL, max_size=None) as ws: await ws.recv() # "connected" await ws.send(json.dumps({"action": "subscribe", "tracks": "*", "countries": ["AU"]})) async for raw in ws: msg = json.loads(raw) if msg["event"] == "subscribed": for race in msg["races"]: races[race["race_id"]] = {b["bookmaker"]: b["runners"] for b in race["bookmakers"]} check(race["race_id"], race["track"], race["race_number"]) print(f"watching {len(msg['races'])} races") elif msg["event"] == "race_update": races.setdefault(msg["race_id"], {})[msg["bookmaker"]] = msg["runners"] check(msg["race_id"], msg["track"], msg["race_number"]) asyncio.run(main())

Leave it running and every time the best available price on a horse changes, you see it, along with the bookmaker that now has it. One subscription to "*" covers every Australian track, today and every day after, so there is nothing to re-subscribe each morning.

A minute of it on a Monday morning:

watching 23 races Newcastle R5 #10 Sacred Well: best win 12.0 -> 11.0 @ Amused Wangaratta R9 #5 Mythic: best win 34.0 -> 35.0 @ Ladbrokes Wangaratta R9 #2 Coco De Luxe: best win 3.8 -> 3.9 @ Sportsbet Wangaratta R9 #15 Shyleah: best win 34.0 -> 41.0 @ TAB Wangaratta R9 #13 I Am Algadon: best win 5.0 -> 5.5 @ Ladbrokes Wangaratta R7 #8 Lord Domino: best win 16.0 -> 13.0 @ Amused

Harness and greyhounds work the same way

Add code="harness" or code="greyhound" to get_meetings and get_races, or "code": "greyhound" to the subscribe message, and all of the code above works unchanged. For the dogs, the runner number is the box number. See the harness racing odds API and greyhound odds API pages for coverage.

Taking it further

  • Only the books you have accounts with. Pass bookmakers=TAB,Sportsbet,Bet365 to /racing/races (or "bookmakers" in the subscribe message) and the best price is the best one you can actually take.
  • Click straight through. Each bookmaker block has a url to that race on the bookmaker's site, so the best price can be a link.
  • Alert on big moves. Compare each new price with the first one you saw for the race and flag runners that have firmed or drifted a lot.
  • Build a screen. The same data drops straight into a grid like the one in our odds comparison screen guide, with runners as rows and bookmakers as columns.

Next steps

You now have best prices and market percentages for every Australian race, live. For the full list of fields and parameters, read the racing API docs. Racing is included in Pro and Elite, or on its own with the $99/mo Racing plan, with no credits. See the coverage page for every bookmaker and code.

Start building with RapidOddsAPI

Real-time, standardised odds from 100+ bookmakers over REST and WebSocket. Start free with 250 credits, no credit card required.