---
title: "Thoroughbred form, career record and profile for one horse"
method: GET
path: "/v1/racing/horses/form"
tags: ["Racing"]
---

# Thoroughbred form, career record and profile for one horse

`GET /v1/racing/horses/form`

## Request

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

Cost: 3 credits. One AU thoroughbred's recent form, career record and profile,
parsed from Racing Australia's public horse page — the registry's own data.

SOURCE AND FRESHNESS. The horse's page is read on demand and the parsed result stored
(`source.fetched_at` dates it), so the first query for a horse pays a page-read to RA
(typically under two seconds) and later reads are instant. A horse's form only
changes when it races or trials, so the stored copy is re-read from RA only when our
own results archive shows the horse has started since it was read, or when the copy
is more than seven days old (a barrier trial is invisible to results, so it surfaces
within a week at worst). Inside six hours it is served without even that check. A
horse collected once, by you or by anyone, is therefore served from our store without
another RA read for as long as it stays unraced. When RA cannot be reached and a
stored copy exists, that copy is served
with `source.stale: true` rather than an error. When RA is refusing reads (its bot
wall) and no stored copy exists, the answer is 503 with a `Retry-After` header and
fresh reads pause for that long; stored horses keep serving throughout. Only a 200
is billed — a 404, 502 or 503 costs nothing.

FRESH READS ARE CAPPED PER KEY AND PACED (2026-10-13). A fresh read is one that has to go
to Racing Australia because we hold no current copy of the horse's page. Each API key may
make at most 30 fresh reads in any rolling 60 seconds, and fresh reads from every caller
together leave at most one every 2.5 seconds (bursts of three); a read whose turn is more
than 20 seconds away is not held open. Past either limit nothing is sent to Racing
Australia and the answer is the one given while RA is refusing reads: a horse we hold is
served from the store, otherwise its runs in our own results where we have them, otherwise
a 503 with `Retry-After`, unbilled, whose detail says which limit it was. Why, measured to
2026-10-12: over the previous 14 days Racing Australia's refusals closed fresh reads for
every caller 94 times, each refused read prolongs a closure, and one key alone sent 44
calls in two minutes.

RESOLVING `horse`. Pass a name, or the `pe:` horse_ref that every thoroughbred surface
now publishes — they resolve identically, because the horse_ref is the folded name. An
`ra:` code from `runner_ref` is accepted too, but prefer the name or horse_ref: Racing
Australia issues a new code for the same horse each day. Names resolve against
horses seen on our own feeds: acceptances roughly three days ahead, plus every archived
result runner (from 2026-09-01) — and, failing both, our own run store and stored form
pages, so a horse that last raced months ago still resolves (2026-10-03). Where we hold
runs but no readable RA page, those runs are the answer (`source.feed`
'puntersedge_results'). A name we have never seen returns 404 even when RA knows the
horse — there is deliberately no name-search against RA itself.

ONE NAME, ONE HORSE. A name resolves to the horse, and the most recently seen of its Racing
Australia codes is read — a horse that has raced more than once holds several codes,
because Racing Australia issues a new one each day. `ambiguous` is always false and
`candidates` always empty; both stay on the response for compatibility. Two different
horses sharing one folded name cannot be told apart here; the AU register does not allow
two ACTIVE horses to share one (a name is released about 17 years after its horse retires).

`horse_ref` ON THE RESPONSE is that identity, `pe:<name>`, and it is the same value on
/v1/racing/results runners and placings, the live board, the closing-line and price-path
archives, /v1/racing/horses/runs and the backfill. `runner_ref` is the code the page was
read under; it changes from day to day. Join a horse on `horse_ref`; join the rows about
one race on `runner_ref`.

THE STORE IS KEYED ON THE HORSE. A page collected under ANY of the horse's codes — by you,
by another customer, or by the paced backfill — answers every later lookup of that horse.

