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
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.
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.
# 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.
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.
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.
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.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.
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.
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.
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.
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