---
title: "One thoroughbred's runs in our own settled results"
method: GET
path: "/v1/racing/horses/runs"
tags: ["Racing"]
---

# One thoroughbred's runs in our own settled results

`GET /v1/racing/horses/runs`

## Request

- Base URL: `https://api.puntersedge.online`
- URL: `https://api.puntersedge.online/v1/racing/horses/runs`
- Auth: API key in header `X-API-Key`

Cost: 3 credits. Every run we hold for one AU thoroughbred, newest first, out of our own
settled results.

THIS IS NOT A CAREER RECORD. Each run here is a race WE collected and settled: the same rows
/v1/racing/results serves, flattened one-per-horse. The store begins at the first complete card
on `source.coverage_from` and `source.coverage_to` is the latest race in it, so `runs_total` is
this horse's record SINCE THAT DATE, not its lifetime. The same run never changes once written,
so the record only grows — build a longer history forward from here.

WHY YOU WOULD USE IT INSTEAD OF /v1/racing/horses/form. That endpoint is Racing Australia's own
form page: deeper (career splits, sectionals, position-in-running, the price path to SP) but
dependent on RA serving us the page. Since 2026-09-21 RA's edge answers every fresh form read
with a human-verification challenge, so horses we have not already stored cannot be read at
all, and we do not automate that challenge. This endpoint has no such dependency — it reads
only what we already hold — and it is the honest answer to "what do you have on this horse
today". Use both: form for depth when RA lets us in, runs for availability always.

RESOLVING `horse` — PASS THE NAME, OR ITS `horse_ref`. A name is folded to letters and digits
(the trailing country parenthetical stripped first) and must match in full — the same rule
/v1/racing/horses/form applies, so one name resolves identically on both. `pe:custo`, the
`horse_ref` published on every thoroughbred runner, placing, live-board entry and archive row,
is the same fold already done and resolves to exactly the same runs. A Racing Australia
horsecode (`ra:MjgwNjM0ODg2NDA`, or the bare code) is accepted too, but it finds one run
rather than a horse: Racing Australia issues a new code for the same horse each day, so a code
filter returns the one run that carries it. `runner_refs` lists every code published under the
name; more than one is normal. The other side of grouping on a name is
that a registered name reused by two horses returns the union of both. A horse we hold no run
for is a 404 and costs nothing; an unknown query parameter is a 422 and costs nothing.

READING A RUN. `position` is the finishing place, null when the horse did not start (`status`
is `scratched`) or started and did not finish — `abnormal` is then RA's own reason cell,
verbatim and from no closed list. `sp` is the official starting price and `sp_note` RA's
favouritism marker; null `sp` means unknown, never zero. `margin` is RA's own text in lengths.
`time_s` is the WINNER's time only: RA prints one time per race, in its race header, and no
per-runner time in the results table, so since 29 September 2026 a position-1 run carries the
race time and every other run's `time_s` is null. Every run of a race also carries the race-level
`race_time_s` and `last_600m_s` (RA's header, seconds) and `prize_total`, and each placed run its
`prize_won` off RA's prize line (dead heats share the places they tie for; 0 outside the paid
places); all four are null on runs stored before that date until the meeting is re-read, and
before 29 September no run carried any time (0 of 17,344 stored runs, measured 2026-09-28).
`trainer` and `jockey` are the ones of that day, which is
what the result published — `profile.trainer` on the form endpoint is the CURRENT trainer.
`field_size` counts the horses that ran. `weight_kg` is the allotted weight and `carried_kg`
the weight actually carried when RA printed one (a claim under it, or an overweight rider over it); `race_class` is RA's own leading
conditions term ('Maiden', 'BenchMark 64'). All three are kept since 28 September 2026. On
`origin: tabtouch` runs, and on runs from 10 August to 27 September 2026 whose result lacked
them, `race_class` and `weight_kg` come from TABtouch's race page instead: its own class wording
('BM58 HCP', 'MDN-SW') and its printed weight, with `carried_kg` null. `result_id` joins back to /v1/racing/results, except on `origin: history` and `origin: tabtouch` runs, whose synthetic 'hist:...' id joins to nothing.

`stats` is computed over every held run, not just the `limit` returned, and counts only starts
(`status: ran`). `runs_total` is that same count of starts; `held_rows_total` is every held row,
scratchings included, and `runs` is the newest `limit` of those rows -- compare `len(runs)` with
`held_rows_total` to know whether you have them all. Before 2026-10-13 `runs_total` counted the
scratchings too.