READING A RUN. Runs come newest first: the page's recent-form table, typically the
last ten starts plus barrier trials (`trial: true` — RA publishes those with $0
prize, 0kg weights and no market). `race_class` is RA's official class string
('MDN-SW', 'BM64', 'CTRY MDN'), the first place race class appears anywhere on this
API. `prices` is the published price path oldest-quote-to-SP and `sp` its last
entry. `margin_l` is lengths behind the winner — on the horse's own win, its winning
margin. `pir` is position in running at the 800m and 400m marks where published.
`placegetters` names the two nearest finishers with their own ra: codes, so one
horse's page walks to its rivals'. Track names are RA's compressed codes ('W FM',
'RAND') served as published. `trainer` on a run is the trainer OF THAT DAY, joined
from our own results archive — populated for runs from 2026-09-01 onward and NULL
before, because RA's form table names only the current trainer (`profile.trainer`);
the archive floor moves back nowhere, so the populated share grows with every
meeting.

THE CAREER BLOCK. RA's own summaries, not derived from the runs list: overall
starts/wins/seconds/thirds, prizemoney, and per-dimension splits (first_up,
second_up, firm/good/soft/heavy/synthetic; track/distance splits appear when RA
renders them). These cover the whole career even where the runs list shows only
recent starts.

JOINING TO LIVE DATA. `runner_ref` here, on /v1/racing/results runners and on enriched
live runners is the same identifier FOR ONE MEETING, so a pre-race model row joins to that
day's official result and to the form read for it. Across meetings, join on `horse_ref`
instead — it is on all of those surfaces too and it does not change. `jockey_code` and
`meet_code` are RA's own identifiers for the rider and the meeting, and those do persist.

Cached 10 minutes per query on top of the six-hour page store.

## Query parameters

- `horse` string, required: Horse name, the `pe:` horse_ref published on every thoroughbred surface ('pe:custo'), or the ra: entry code from runner_ref on results/live runners. Names match case- and punctuation-insensitively on the whole name, and a name and its horse_ref resolve identically.

## Headers

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

## Response `200`

Successful Response

