PuntersEdge › South African racing › South African Horse Racing Odds API in Python
18+. Gambling support in South Africa: the National Responsible Gambling Programme's toll-free counselling line 0800 006 008, WhatsApp 076 675 0710, responsiblegambling.org.za.
South African racing · Python
Four short scripts: today's card with every bookmaker's legs, the best price on each runner and who is quoting it, the latest 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": "ZA", "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("places_paid"), books)One call returns every open race with every bookmaker's legs; 16 races at the moment this page was rendered.
# The best win price on each runner, and which bookmaker is quoting it. A runner priced by
# one book has nothing to compare, so it is printed without a "best".
race = races[0]
for runner in race["runners"]:
legs = {leg["key"]: leg["win_price"] for leg in runner["bookmakers"] if leg.get("win_price")}
if not legs:
continue
cells = " | ".join(f"{k} {v}" for k, v in sorted(legs.items()))
best = max(legs, key=legs.get) if len(legs) > 1 else None
print(runner["number"], runner["name"], cells, f"-> best {legs[best]} at {best}" if best else "")Decimal throughout. The feed does not pick a best price for you; the comparison is yours to make, so it is three lines of Python.
import csv, io
r = requests.get(f"{API}/racing/closing-lines",
params={"country": "ZA", "category": "horse", "date": "2026-10-15",
"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-15")
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.
# A race's price path begins about an hour before the off and price-history answers 404
# before the first point, so take a race that has already closed: the first row of step 3.
race_id = rows[0]["race_id"]
path = requests.get(f"{API}/racing/price-history",
params={"race_id": race_id,
"bookmakers": "easybet_za,topbet_za,betway_za,hollywoodbets"},
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 60 minutes before the off to the jump, for the race in hand; the archived opening price is the first point of that window.
race_id, venue, race_number (the meeting's running order), start_time in UTC.places_paid, and distance_m or track_condition where a bookmaker on the race publishes them.runners[] with number, name, jockey, trainer, and weight, form, barrier (the draw) where published.scratchings[]: non-runners, removed from runners.key: the bookmaker, e.g. easybet_za, topbet_za, betway_za, pointsbetau.win_price, and place_price from the bookmakers that quote one.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. Every example on this page is plain requests against the REST API. The client on PyPI (pip install puntersedge) wraps the same calls.
A next-to-go call costs the same whatever it returns, so one call for the whole South African card is one call. The endpoint reference lists every cost.
Easybet, Topbet, Betway and Hollywoodbets under the keys easybet_za, topbet_za, betway_za, hollywoodbets, plus the Australian bookmakers that price the race. Filter with bookmakers= to keep only some of them.
The live card is on the free key; the closing-price archive and price paths open on a paid plan.
A free key reaches the live South African card; the closing-price archive and price paths open on a paid plan.
Get a free key