Testing the received wisdom about a track
One distance, min_runners at eight or more, and read the interval before the headline. Most famous biases do not survive it.
Documentation / Courses & bias
/v1/courses/{course_id}/draw-bias
Low, middle and high thirds of the field, per distance.
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 |
|---|---|---|---|---|
| course_id | path | string | yes | Opaque course identifier. |
| distance | query | string | no | Restrict to one distance as published, e.g. 5f. Strongly recommended; omitting it averages away the effect you are looking for. |
| min_runners | query | integer | no | Smallest field to include. Defaults to 8, because a draw cannot matter in a field of four. |
| going | query | string | no | Restrict to one going. Draw effects on turf often depend on the ground. |
curl https://api.apihorseracing.com/v1/courses/cr_JSRC8Z/draw-bias \ -H "X-API-Key: $AHR_KEY"
$ch = curl_init('https://api.apihorseracing.com/v1/courses/cr_JSRC8Z/draw-bias'); 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/courses/cr_JSRC8Z/draw-bias', { 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/courses/cr_JSRC8Z/draw-bias", headers={"X-API-Key": os.environ["AHR_KEY"]}, ).json()
curl https://api.apihorseracing.com/v1/courses/cr_JSRC8Z/draw-bias?key=$AHR_KEY
$url = 'https://api.apihorseracing.com/v1/courses/cr_JSRC8Z/draw-bias' . '?key=$AHR_KEY'; $data = json_decode(file_get_contents($url), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/courses/cr_JSRC8Z/draw-bias?key=$AHR_KEY' ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/courses/cr_JSRC8Z/draw-bias?key=$AHR_KEY", ).json()
{ "meta": { "request_id": "9ec3c1f9249ae1cd", "data_as_of": "2026-09-12T05:10:27+00:00", "plan": "analyst", "window": "2017 → upcoming", "count": 0 }, "data": { "course_id": "cr_JSRC8Z", "course": "Fontwell", "distance": "all", "window": { "from": null, "to": null, "note": "The whole archive. Your plan sets no date limit." }, "by_third": [], "by_stall": [], "note": "Only races with a recorded draw and six or more runners.", "distances_available": [] } }
A real response from /v1/courses/cr_JSRC8Z/draw-bias,
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 |
|---|---|---|
| course_id | string | Opaque course identifier. |
| course | string | Course name. |
| distance | string | Official distance as published, e.g. "3m 3f 119y". |
| window | string | How far back and forward that plan can read. |
| 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
Whether starting position matters here. The field is split into low, middle and high thirds by stall number, and each third gets a full statistics block including A/E, so the question asked is whether a stall beat its price rather than whether it won.
Thirds rather than individual stalls, deliberately. Individual stall numbers across varying field sizes produce rows with a handful of runners each and an apparently dramatic pattern that is entirely noise. Thirds are comparable across field sizes and have enough behind them to say something.
Ask for a single distance. Draw bias is a property of a course, a distance and often a going, and averaging a five furlong sprint together with a mile and a half will reliably show no effect at a track where a real one exists.
Expect most of these to come back insignificant, and treat that as the endpoint working. Draw bias is genuine at a handful of tracks and folklore at most of the rest, and the interval is what separates the two.
One distance, min_runners at eight or more, and read the interval before the headline. Most famous biases do not survive it.
Where low and high thirds have intervals that do not overlap, and the pattern holds across goings, you have something.
The A/E per third is directly usable as a correction, and it already accounts for whatever the market was pricing in.
Only flat racing carries a draw. Jumps races return an empty result rather than an error, because having no draw is a fact about the race rather than a failure of the query.