- HorseFormOut
  - `horse_name` string, nullable
  - `horse_ref` string, nullable: The HORSE: `pe:<name>`, the registered name folded to letters and digits. The same value on /v1/racing/results runners and placings, the live board, the closing-line and price-path archives, /v1/racing/horses/runs and the form backfill — and accepted as `horse` here. This is the key to join a horse across meetings.
  - `runner_ref` string, nullable: The Racing Australia code the page was read under — the most recent code we hold for the horse. Racing Australia issues a new code for the same horse each day, so it is not a horse identity — `horse_ref` is.
  - `ambiguous` boolean, required: ALWAYS FALSE since 2026-09-21, and kept only so that callers reading it do not break. A name now resolves to one horse: several ra: codes under one name are the ordinary case (they are entry codes), not two horses. `candidates` is empty with it.
  - `candidates` HorseCandidate[]
    - `runner_ref` string, required: A Racing Australia code for this horse. `candidates` is always empty: a name resolves to one horse. See `ambiguous`.
    - `runner_name` string, nullable
    - `last_seen` string, nullable: Most recent date this ref appeared on our feeds (acceptance or result).
    - `source` string, required: Where it was seen: acceptances or results.
  - `profile` HorseProfile
    - `age_sex` string, nullable: As published, e.g. '4yo Bay Gelding'.
    - `dob` string, nullable
    - `sire` string, nullable
    - `dam` string, nullable
    - `status` string, nullable
    - `trainer` string, nullable
    - `trainer_location` string, nullable
    - `owners` string, nullable
    - `colours` string, nullable
    - `last_gear_change` string, nullable: As published, date and gear, e.g. '20-Aug-2026, Blinkers, First Time, ...'.
  - `career` HorseCareer
    - `starts` integer, nullable
    - `wins` integer, nullable
    - `seconds` integer, nullable
    - `thirds` integer, nullable
    - `prizemoney` integer, nullable: Career prizemoney, whole AUD.
    - `bonus` integer, nullable: Total bonus (e.g. BOBS), whole AUD.
    - `min_dist_win_m` integer, nullable
    - `max_dist_win_m` integer, nullable
    - `splits` object: RA's own career splits, keyed first_up, second_up, firm, good, soft, heavy, synthetic — plus track, distance and track_distance when RA renders them. Each value is starts/wins/seconds/thirds.
  - `runs` HorseRun[]
    - `date` string, nullable
    - `venue` string, nullable: Venue name. Set when `source.feed` is 'puntersedge_results'; RA's own page gives the compressed `track` code instead and leaves this null.
    - `origin` string, nullable: Only when `source.feed` is 'puntersedge_results': 'results' (our own settled results) or 'history' (collected afterwards from RA's results page).
    - `track` string, nullable: RA's compressed track code as published ('W FM' is Warwick Farm, 'RAND' Randwick).
    - `meet_code` string, nullable: RA meeting code; every runner in the same meeting shares it.
    - `race_number` integer, nullable
    - `trial` boolean, required: True for a barrier trial: $0 prize, 0kg weights and no market are how RA publishes them.
    - `position` integer, nullable
    - `field_size` integer, nullable
    - `result_raw` string, nullable: The position cell as published, kept for rows that don't parse to a number.
    - `distance_m` integer, nullable
    - `going` string, nullable: Condition word: Firm, Good, Soft, Heavy, Synthetic.
    - `going_rating` integer, nullable: The number after the condition (Soft7 -> 7). Null where RA prints none.
    - `race_class` string, nullable: RA's official class string for that race: 'MDN-SW', 'CTRY MDN', 'BM64', '3Y MDN-TRL'.
    - `prize_total` integer, nullable
    - `prize_won` integer, nullable: This horse's cut, when RA shows it.
    - `jockey` string, nullable
    - `jockey_code` string, nullable: RA's stable jockey code, consistent across every page that names the same rider.
    - `trainer` string, nullable: Trainer of the day for THIS run, filled from our own results archive — RA's form table never names the trainer per run. Populated for runs from 2026-09-01 onward (the archive's full-runner floor) and NULL before that; `profile.trainer` is the horse's current trainer, which is usually but not always the same person.
    - `weight_kg` number, nullable
    - `barrier` integer, nullable
    - `time_s` number, nullable: Official race time in seconds.
    - `sectional_distance_m` integer, nullable: The published closing sectional's distance, usually 600.
    - `sectional_time_s` number, nullable
    - `margin_l` number, nullable: Margin in lengths. On this horse's own win it is the winning margin over the runner-up.
    - `pir` RunPIR[]: Position in running at the published marks (800m/400m).
      - `at_m` integer, required: Metres from the finish, e.g. 800.
      - `position` integer, required
    - `prices` number[]: The published price path, opening quote through SP, oldest first.
    - `sp` number, nullable: Starting price — the last entry of `prices`.
    - `placegetters` RunPlacegetter[]: The two nearest finishers RA lists (2nd and 3rd on this horse's win; the winner and runner-up otherwise), each with its own ra: code.
      - `position` integer, required
      - `name` string, required
      - `runner_ref` string, nullable: That horse's ra: code from that meeting. Racing Australia issues a new code each day, so to look the horse up here pass its name.
      - `weight_kg` number, nullable
  - `source` HorseSource
    - `feed` string: 'racing_australia' -- Racing Australia's own form page (profile, career, full run lines). 'puntersedge_results' -- RA was not serving this horse's page and no stored copy existed, so `runs` is the horse's record in OUR OWN collected results instead; `profile` and `career` are then null, and `note` says what the runs cover.
    - `note` string, nullable: Set with feed 'puntersedge_results': what these runs are, and what they are not.
    - `region` string
    - `sport` string
    - `fetched_at` string, nullable: UTC time this horse's page was last read from RA.
    - `stale` boolean: True when RA could not be reached just now and this is the stored copy served past its six-hour refresh window.

## 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)
