Growing Discord community — direct access to the developer, live coverage & picks. Join the Discord Join now →
Matches Leagues Predictions The Edge Money News Stats Coverage
Docs / More Sports APIs pro

Horse Racing API#

The Horse Racing API covers UK and Ireland racing: tracks, daily meetings, races with runners, multi-bookmaker decimal odds per runner, plus horse, jockey and trainer profiles.

Base URL: https://sports.bzzoiro.com/horseracing/api/v2/

Access#

Requires the Sports Addon ($7/month) — it unlocks the tennis, CS2, darts, hockey, basketball and horse racing APIs plus their MCP servers. Get it at /addons/. You also need a free account token from /register/.

Authorization: Token YOUR_API_KEY

Without a token you get 401:

{"error": "authentication required", "code": "authentication_required",
 "detail": "Send Authorization: Token <YOUR_TOKEN> on every request.",
 "register": "https://sports.bzzoiro.com/register/"}

With a token but no addon you get 402:

{"error": "Sports Addon required", "code": "addon_required",
 "detail": "Tennis, CS:GO, darts, hockey, basketball and horse racing APIs require the Sports Addon ($7/mo, $5 for Founder badge holders).",
 "pricing_url": "https://sports.bzzoiro.com/pricing/"}

List endpoints paginate with limit (default 50, max 200) and offset, and return {count, next, previous, results}.

Endpoints#

Endpoint Description
GET /horseracing/api/v2/tracks/ List race tracks
GET /horseracing/api/v2/tracks/{id}/ Track detail
GET /horseracing/api/v2/meetings/ List meetings (a day of racing at a track)
GET /horseracing/api/v2/meetings/{id}/ Meeting detail with its races
GET /horseracing/api/v2/races/ List races
GET /horseracing/api/v2/races/live/ Races currently in progress
GET /horseracing/api/v2/races/next-to-jump/ Races starting in the next 30 minutes
GET /horseracing/api/v2/races/{id}/ Race detail with runners
GET /horseracing/api/v2/runners/ List runners
GET /horseracing/api/v2/runners/{id}/ Runner detail
GET /horseracing/api/v2/runners/{id}/odds/ All bookmaker odds for a runner
GET /horseracing/api/v2/horses/ Search horses
GET /horseracing/api/v2/horses/{id}/ Horse profile
GET /horseracing/api/v2/jockeys/ Search jockeys
GET /horseracing/api/v2/jockeys/{id}/ Jockey profile
GET /horseracing/api/v2/trainers/ Search trainers
GET /horseracing/api/v2/trainers/{id}/ Trainer profile

Tracks and meetings#

Tracks query parameters: search, country, limit, offset.

Meetings query parameters

Param Type Description
date date One day (YYYY-MM-DD)
date_from / date_to date Date range
country string ISO 3166-1 alpha-2 code, case-insensitive (GB, IE, FR, US). England, Scotland, Wales and Northern Ireland are all GB
track int Filter by track id
limit / offset int Pagination
curl -H "Authorization: Token YOUR_API_KEY" \
  "https://sports.bzzoiro.com/horseracing/api/v2/meetings/?date=2026-08-02&country=GB"
{"count": 5, "results": [
  {"id": 1207, "track": {"id": 31, "name": "Goodwood", "country": "GB"},
   "date": "2026-08-02", "races_count": 7, "going": "Good"}
]}

Races#

Query parameters

Param Type Description
meeting int Filter by meeting id
track int Filter by track id
status string race_stage value, case-insensitive: WEIGHEDIN, DORMANT, OFF, RESULT, FINISHED, ABANDONED, RACEVOID
surface string e.g. turf, all-weather
date_from / date_to date YYYY-MM-DD
limit / offset int Pagination

GET /races/next-to-jump/ is the quickest way to build a "next races" widget — it returns races going off within 30 minutes, soonest first. GET /races/live/ is cached 30 seconds.

GET /races/{id}/ includes the runner list:

{"id": 88412, "meeting_id": 1207, "name": "Sussex Stakes",
 "off_time": "2026-08-02T14:35:00+00:00", "status": "upcoming",
 "distance": "1m", "surface": "turf", "runners_count": 9,
 "runners": [
   {"id": 774201, "cloth_number": 1, "draw_number": 4, "finish_position": null,
    "horse_name": "Field Of Gold", "jockey_name": "K. Stott",
    "trainer_name": "J. & T. Gosden",
    "current_odds_decimal": 2.10, "current_odds_fractional": "11/10",
    "is_favourite": true, "ride_status": "RUNNER"}
 ]}

Race stage is a sample, not an event#

Every race summary carries race_stage and, next to it, stage_observed_at — when we last read the race from the card, not when the stage changed.

The card is swept every ten minutes. DORMANT lasts hours and WEIGHEDIN lasts until the card is retired, so a ten-minute sample lands on one of those almost every time. The stages in between — GOINGDOWN, PARADING, ATTHEPOST, OFF — last a couple of minutes each and a ten-minute sample does not see them: across 13,373 races over ninety days, 2 were caught mid-sequence.

So read it as "the stage as of stage_observed_at", which may be ten minutes old, and do not build a state machine that expects to observe every transition. If you need to know the moment a race goes to post, this field cannot give it to you.

Non-runners#

A withdrawn horse stays in the runners array — it is still an entry in the race, and removing it would silently renumber a card a client has already drawn. What marks it is ride_status, on every runner in the racecard and on GET /runners/{id}/:

ride_status Meaning
RUNNER Will run
NONRUNNER Declared and then withdrawn
WITHDRAWN Withdrawn at or after the off
DOUBTFUL Declared, participation not confirmed
"" Not published for this race

Filter on it rather than on the presence of odds: a non-runner can keep its last traded price until the market is settled.

Non-finishers#

finish_position is null until the result is in, and stays null for any runner that did not finish — non-runners and horses that fell, pulled up, unseated their rider or refused. For those, casualty on GET /runners/{id}/ carries the reason as published with the result (null when the horse finished or nothing was published).

Starting price#

GET /runners/{id}/ carries the starting price (SP): sp_fractional (e.g. "6/4") and sp_decimal (fraction + 1, so "6/4" is 2.5 and evens is 2.0). SP is the final price at the off. It is filled once the race is settled (weighed in or result published) and does not change afterwards.

It is null / "" before the race is settled, for non-runners, and for the few runners with no final price published. Use current_odds_* for the price before the off; use sp_* for anything that measures value after it.

Runners and odds#

Runners query parameters: race, horse, jockey, trainer, limit, offset.

GET /runners/{id}/odds/ returns every bookmaker's current decimal price for that runner:

{"runner_id": 774201, "odds": [
  {"bookmaker": "bet365", "decimal_odds": 2.10, "updated_at": "2026-08-02T13:58:12+00:00"},
  {"bookmaker": "williamhill", "decimal_odds": 2.05, "updated_at": "2026-08-02T13:57:40+00:00"}
]}

Horses, jockeys and trainers#

All three follow the same pattern: a search list (query, limit, offset) plus a detail endpoint with career form.

curl -H "Authorization: Token YOUR_API_KEY" \
  "https://sports.bzzoiro.com/horseracing/api/v2/jockeys/?query=stott"
{"count": 1, "results": [
  {"id": 2210, "name": "K. Stott", "wins_this_season": 61, "win_rate": 0.19}
]}

Coverage and off-season#

Racing runs every day, but cards are published a few days ahead, so the thirty-day count is always much smaller than the weekly one.

GET /api/v2/coverage/?sport=horseracing says so directly, without a token:

{"sport": "horseracing", "status": "in_season", "events_next_7d": 230,
 "events_next_30d": 258, "priced_next_7d": 230, "live_now": 0,
 "next_event_at": "2026-08-19T17:48:00+00:00", "last_event_at": "2026-08-19T17:45:00+00:00"}

status is in_season (fixtures within seven days), between_events (nothing this week, something later), off_season (nothing ahead and the last event is more than a fortnight old) or no_fixtures. Drop the ?sport= to get every sport in one call — worth doing before a polling loop decides what to ask for.

View this page as Markdown · Found a mistake? Tell us