Cached 10 minutes per query; the store itself refreshes hourly at :59.

## Query parameters

- `horse` string, required: Horse NAME (matched on letters and digits, case- and punctuation-insensitive, whole name), or the `horse_ref` any thoroughbred surface publishes ('pe:custo') — either gathers the whole record, and they resolve identically. A Racing Australia horsecode from `runner_ref` ('ra:MjgwNjM0ODg2NDA' or the bare code) is accepted too, but it pins one results entry, usually one meeting's runs, not the horse.
- `limit` integer: Most recent runs to return.

## Headers

- `X-API-Key` string
- `X-RapidAPI-Proxy-Secret` string

## Response `200`

Successful Response

- HorseRunsOut
  - `horse` string, required: The horse's name as our results spell it.
  - `name_key` string, required: The identity the runs were grouped on (letters and digits).
  - `horse_ref` string, nullable: `name_key` as the identifier every thoroughbred surface publishes: `pe:<name_key>`. Pass it back here, or to /v1/racing/horses/form, and read it off /v1/racing/results runners and placings, the live board and the closing-line and price-path archives to gather one horse across meetings.
  - `runner_refs` string[], required: Every Racing Australia horsecode our results published under this name, sorted. Several is normal, not an error — see `runs[].runner_ref`.
  - `runs_total` integer, required: STARTS held for this horse since `source.coverage_from`: runs with `status` `ran`, the same count as `stats.starts`. A scratching is not a start. Before 2026-10-13 this counted every held row, scratchings included; that count is `held_rows_total`.
  - `held_rows_total` integer, required: Every row held for this horse since `source.coverage_from`, scratchings included. `runs` is the newest `limit` of THESE rows, so page on this, not on `runs_total`. Measured on 2026-10-12, 61,719 of the store's 282,639 rows (21.8%) were scratchings.
  - `runs` HorseRunRow[], required
    - `date` string, nullable: Sydney calendar date of the race; the London date for a British or Irish race (/v1/racing/form, 2026-10-12).
    - `start_time` string, nullable: Scheduled start, UTC.
    - `venue` string, nullable
    - `race_number` integer, nullable
    - `distance_m` integer, nullable
    - `race_name` string, nullable
    - `track_condition` string, nullable: As published for the meeting, e.g. 'Good 4'.
    - `number` integer, nullable: Saddlecloth number.
    - `box` integer, nullable: Barrier. Null on a scratching — a runner that never jumped has no barrier.
    - `position` integer, nullable: Finishing place. Null when the horse did not start (see `status`) or started and did not finish (see `abnormal`).
    - `status` string, nullable: `ran` or `scratched`, exactly as /v1/racing/results publishes it.
    - `abnormal` string, nullable: Racing Australia's own Finish cell whenever it is not a number, passed through verbatim (Fell, PulledUp, TailedOff, LR, FF, LP, and 'SB' for scratched at the barrier). THERE IS NO CLOSED LIST — an unseen code reaches you unchanged. Read it with `status`.
    - `margin` string, nullable: Lengths behind the winner as RA printed it ('0.75L'); null where it prints none.
    - `time_s` number, nullable: The horse's own time in seconds, which RA publishes for the WINNER only: since 29 September 2026 a position-1 run carries the race time from RA's race header (the same value as `race_time_s`); every other run's is null, because RA's results table prints no per-runner time and a beaten horse's time is not the race time. Null on runs stored before then until that meeting is re-read.
    - `sp` number, nullable: Official starting price, decimal return on $1. Null = unknown, never zero.
    - `sp_note` string, nullable: RA's favouritism marker: 'F' favourite, 'EF' equal favourite; null otherwise.
    - `dead_heat` boolean
    - `trainer` string, nullable: Trainer of the day for THIS run, as the result published it.
    - `jockey` string, nullable: Rider of the day for THIS run.
    - `field_size` integer, nullable: Horses that ran in that race.
    - `weight_kg` number, nullable: Allotted weight in kg, as RA's results page prints it. Kept since 28 September 2026. On origin tabtouch runs, and on runs from 10 August to 27 September 2026 whose result lacked one, it is the weight TABtouch's race page printed for the starter. Null on a scratching and where neither source had one.
    - `carried_kg` number, nullable: Weight the horse actually carried, printed by RA only when it differs from the allotted weight ('58kg (cd 56kg)'): usually an apprentice's claim (under), sometimes a rider's overweight (over), so do not assume the sign. Null when RA printed none; read coalesce(carried_kg, weight_kg) for what was carried.
    - `race_class` string, nullable: The race's class as RA's own leading conditions term -- 'Maiden', 'BenchMark 64', 'Class 1', 'Group 1' -- mechanical, never inferred, on runs stored since 28 September 2026 and on origin history runs. On origin tabtouch runs, and on runs from 10 August to 27 September 2026, it is TABtouch's class line as printed -- 'BM58 HCP', 'MDN-SW', 'GROUP 1 3UP HCP' -- so normalise the two wordings before comparing classes across runs. Null where neither source had one.
    - `race_time_s` number, nullable: The race's winning time in seconds, from RA's race header ('Time: 0:58.69' -> 58.69). The same on every run of that race. Kept since 29 September 2026: null on a run stored before then until that meeting is re-read.
    - `last_600m_s` number, nullable: The race's last-600m time in seconds, from the same header ('Last 600m: 0:34.49' -> 34.49) -- the leader's closing sectional as RA times it, a RACE figure, not this horse's own. Kept since 29 September 2026.
    - `prize_total` integer, nullable: Total prize money of the race in AUD, from RA's prize line ('Of $30,000.'). Kept since 29 September 2026.
    - `prize_won` number, nullable: Place money this horse won in AUD, read off the same prize line by its finishing place ('1st $15,000, 2nd $5,300, ...'); dead-heaters share the places they tie for equally. 0 for a finish outside the places RA lists as paid; null for a horse with no finishing place (scratched, fell) or a race whose prize line was not read. Excludes bonuses and welfare-fund amounts. Kept since 29 September 2026.
    - `runner_ref` string, nullable: The Racing Australia code our results collector published for this run, from race day — the same value on the matching /v1/racing/results runner and in the archives for that race. Racing Australia issues a new code for the same horse each day, so it differs from run to run: group a horse's runs on `horse_ref`, not on this.
    - `horse_ref` string, nullable: The horse this run belongs to: `pe:<name>`, the registered name folded to letters and digits. Identical on every run of the same horse, and the same value /v1/racing/results runners and placings, the live board, the closing-line and price-path archives and /v1/racing/horses/form carry — the key that joins a horse across meetings, where `runner_ref` joins the rows about one race.
    - `result_id` string, nullable: Id of the /v1/racing/results row this run came from. A run with origin 'history' or 'tabtouch' carries a synthetic 'hist:...' id that joins to /v1/racing/results on nothing.
    - `origin` string, nullable: 'results' -- from our own settled results (/v1/racing/results). 'history' -- collected afterwards from Racing Australia's results page for a race held before our live results began: same fields, no /v1/racing/results row behind it. 'tabtouch' -- collected afterwards from TABtouch's public race page for a race older than Racing Australia's results pages reach (about fifteen months back): number, rider, barrier, weight, trainer and the first four placings, so position null there means unplaced; no margin, time or sp.
  - `stats` HorseRunsStats, required
    - `starts` integer, required: Runs with status `ran` — scratchings are excluded from every figure here. Equal to `runs_total` since 2026-10-13; `held_rows_total` counts the scratchings too, which is why it can be higher.
    - `wins` integer, required
    - `places` integer, required: Finishes in the first three, wins included.
    - `win_pct` number, required
    - `place_pct` number, required
  - `source` RunsSourceBlock, required
    - `feed` string: Where the data comes from: our own collection of published AU thoroughbred results (Racing Australia's results pages, read hourly by our results collector), flattened to one row per horse per race and refreshed hourly. Not Racing Australia's form page — that is /v1/racing/horses/form.
    - `region` string
    - `sport` string
    - `coverage_from` string: The first complete card in the store. A horse's record here starts here — it is not a career.
    - `coverage_to` string, nullable: Sydney date of the latest race in the store.
    - `collectors` string[]: Which of our collectors can contribute a run. AU thoroughbred results come from one: the Racing Australia results pages.
    - `refreshed_hourly` boolean: The store is rebuilt from our own results at :59 each hour, after the results pass.

## Other responses

- `401`: Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors
- `402`: Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing
- `422`: Validation Error
- `429`: Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits
- `500`: Unexpected server error. Safe to retry with backoff.

---

[API](https://skmtc.dev/puntersedge/apis/puntersedge-odds-api.md) · [All operations](https://skmtc.dev/puntersedge/apis/puntersedge-odds-api/llms.txt) · [OpenAPI document](https://skmtc.dev/puntersedge/apis/puntersedge-odds-api/revisions/f35b6aab1946?raw)
