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

PuntersEdge › UK & Irish racing › UK Horse Racing Odds API in Python

18+. Gambling support: in Great Britain, GamCare's National Gambling Helpline 0808 8020 133 and BeGambleAware.org; in Ireland, GamblingCare.ie.

UK & Irish racing · Python

The British and Irish card in Python, bookmaker by bookmaker.

Four short scripts: today's card with every bookmaker's legs, decimals shown as UK fractions only where the ladder allows, yesterday's closing prices as CSV, and the price path of one race. Plain requests against a REST API; the official client is optional.

Set PUNTERSEDGE_API_KEY in your environment. Every call below also works with pip install puntersedge; the examples stay on requests so each parameter is visible.

1. Today's British and Irish card

import os, requests

API = "https://api.puntersedge.online/v1"
H = {"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]}

races = requests.get(f"{API}/racing/next-to-go",
                     params={"country": "GB,IE", "category": "horse", "num_races": 200},
                     headers=H, timeout=20).json()

for race in races:
    books = sorted({leg["key"] for r in race["runners"] for leg in r["bookmakers"]})
    print(race["venue"], race["race_number"], race["start_time"], race.get("track_condition"), books)

One call returns every open race with every bookmaker's legs; 35 races at the moment this page was rendered.

2. Decimals to UK fractions, honestly

# The UK fractional ladder, as bookmakers print it. A decimal becomes a fraction only when
# it sits exactly on a rung; anything else stays a decimal rather than becoming a fraction
# the book never quoted (an Australian 3.17 stays 3.17).
LADDER = [(1, 1), (21, 20), (11, 10), (6, 5), (5, 4), (13, 10), (11, 8), (7, 5), (6, 4), (8, 5),
          (13, 8), (17, 10), (7, 4), (9, 5), (15, 8), (19, 10), (2, 1), (21, 10), (85, 40), (11, 5),
          (9, 4), (23, 10), (12, 5), (5, 2), (13, 5), (11, 4), (14, 5), (3, 1), (16, 5), (10, 3),
          (17, 5), (7, 2), (18, 5), (4, 1), (9, 2), (5, 1), (11, 2), (6, 1), (13, 2), (7, 1),
          (15, 2), (8, 1), (17, 2), (9, 1), (10, 1), (11, 1), (12, 1), (14, 1), (16, 1), (20, 1),
          (25, 1), (33, 1), (40, 1), (50, 1), (66, 1), (100, 1),
          (20, 21), (10, 11), (5, 6), (4, 5), (8, 11), (4, 6), (8, 13), (4, 7), (8, 15), (1, 2),
          (4, 9), (2, 5), (4, 11), (1, 3), (2, 7), (1, 4), (2, 9), (1, 5), (1, 6), (1, 8), (1, 10)]

def fraction(decimal, tol=0.006):
    best = min(LADDER, key=lambda r: abs(1 + r[0] / r[1] - decimal))
    if abs(1 + best[0] / best[1] - decimal) > tol:
        return None
    return "evens" if best[0] == best[1] else f"{best[0]}/{best[1]}"

race = races[0]
for runner in race["runners"]:
    cells = [f'{leg["key"]} {fraction(leg["win_price"]) or leg["win_price"]}' for leg in runner["bookmakers"]]
    print(runner["number"], runner["name"], " | ".join(cells))

The feed is decimal. The ladder is the UK one; a price off the ladder stays a decimal.

3. Yesterday's closing prices as CSV

import csv, io

r = requests.get(f"{API}/racing/closing-lines",
                 params={"country": "GB,IE", "category": "horse", "date": "2026-10-05",
                         "closing_only": "true", "format": "csv", "limit": 5000},
                 headers=H, timeout=60)
rows = list(csv.DictReader(io.StringIO(r.text)))
print(len(rows), "runner-bookmaker closing lines on 2026-10-05")
for row in rows[:5]:
    print(row["venue"], row["race_number"], row["runner_name"], row["bookmaker_key"],
          row["open_win_price"], "->", row["close_win_price"], "at", row["close_secs_to_jump"], "s")

closing_only=true keeps the rows whose close was read inside the closing window; page with offset.

4. The price path of one race

path = requests.get(f"{API}/racing/price-history",
                    params={"race_id": races[0]["race_id"],
                            "bookmakers": "ladbrokes_uk,williamhill,betway_uk"},
                    headers=H, timeout=20).json()
print({k: (len(v) if isinstance(v, list) else v) for k, v in path.items()})

Every captured point per bookmaker from the first quote to the off, for the race in hand.

Reading the response

Per race

  • race_id, venue, race_number (the meeting's running order), start_time in UTC.
  • race_name, distance_m, track_condition (the going), places_paid.
  • runners[] with number, name, jockey, trainer, weight, form, barrier (the draw).
  • scratchings[]: non-runners, removed from runners.

Per bookmaker leg

  • key: the bookmaker, e.g. ladbrokes_uk, williamhill, betway_uk, tabtouch.
  • win_price, place_price (each-way derived for the UK books).
  • last_update, age_seconds, stale: when it was read and whether it is fresh by that book's own cadence.

Credits: a next-to-go call costs the same whatever it returns, so one call for the whole card is one call. The endpoint reference lists every parameter and cost; the freshness page explains age_seconds, stale and refresh_tier.

Questions

Do I need the Python client?

No. The API is plain REST, and the examples on this page use requests so every parameter is visible. The official client, pip install puntersedge, wraps the same endpoints and reads PUNTERSEDGE_API_KEY from the environment.

How many credits does one card call cost?

A next-to-go call costs the same whatever it returns, so one call for the whole British and Irish card is one call's credits. The plan table on the overview page carries the allowances.

How do I convert the decimals to fractions?

Only where the decimal sits exactly on the UK ladder: 4.00 is 3/1, 1.91 is 10/11. The function on this page does that and leaves anything else as a decimal rather than inventing a fraction the book never quoted.

Run it against today's card.

A free key reaches the live British and Irish card; the closing-price archive and price paths open on a paid plan.

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