Settling your own records
Positions one to four are returned separately rather than as a placed count, so you apply your own place terms instead of ours.
Documentation / Races & markets
/v1/races/{race_id}/result
Finishing order, casualties, dividends and prize money.
Send your key in the X-API-Key header, or as
?key= if your tool cannot set one. A date outside your plan's window returns
403 outside_window naming the window, never an empty array.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| race_id | path | string | yes | Opaque race identifier. |
curl https://api.apihorseracing.com/v1/races/rc_1P005G7/result \ -H "X-API-Key: $AHR_KEY"
$ch = curl_init('https://api.apihorseracing.com/v1/races/rc_1P005G7/result'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('AHR_KEY')], ]); $data = json_decode(curl_exec($ch), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/races/rc_1P005G7/result', { headers: { 'X-API-Key': process.env.AHR_KEY } } ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/races/rc_1P005G7/result", headers={"X-API-Key": os.environ["AHR_KEY"]}, ).json()
curl https://api.apihorseracing.com/v1/races/rc_1P005G7/result?key=$AHR_KEY
$url = 'https://api.apihorseracing.com/v1/races/rc_1P005G7/result' . '?key=$AHR_KEY'; $data = json_decode(file_get_contents($url), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/races/rc_1P005G7/result?key=$AHR_KEY' ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/races/rc_1P005G7/result?key=$AHR_KEY", ).json()
{ "meta": { "request_id": "87908b1270b3d626", "data_as_of": "2026-09-12T05:10:27+00:00", "plan": "analyst", "window": "2017 → upcoming", "count": 5 }, "data": { "race_id": "rc_1P005G7", "course": "Fontwell", "date": "2021-02-28", "off_time": "16:40:58", "winning_time": "7m 10.95s", "book_percentage": 127.62, "placed": 3, "dividends": { "currency": "GBP", "win": 4.7, "place": [ 1.4, 1.1, "… 1 more" ], "exacta": 6.9, "trifecta": 86.9, "straight_forecast": 5.4, "swingers": [ 1.2, 11.2, "… 1 more" ], "place_pot": { "pot": 5.7, "pool": 80542.57, "win_stakes": 10142.17 }, "quad_pot": { "pot": 1.8, "pool": 6901.81, "win_stakes": 2706.56 } }, "prizes": [ { "position": 1, "prize": 1949 }, { "position": 2, "prize": 572 }, "… 2 more" ], "result": [ { "position": 1, "cloth": 13, "horse_id": "hs_V8JGH6", "horse": "The Galloping Bear", "sp": "10/3", "sp_decimal": 4.3333, "beaten_distance": null, "casualty": null, "jockey": "Millie Wonnacott", "trainer": "Ben Clarke" }, { "position": 2, "cloth": 4, "horse_id": "hs_SJX1WR", "horse": "Sametegal", "sp": "2/5", "sp_decimal": 1.4, "beaten_distance": null, "casualty": null, "jockey": "H Cobden", "trainer": "P F Nicholls" }, "… 3 more" ], "unplaced": [ { "position": null, "cloth": 1, "horse_id": "hs_3QX1NCY", "horse": "Djin Conti", "sp": "16/1", "sp_decimal": 17, "beaten_distance": null, "casualty": "PulledUp", "jockey": "Tom Cannon", "trainer": "S C Robinson" }, { "position": null, "cloth": 2, "horse_id": "hs_YBW9N7", "horse": "Le Reve", "sp": "50/1", "sp_decimal": 51, "beaten_distance": null, "casualty": "PulledUp", "jockey": "Bryony Frost", "trainer": "J P Owen" }, "… 9 more" ] } }
A real response from /v1/races/rc_1P005G7/result,
captured by calling the live API rather than written by hand.
Each field is described once and rendered on every endpoint that returns it, so the wording cannot drift between pages.
| Field | Type | What it is |
|---|---|---|
| race_id | string | Opaque race identifier. Stable, and safe to store. |
| course | string | Course name. |
| date | date | Race date, YYYY-MM-DD. |
| off_time | string | Actual off time where recorded. |
| winning_time | string | Winning time as published. |
| book_percentage | number | The field's implied probabilities added up. 100 would be a fair book; the excess is the margin. Ours average 117 in small fields and 133 in sixteen or more. |
| placed | integer | How many places the race paid. |
| dividends | object | Tote returns to a one unit stake. |
| prizes | array | Prize money by finishing position. |
| position | integer | Finishing position. Null means the runner did not complete or was not published below the placings. |
| cloth | integer | Cloth number. |
| horse_id | string | Opaque horse identifier. |
| horse | string | Horse name. |
| sp | string | Starting price, fractional, as returned. |
| sp_decimal | number | Starting price as a decimal, computed once at load so you are not parsing fractions per row. |
| beaten_distance | string | Distance behind the horse in front. |
| casualty | string | Why a runner did not complete: PulledUp, Fell, UnseatedRider, BroughtDown, RefusedToRace, RanOut. |
| jockey | string | Rider name. |
| trainer | string | Trainer name. |
Where a time exists it is as published. /v1/courses/{id}/standard-times reports the median per distance and counts the implausible ones rather than quietly serving them.
An absent dividend is a fact about the race rather than a gap in the data, and the endpoint says so instead of returning an error.
America publishes the placings and nothing below them, so position is null for most of the field. Britain, Ireland, France and South Africa carry a position for the great majority of runners. /v1/countries returns the current figure for each.
They are published on the horse profile rather than the result page, so they fill in behind the main crawl.
| Code | Status | When |
|---|---|---|
| unauthorized | 401 | No key, or one we do not recognise. Check the header name and that the key has not been rotated from your account page. |
| outside_window | 403 | You asked for a date your plan cannot see. The body names the window and the date you asked for. Search endpoints clamp instead of refusing, and report what they actually searched in meta. |
| not_found | 404 | The identifier is well formed but nothing has it. Distinct from invalid_id, which means the identifier itself is wrong. |
| invalid_id | 422 | The prefix does not match the kind of thing the path expects, or the identifier is malformed. A horse id passed where a race is wanted lands here rather than returning the wrong race. |
| rate_limited | 429 | Past your per-minute rate. Retry-After is set, and X-RateLimit-Remaining tells you where you stand on every successful response too. |
| server_error | 500 | Ours, not yours. Every response carries a request_id; quote it and we can find the exact request. |
A real request from your browser straight to
api.apihorseracing.com with
your own key. Nothing is proxied and nothing is logged by this page.
Remembered in this browser so you do not paste it on every page. Forget it
The finishing order, split into those who completed and those who did not, with the winning time, the book percentage, tote dividends and prize money.
The split is deliberate. Lumping a horse that pulled up in with a horse that finished eleventh, both carrying a null position, is how you end up counting casualties as runners. Here the unplaced array carries a casualty reason where one exists, so you always know which you are looking at.
Six casualty reasons are published: PulledUp, Fell, UnseatedRider, BroughtDown, RefusedToRace and RanOut. In jumps racing these are a real signal rather than an edge case; Britain records one on about five percent of runners.
Positions one to four are returned separately rather than as a placed count, so you apply your own place terms instead of ours.
The casualty reasons make a jumps analysis possible that a placings-only feed cannot support at all.
The margin to second and the winning time are here; the beaten distance for each runner fills in behind the pedigree pass.
Returns not_found where a race exists but has no result yet. That is a different thing from a missing race and deserves a different response.