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.