18+ Only  |  Gambling can be addictive — please gamble responsibly  |  Gambling Help: 1800 858 858  |  GambleAware

Build an Australian racing odds comparison table in Python

An odds comparison table puts every runner down the side and every bookmaker across the top, so you can see at a glance who is paying the most on each runner. This guide builds one for the next Australian races in about 60 lines of Python, with one request to the PuntersEdge API. No scraping and no connector per bookmaker.

The code below was run against the live API on 14 October 2026, and the output further down is what it printed.

18+ only. This is market-data tooling, not betting advice. Prices move constantly; check the bookmaker before acting. Gambling Help: 1800 858 858.

1. Get a free key

  1. Enter your email on the signup form. The free plan is 3,000 credits a month and needs no card.
  2. Click the link in the verification email. A new key does not authenticate until you do.
  3. Put the key in an environment variable rather than in your code:
pip install requests
export PUNTERSEDGE_API_KEY=paste_your_key_here

2. The code

It makes one call to GET /v1/racing/next-to-go, keeps the Australian races, works out the best price per runner and prints the grid.

import os
import sys

import requests

API = "https://api.puntersedge.online/v1"
KEY = os.environ.get("PUNTERSEDGE_API_KEY")
if not KEY:
    sys.exit("Set PUNTERSEDGE_API_KEY first: https://puntersedge.online/api#signup")


def next_races(count=3, category="horse"):
    r = requests.get(
        f"{API}/racing/next-to-go",
        params={"country": "AU", "include_unresolved": "true",
                "categories": category, "num_races": 20},
        headers={"X-API-Key": KEY},
        timeout=20,
    )
    if r.status_code != 200:
        sys.exit(f"HTTP {r.status_code}: {r.json().get('detail', r.text)}")
    races = [race for race in r.json() if race.get("country") in ("AU", None)]
    return races[:count]


def best_price(runner):
    fresh = [b for b in runner["bookmakers"] if b.get("win_price") and not b.get("stale")]
    if not fresh:
        return None, set()
    top = max(b["win_price"] for b in fresh)
    return top, {b["key"] for b in fresh if b["win_price"] == top}


def print_table(race):
    counts = {}
    for runner in race["runners"]:
        for b in runner["bookmakers"]:
            if b.get("win_price"):
                counts[b["key"]] = counts.get(b["key"], 0) + 1
    books = sorted(counts, key=lambda k: -counts[k])

    print(f"\n{race['venue']} R{race['race_number']}  ({race['start_time']} UTC)")
    print(f"{'#':>3} {'runner':<20}" + "".join(f"{k[:6]:>8}" for k in books) + f"{'best':>8}")
    for runner in race["runners"]:
        top, winners = best_price(runner)
        legs = {b["key"]: b for b in runner["bookmakers"] if b.get("win_price")}
        cells = ""
        for k in books:
            b = legs.get(k)
            if b is None:
                cells += f"{'-':>8}"
            elif b.get("stale"):
                cells += f"{'(%.2f)' % b['win_price']:>8}"
            else:
                cells += f"{b['win_price']:>7.2f}" + ("*" if k in winners else " ")
        print(f"{runner['number']:>3} {runner['name'][:20]:<20}{cells}{top or 0:>8.2f}")


for race in next_races():
    print_table(race)

3. What each part does

One request for the whole card. num_races=20 is the most a 2-credit call returns. country=AU on its own drops races: most of an Australian card has no country label until the meeting is confirmed. include_unresolved=true brings those back, and the filter keeps races labelled AU or not yet labelled.

Fresh prices only. Every bookmaker quote carries stale and age_seconds. A stale quote is still shown, in brackets, but it never counts as the best price. Inside three hours of the jump most books re-price on every poll, typically every 8–20 seconds, and more of them arrive; further out they re-price about every 15 minutes. The freshness rules explain the thresholds.

Columns sorted by coverage. The bookmakers that price the most runners come first, so the left of the table is the part of the market most punters can see. Ties for the best price are all marked.

Scratched runners are never in runners. They come back in the race's scratchings list.

4. The output

The first two races of the run, verbatim (14 October 2026, 09:05 AEDT):

