Horse Racing Odds API: Race Cards, Runners & Each-Way Prices
Horse racing and greyhound odds as JSON: race cards with runners, jockeys, trainers and each-way terms over REST, plus racing streams. Python code included.
PulseScore serves horse racing and greyhounds, and harness racing on the books that run it, as race cards: one JSON document per race with its runners, their prices and the race's each-way terms. You can poll the cards over REST from every bookmaker that publishes racing, or subscribe to racing streams that send each card once and then only the runners that changed.
Which bookmakers carry racing?
Racing comes from the books that sell it, so coverage follows each bookmaker's own card:
- UK and Irish books: Bet365, Paddy Power, Betfred, Ladbrokes, William Hill, Sky Bet, Betfair Sportsbook, Betway, Star Sports, Unibet UK and BetMGM (CO.UK), with horses and greyhounds.
- Australian books: TAB, Sportsbet AU, Ladbrokes (AU), BetDeluxe (AU), BetRight and Unibet AU, with thoroughbreds and greyhounds.
- Continental Europe: PMU, France's pari-mutuel operator, with the French tote card, and Sisal and Eplay24 (IT) in Italy.
Harness racing comes from TAB, Sportsbet AU, Ladbrokes (AU), BetDeluxe (AU), BetRight, Unibet UK and Sisal, and 10Bet (CO.UK) lists trotting. The bookmaker pages go deeper on the books where racing is the main product: TAB's racing API adds Place and Tote Win prices, Sportsbet AU and BetDeluxe (AU) publish a place price per runner, Unibet UK carries cards from many countries, and PMU is the French tote card itself.
The live list is racingSports per bookmaker in GET https://api.pulsescore.net/api/stream/capabilities (no key needed, refreshed every 10 minutes), and the sport availability page shows whether each feed is up right now.
The REST routes
Every racing bookmaker serves its cards under its own prefix, with your key in the X-Secret header:
- Bet365:
GET /api/v3/bet365/sports/horse-racing/racesandGET /api/v3/bet365/greyhounds/racesreturn the whole upcoming card in one response. - Every other racing book:
GET /api/{bookmaker}/horse-racing/racesandGET /api/{bookmaker}/greyhounds/races, for example/api/betfair-sb/horse-racing/races. Several of them page the list with the samepageandlimitparameters as the sports boards (TAB, Sportsbet AU and PMU do), and many also serve one race at/horse-racing/races/:id. - Harness racing follows the same pattern under its own code where a book has it: on TAB that is
/api/tab/harness/races.
Each bookmaker's Swagger page lists its exact racing routes and response schema: https://api.pulsescore.net/api/{bookmaker}/doc, and https://api.pulsescore.net/v3/bet365/doc for Bet365.
What a race looks like
{
"total": 105,
"races": [
{
"id": "HorseRace::Australia & New Zealand::Mornington::20260904080000::9",
"name": "Mornington",
"region": "Australia & New Zealand",
"startDate": "2026-09-04T08:00:00Z",
"EW": "EW: 1/5 odds 3 places",
"active": "true",
"isInPlay": "unknown",
"settled": false,
"horses": [
{
"id": "2047569135",
"number": "10",
"name": "Silver Bullet",
"jockey": "Joe Bowditch",
"trainer": "",
"active": true,
"EW": { "status": "active", "fractional": "7/2", "decimal": "4.5000" }
}
]
}
]
}A few things to know before you parse it:
- Runners sit under
horsesorgreyhounds, in the bookmaker's own spelling. Several books file greyhound races underhorses, so read whichever list is present. - The race's
EWis the each-way terms as text; a runner'sEWis its win price, withdecimalas a string andfractionalwhere the book quotes one (it can be empty). - Some race fields are strings (
active,isInPlay) while the runner'sactiveis a boolean. Greyhounds have no jockey, and some books leave out the runner number. - Prices fill in as the off time approaches, so an early card can list runners without a price yet.
- Some books add prices per runner in a
marketslist: Sportsbet AU and BetDeluxe (AU) publish aPlaceprice, and TAB also hasTote Win.
Each-way terms and the place price
An each-way bet is two bets of the same stake: one to win at the full price, and one to place at a fraction of the price, which pays if the runner finishes within the places. EW: 1/5 odds 3 places means the place half pays one fifth of the win odds for a top-three finish.
In decimal odds the place price is 1 + (win - 1) × fraction. Silver Bullet at 7/2 (4.5) with 1/5 terms places at 1 + 3.5 × 0.2 = 1.70, so 10 each way (20 staked) returns 45 + 17 = 62 if it wins, 17 if it finishes second or third, and nothing otherwise. Terms differ by bookmaker and by race, so always read them from the race rather than assuming 1/4 or 1/5.
import re
import requests
TERMS = re.compile(r"(\d+)/(\d+)\s+odds\s+(\d+)\s+places?", re.IGNORECASE)
def parse_terms(text):
"""'EW: 1/5 odds 3 places' -> (0.2, 3); None when the race has no each-way terms."""
m = TERMS.search(text or "")
return (int(m[1]) / int(m[2]), int(m[3])) if m else None
def win_decimal(price):
"""A runner's EW price object -> decimal odds, or None while it is unpriced."""
if price.get("decimal"):
return float(price["decimal"])
if price.get("fractional"):
num, den = price["fractional"].split("/")
return 1 + int(num) / int(den)
return None
def each_way_return(stake, win, fraction, places, finish):
"""Total returned on `stake` each way (2 x stake staked) for a finishing position."""
place = 1 + (win - 1) * fraction
return round((stake * win if finish == 1 else 0) + (stake * place if finish <= places else 0), 2)
print(each_way_return(10, 4.5, 0.2, 3, finish=1)) # 62.0
card = requests.get(
"https://api.pulsescore.net/api/v3/bet365/sports/horse-racing/races",
headers={"X-Secret": "YOUR_API_KEY", "Accept-Encoding": "gzip"},
timeout=15,
).json()
for race in card["races"]:
terms = parse_terms(race.get("EW"))
print(race["startDate"], race["name"], race.get("EW") or "no each-way terms")
for runner in race.get("horses") or race.get("greyhounds") or []:
win = win_decimal(runner.get("EW") or {})
if not runner.get("active") or win is None:
continue
line = f' {runner.get("number", ""):>3} {runner["name"]:<24} win {win:6.2f}'
if terms:
fraction, places = terms
line += f" place {1 + (win - 1) * fraction:5.2f} (top {places})"
print(line)The same code reads a greyhound card from /greyhounds/races and, with the URL changed, any other racing book. The REST vs WebSocket vs delta streams guide covers how often you can poll on your plan.
Racing over the Stream API
Racing streams share the socket with sports streams, but they are opt-in: kinds defaults to live,prematch, so add racing:
wss://api.pulsescore.net/api/stream/ws?key=YOUR_API_KEY&bookmakers=tab,bet365&sports=horse-racing,greyhounds&kinds=racingA race is not an event: it has no markets. Each stream sends the race documents exactly as REST serves them, then patches them in the order set, unset, rDel, rSet. rDel removes runners by key; rSet carries whole runner objects with their key in k, so a runner whose price moved arrives complete:
{ "t": "delta", "stream": "tab/racing/horse-racing", "seq": 7684229893122425111, "changes": [
{ "eventId": "SANDOWN_R9", "op": "patch",
"set": { "isInPlay": "true" },
"rSet": [ { "k": "1", "id": "1", "number": "1", "name": "INSINUATING", "active": true } ],
"rDel": [ "7" ] } ] }A runner's key is its id when present, else number|trap|name. When several runners of one race share an id, each is keyed id|number|trap|name, and runners nothing tells apart get ##2, ##3 in document order. The reference clients' RaceBoard applies all of this, and their StreamConnection picks it for every racing stream on its own; the Stream API documentation has both clients and the full rules. The prematch filters (league, from, to, markets) do not apply to racing.
Racing codes count toward your plan's distinct-sports limit like any other sport. On PRO (1 sport) you can stream horse racing from every racing bookmaker on one connection; horse racing plus greyhounds is two sports and takes MAX (3 sports) or ULTRA (all sports).
Frequently asked questions
Which bookmakers have horse racing odds in the PulseScore API?
Racing comes from the books that publish it: UK and Irish books such as Bet365, Paddy Power, Betfred, Ladbrokes and Sky Bet, Australian books such as TAB, Sportsbet AU and Ladbrokes (AU), PMU in France and Sisal in Italy, among others. The live list is racingSports per bookmaker in GET /api/stream/capabilities.
Are greyhound and harness races included?
Greyhounds come from most racing books. Harness racing comes from TAB, Sportsbet AU, Ladbrokes (AU), BetDeluxe (AU), BetRight, Unibet UK and Sisal, and 10Bet (CO.UK) lists trotting.
Does the API give place odds?
Every race carries the bookmaker's each-way terms, such as EW: 1/5 odds 3 places, so the place price is 1 + (win - 1) x fraction in decimal odds. Some books also publish a separate Place price per runner, and TAB adds a Tote Win price.
Why do some runners have no price?
Prices fill in as the off time approaches, so an early card can list runners the bookmaker has not priced yet. Skip runners whose EW price has neither a decimal nor a fractional value, and runners that are not active.
Can I stream race prices instead of polling?
Yes. Add kinds=racing to a Stream API connection on PRO, MAX or ULTRA. Each race is sent once, then only the runners that changed, and racing codes count toward your plan's distinct-sports limit.
Keep reading
Live Odds: REST Polling vs WebSocket vs Delta Streams
REST polling, the live WebSocket or the delta Stream API? How PulseScore's three ways to get live odds compare on limits, bandwidth, code and plans.
Implied Probability, Overround and No-Vig Odds Explained
Convert odds to implied probability, measure a bookmaker's overround and margin, remove the vig to get fair odds, and run the math in Python on live API data.