Testing the folklore
direction=shortened with a min_move, over as much of the archive as your plan reaches. Read the interval.
Documentation / Market & analysis
/v1/market/movers
Whether money moving tells you anything.
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 |
|---|---|---|---|---|
| direction | query | string | no | shortened or drifted. Omit for both. |
| min_move | query | number | no | Smallest move to include, as a ratio of the opening price. 0.25 is a quarter shorter or longer. |
| region | query | string | no | Restrict to one jurisdiction. |
| run_type | query | string | no | Restrict to one run type. |
| date_from | query | string | no | YYYY-MM-DD. Clamped to your plan's window. |
| date_to | query | string | no | YYYY-MM-DD. Clamped to your plan's window. |
curl https://api.apihorseracing.com/v1/market/movers \ -H "X-API-Key: $AHR_KEY"
$ch = curl_init('https://api.apihorseracing.com/v1/market/movers'); 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/market/movers', { 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/market/movers", headers={"X-API-Key": os.environ["AHR_KEY"]}, ).json()
curl https://api.apihorseracing.com/v1/market/movers?key=$AHR_KEY
$url = 'https://api.apihorseracing.com/v1/market/movers' . '?key=$AHR_KEY'; $data = json_decode(file_get_contents($url), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/market/movers?key=$AHR_KEY' ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/market/movers?key=$AHR_KEY", ).json()
{ "meta": { "request_id": "6d16f0b3ce4b43c8", "data_as_of": "2026-09-12T05:10:27+00:00", "plan": "analyst", "window": "2017 → upcoming", "count": 3 }, "data": { "window": { "from": null, "to": null, "note": "The whole archive. Your plan sets no date limit." }, "by_movement": [ { "movement": "drifted", "runs": 238036, "priced": 238036, "first": 20197, "second": 23001, "third": 24628, "fourth": 25263, "win_pct": 0.0848, "top3_pct": 0.2849, "expected_wins": 19621.82, "a_e": 1.029, "a_e_range": [ 1.015, 1.044 ], "a_e_beats_market": true, "significant": true, "low_sample": false, "pl_1pt": -66141.76, "roi_pct": -27.79 }, { "movement": "drifted sharply", "runs": 166181, "priced": 166181, "first": 6538, "second": 8556, "third": 10956, "fourth": 13410, "win_pct": 0.0393, "top3_pct": 0.1568, "expected_wins": 6609.09, "a_e": 0.989, "a_e_range": [ 0.965, 1.013 ], "a_e_beats_market": false, "significant": false, "low_sample": false, "pl_1pt": -65830.21, "roi_pct": -39.61 }, "… 3 more" ], "examples": [ { "race_id": "rc_37HG0ZZ", "horse_id": "hs_2P8EM42", "horse": "How About It", "course": "Southwell", "date": "2018-03-05", "opening": "9/1", "sp": "150/1", "drift": 14.1, "position": 4 }, { "race_id": "rc_1QGF7XR", "horse_id": "hs_5GG8NB", "horse": "Jour D'orage", "course": "Worcester", "date": "2024-09-27", "opening": "200/11", "sp": "250/1", "drift": 12.0853, "position": 8 }, "… 1 more" ], "note": "Movement is starting price against opening price. Only runners whose opening price was published." } }
A real response from /v1/market/movers?limit=3,
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 |
|---|---|---|
| window | string | How far back and forward that plan can read. |
| runs | integer | Rides in the slice, priced or not. |
| priced | integer | Rides carrying a starting price. A/E and the profit figures use these; runs is the wider count. |
| first | integer | Wins. |
| second | integer | Seconds. Positions are returned separately so you apply your own place terms. |
| third | integer | Thirds. |
| fourth | integer | Fourths. |
| win_pct | number | Wins over runs. The number everyone quotes and the one that tells you least. |
| top3_pct | number | First, second or third over runs. Not a place strike rate; place terms vary. |
| expected_wins | number | Wins the market implied, summed over the slice. |
| a_e | number | Actual wins over expected. Above 1.00 means winning more often than the prices said. |
| a_e_range | array | Ninety five percent interval on A/E. If it spans 1.00 the figure is not evidence of anything. |
| a_e_beats_market | boolean | True only where the whole interval sits above 1.00. |
| significant | boolean | Whether the interval excludes 1.00 in either direction. |
| low_sample | boolean | Fewer than five expected wins. Treat A/E as noise. |
| pl_1pt | number | One point on every runner, settled at starting price. |
| roi_pct | number | Level stakes return. Backing everything loses more than the book margin, because equal stakes overweight the longshots. |
| race_id | string | Opaque race identifier. Stable, and safe to store. |
| horse_id | string | Opaque horse identifier. |
| horse | string | Horse name. |
| course | string | Course name. |
| date | date | Race date, YYYY-MM-DD. |
| opening | string | The price the runner opened at, where published. |
| sp | string | Starting price, fractional, as returned. |
| drift | number | Starting price against opening, as a ratio. Positive drifted, negative shortened. |
| position | integer | Finishing position. Null means the runner did not complete or was not published below the placings. |
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.
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.
| 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. |
| upgrade_required | 403 | The endpoint returns computed statistics and your plan does not include them. The body names every plan that does reach it, so you never have to look it up. |
| invalid_param | 422 | A date that is not YYYY-MM-DD, a by= dimension we do not have, or a cursor that did not come from us. The message names the parameter. |
| 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
Whether money moving before the off tells you anything, measured across every runner that carries an opening price. Movers are grouped by how far they moved and in which direction, each band with a full statistics block.
The question is old and the folklore is confident: a horse that shortens is fancied and a drifter is not. This is the endpoint that decides whether that survives a confidence interval.
Two things limit it, and both should be understood before you read a result. Price movement exists only where the source published an opening price, which in this archive means Britain on about 59% of runners and Ireland on about 52%, and essentially nowhere else. And the movement is opening to starting price, not a full series, so a horse that shortened and drifted back looks like it never moved.
Both facts are on every runner in price_path.source, and this endpoint only considers runners where a real opening price exists rather than treating an absent one as no movement.
direction=shortened with a min_move, over as much of the archive as your plan reaches. Read the interval.
If a band does show a genuine effect, the A/E is directly usable as a weight, and it already accounts for the price.
The coverage limitation means any model using this only works on British and Irish racing. Better to know that first.