Rosehill R1  (2026-10-14T02:30:00Z UTC)
  # runner                   tab    neds  boostb  ladbro  betrig  bet365  betdel  betr_a  unibet  tabtou  palmer  betgol  sports  points    best
  1 Ammunition            41.00   41.00   41.00   41.00   41.00   34.00   41.00   41.00   41.00   41.00   41.00   41.00   51.00*  41.00    51.00
  3 Master Chief          41.00   41.00   41.00   41.00   51.00*  34.00   41.00   41.00   41.00   41.00   41.00   41.00   51.00*  41.00    51.00
  4 Nevada Falls          16.00   16.00   21.00   16.00   13.00   23.00*  13.00   14.00   15.00   21.00   21.00   21.00   17.00   13.00    23.00
  5 Rain Delay             3.80    4.00    3.90    4.00    3.70    3.80    4.00    4.00    4.00    3.90    4.00    3.90    4.40*   4.00     4.40
  7 Yotei                  8.00    8.00    8.00    8.00    8.00    7.00    8.00    8.00    8.00    8.00    8.00    8.00    8.50*   8.00     8.50
  8 Affari                 1.85    1.75    1.80    1.75    1.85    2.30*   1.75    1.80    1.75    1.80    1.75    1.80    1.70    1.80     2.30
 10 Evanta                15.00   15.00   15.00   15.00   12.00   15.00   16.00   15.00   15.00   15.00   15.00   15.00   17.00*  16.00    17.00
 11 In Her Name           26.00*  23.00   26.00*  23.00   23.00   26.00*  23.00   23.00   21.00   23.00   21.00   26.00*  23.00   23.00    26.00
 12 Pearl Of Dubai        11.00*  10.00   11.00*  10.00   10.00   11.00*  10.00   11.00*  11.00*  11.00*  11.00*  11.00*  10.00   10.00    11.00
 13 To Dubai We Go        81.00*  81.00*  71.00   81.00*  71.00   71.00   81.00*  67.00   71.00   61.00   71.00   71.00   81.00*  81.00*   81.00

Caulfield R1  (2026-10-14T02:40:00Z UTC)
  # runner                tabtou     tab  betr_a  sports  points  unibet    neds  ladbro  betgol  palmer  betdel  bet365  betrig  boostb    best
  1 Act Up                12.00   13.00*  13.00*  12.00   13.00*  12.00   12.00   12.00   10.00   12.00   12.00  (13.00)  12.00   10.00    13.00
  2 Royal Press            2.30    2.30    2.30    2.30    2.30    2.30    2.50*   2.50*   2.30    2.30    2.30   (2.20)   2.30    2.30     2.50
  3 Stone Skyrider        10.00*  10.00*  10.00*  10.00*  10.00*  10.00*  10.00*  10.00*  10.00*  10.00*  10.00* (11.00)  10.00*  10.00*   10.00
  4 Fabienne               8.00*   8.00*   8.00*   8.00*   8.00*   8.00*   8.00*   8.00*   8.00*   8.00*   8.00*  (8.50)   8.00*   8.00*    8.00
  5 Maalefushi             4.40*   4.40*   4.40*   4.40*   4.40*   4.40*   4.40*   4.40*   4.40*   4.40*   4.40*  (4.60)   4.20    4.40*    4.40
  6 Soldi                  4.00*   4.00*   3.90    4.00*   4.00*   4.00*   4.00*   4.00*   3.80    4.00*   4.00*  (4.00)   3.90    3.80     4.00

* is the best fresh price; (13.00) is a stale quote left out of "best"; - means the book isn't quoting that runner.

5. The finished script: HTML, CSV and a keyless sample

odds_table.py is the same idea as a complete tool. It is a single file with no dependencies beyond the standard library, and it adds:

git clone https://github.com/Propertyscout001/australian-racing-api-python
cd australian-racing-api-python
python3 odds_table.py --category horse --races 5 --html odds.html

6. Credits, plans and showing the table to other people

QuestionAnswer
What does a run cost?2 credits for up to 20 races. The free plan's 3,000 credits cover about 1,500 runs a month. Every response carries X-Credits-Remaining, and GET /v1/usage is free.
Can I run it on the free plan?Yes. /v1/racing/next-to-go is open to every plan.
Polling it all day?Once a minute across the 11-hour Australian card is about 1,320 credits a day, which is Standard territory. The pricing page has a calculator.
Can I put the table on my website?That's the Display licence on Plus, with a visible credit. See below.

From section 8.1 of the terms:

Personal use — Free, Hobby and Standard. You may use the data for yourself: your own betting decisions, your own models, research, backtesting and dashboards you look at.

Display with attribution — Plus. You may show our odds to your own users, including your own paying subscribers, provided every surface that displays them carries a visible Prices by PuntersEdge credit linking to puntersedge.online.

Using the data in a business, for client work or in a product you sell is covered by the other rungs of 8.1. If your use doesn't fit, email hamish@punters-edge.com and you'll get an answer in writing.

7. The same table for sports

For sports, GET /v1/best-odds/{sport} returns each selection's best_price, best_bookmaker and an all_prices list of every book's price, so the same pivot works: selections down the side, bookmakers across the top. Check the event list isn't empty before indexing into it, because seasonal sports return no events between seasons. The coverage map shows which sports carry more than one bookmaker right now.

8. Things that will catch you out

Get a free API key →

API reference for the endpoints in this guide

This site contains wagering-related analysis and is intended for Australian users aged 18+. Gambling involves risk. Please gamble responsibly.