Spotting improvement
A rising rolling average over the last few runs, at a horse that is not yet shortening in the market.
Documentation / Horses & pedigree
/v1/horses/{horse_id}/trend
Each run measured against what the market expected of it.
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 |
|---|---|---|---|---|
| horse_id | path | string | yes | Opaque horse identifier. |
| window | query | integer | no | Runs per rolling window, 3 to 20. Defaults to 6. |
curl https://api.apihorseracing.com/v1/horses/hs_V8JGH6/trend \ -H "X-API-Key: $AHR_KEY"
$ch = curl_init('https://api.apihorseracing.com/v1/horses/hs_V8JGH6/trend'); 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/horses/hs_V8JGH6/trend', { 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/horses/hs_V8JGH6/trend", headers={"X-API-Key": os.environ["AHR_KEY"]}, ).json()
curl https://api.apihorseracing.com/v1/horses/hs_V8JGH6/trend?key=$AHR_KEY
$url = 'https://api.apihorseracing.com/v1/horses/hs_V8JGH6/trend' . '?key=$AHR_KEY'; $data = json_decode(file_get_contents($url), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/horses/hs_V8JGH6/trend?key=$AHR_KEY' ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/horses/hs_V8JGH6/trend?key=$AHR_KEY", ).json()
{ "meta": { "request_id": "816f8567fdf35ca4", "data_as_of": "2026-09-12T05:10:27+00:00", "plan": "analyst", "window": "2017 → upcoming", "count": 17 }, "data": { "horse_id": "hs_V8JGH6", "horse": "The Galloping Bear", "window": { "from": null, "to": null, "note": "The whole archive. Your plan sets no date limit." }, "runs": [ { "date": "2019-04-16", "course": "Exeter", "run_type": null, "class": "4", "field_size": 11, "position": 5, "casualty": null, "sp": "8/1", "expected_win": 0.0913, "above_market": -0.0913, "cumulative": -0.0913 }, { "date": "2021-02-28", "course": "Fontwell", "run_type": null, "class": "6", "field_size": 16, "position": 1, "casualty": null, "sp": "10/3", "expected_win": 0.1808, "above_market": 0.8192, "cumulative": 0.7279 }, "… 15 more" ], "summary": { "total_runs": 17, "cumulative_vs_market": 1.586, "last_6_vs_market": -0.515, "direction": "declining" } } }
A real response from /v1/horses/hs_V8JGH6/trend,
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 |
|---|---|---|
| horse_id | string | Opaque horse identifier. |
| horse | string | Horse name. |
| window | string | How far back and forward that plan can read. |
| runs | integer | Rides in the slice, priced or not. |
| date | date | Race date, YYYY-MM-DD. |
| course | string | Course name. |
| run_type | string | Flat, Hurdle, Chase or NH Flat. |
| class | string | Race class. |
| position | integer | Finishing position. Null means the runner did not complete or was not published below the placings. |
| casualty | string | Why a runner did not complete: PulledUp, Fell, UnseatedRider, BroughtDown, RefusedToRace, RanOut. |
| sp | string | Starting price, fractional, as returned. |
| direction | string | drifted, shortened or unchanged. |
The code a race was run under lives on the horse profile rather than the race, so it arrives on a later pass. A null run_type means not yet fetched, not unclassified.
Class is published in Britain and reliably absent elsewhere. Do not treat a null class as an ungraded race.
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.
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. |
| 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. |
| 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. |
| 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
Each run measured against what the market expected of it, in date order, with a rolling average so a direction is visible rather than inferred.
A finishing position on its own says almost nothing about improvement: third of four at odds-on is a worse run than sixth of twenty at 33/1. This endpoint scores each run against its own market instead, so a horse beating its price repeatedly shows as a rising line whatever the finishing positions look like.
It answers the question everybody asks of form and form cannot answer: is this horse going the right way.
A rising rolling average over the last few runs, at a horse that is not yet shortening in the market.
The same in reverse, and rather more common in older horses.
Combine with the layoff endpoint. Improving before a break is not the same as improving after one.
Needs runs to say anything. Under about eight the rolling average is mostly noise, and low_sample says so.