Testing whether money moving means anything
This endpoint plus /v1/market/movers is the whole experiment. One gives you the movement per race, the other the population answer across the archive.
Documentation / Races & markets
/v1/races/{race_id}/market
Book percentage, every price, and what moved before the off.
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/market \ -H "X-API-Key: $AHR_KEY"
$ch = curl_init('https://api.apihorseracing.com/v1/races/rc_1P005G7/market'); 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/market', { 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/market", headers={"X-API-Key": os.environ["AHR_KEY"]}, ).json()
curl https://api.apihorseracing.com/v1/races/rc_1P005G7/market?key=$AHR_KEY
$url = 'https://api.apihorseracing.com/v1/races/rc_1P005G7/market' . '?key=$AHR_KEY'; $data = json_decode(file_get_contents($url), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/races/rc_1P005G7/market?key=$AHR_KEY' ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/races/rc_1P005G7/market?key=$AHR_KEY", ).json()
{ "meta": { "request_id": "fcbeef222e9d42df", "data_as_of": "2026-09-12T05:10:27+00:00", "plan": "analyst", "window": "2017 → upcoming", "count": 16 }, "data": { "race_id": "rc_1P005G7", "book_percentage": 127.62, "runners": [ { "horse_id": "hs_V8JGH6", "horse": "The Galloping Bear", "sp": "10/3", "sp_decimal": 4.3333, "implied": 0.2308, "price_path": { "opening": "6/1", "opening_decimal": 7, "touched": null, "sp": "10/3", "sp_decimal": 4.3333, "drift": -0.381, "source": "movements", "direction": "shortened" }, "favourite": false, "market_position": 2 }, { "horse_id": "hs_SJX1WR", "horse": "Sametegal", "sp": "2/5", "sp_decimal": 1.4, "implied": 0.7143, "price_path": { "opening": "4/11", "opening_decimal": 1.3636, "touched": [ "1/3" ], "sp": "2/5", "sp_decimal": 1.4, "drift": 0.0267, "source": "movements", "direction": "drifted" }, "favourite": true, "market_position": 1 }, "… 14 more" ] } }
A real response from /v1/races/rc_1P005G7/market,
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. |
| 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. |
| runners | integer | Runners who took part. |
| 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. |
| implied | number | One over the decimal price. Sums to the book percentage across a field, not to one. |
| price_path | object | What the price did before the off. See its own fields below. |
| opening | string | The price the runner opened at, where published. |
| touched | array | Prices touched between opening and starting price. |
| drift | number | Starting price against opening, as a ratio. Positive drifted, negative shortened. |
| source | string | full_history where a complete price series exists, movements where only an opening price does, starting_price_only where neither. |
| direction | string | drifted, shortened or unchanged. |
| favourite | boolean | Shortest price in the race. Joint favourites are all flagged. |
| market_position | integer | Rank by price, one being shortest. Ties share a rank. |
The source publishes no odds history on a settled race, only an opening price and any prices touched. About one runner in five carries one. price_path.source tells you which kind you have on every runner, so you never have to infer it.
| 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
Every price in a race, what the book added up to, and which runners moved before the off. Two things here do not exist in a results feed.
Book percentage is the field's implied probabilities summed. A hundred would be a fair book; the excess is the margin taken out. Ours average about 117 in fields of two to seven and 133 in sixteen or more, and that spread is the reason backing every runner blind loses more than you would expect from the headline figure.
Price movement is the other. The source publishes no odds history on a settled race, only an opening price and any prices touched, so price_path carries a source field naming exactly which kind you have on every runner rather than leaving you to infer it from a null.
Both favourite and market_position are derived here rather than taken from the source, which leaves both empty on a settled race. Runners are ranked on decimal price, shortest first, and joint favourites share rank one.
This endpoint plus /v1/market/movers is the whole experiment. One gives you the movement per race, the other the population answer across the archive.
implied on each runner sums to the book percentage rather than to one. If you want probabilities that sum to one, use the expected field on the statistics endpoints, which is normalised for exactly this reason.
A book percentage well above the average for its field size is a race where the layers wanted more margin than usual.