---
title: "Settled race results"
method: GET
path: "/v1/racing/results"
tags: ["Racing"]
---

# Settled race results

`GET /v1/racing/results`

## Request

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

Cost: 2 credits. Settled AU/NZ race results — finishing order with the bookmaker's own
settled fixed prices, plus scratchings and their deductions — and, from 2026-10-06, British and
Irish thoroughbred results from Betway's settlement feed: every runner with its starting price
(`sp` on placings and runners) and its finishing position where Betway publishes one (the whole
order on the British races measured; on the Irish, the first four first and the rest within
about 25 minutes of the off, while the result is still `interim`), non-runners as deductions with
no Rule 4 figure, no dividends and no fixed settling prices (`win_price`/`place_price` null). A starter
Betway has still not ranked once the result is `final` did not finish -- Betway ranks every finisher and
leaves fallers and pulled-up horses unranked -- and from 2026-10-12 carries `abnormal: did not finish`
with `position` null. UK racecards
publish no race numbers; a GB/IE result takes the running-order number of the race it matched.
From 2026-10-12, British and Irish greyhound results come from PointsBet's settled race card: the
paid places (three on a six-dog field) with PointsBet's own settled fixed prices as
`win_price`/`place_price`, the whole field as `runners` with a position on each placed dog,
scratchings as deductions; no starting prices and no dividends on these races.

`status` is `interim` until the placings have been stable for 30 minutes, then `final`.
A race that did not run is `abandoned` (Racing Australia's official notice, applied to every
race of the meeting, with `status_note` carrying RA's word: abandoned, postponed, or
transferred to another venue) — empty `placings`, null `runners`, filterable with
`status=abandoned`.
Settle only against `final`: an interim placing can still change on protest.

**A `runners` entry is not necessarily a starter.** `status` says whether the runner ran:
`ran`, or `scratched` / `late_scratching` / `reserve` for one that did not. Since
2026-09-08 an AU thoroughbred scratched at the barrier arrives as `status: scratched`
carrying `abnormal: 'SB'`; a result written before that date carries such a runner as
`status: ran` with a null `position`. Such a row
can still carry `sp` — the last price RA printed for it. That price is NOT what its
`deductions` row was struck from (measured: 1/`sp` matches the published win deduction on
none of the five stored priced rows), so take the deduction from `deductions` itself and
never derive it. Settle on `status`, not on presence in `runners`. The thoroughbred and
greyhound collectors do NOT spell a late scratching the same way; the `runners` field
description says exactly how to tell them apart.

**Settled prices can be null while `status` is `interim` — this is the SP timing, not a
gap.** The source book publishes the finishing order before its settled dividends, so a
result fetched in the minutes after the jump can carry placings whose `win_price` /
`place_price` are still null. They fill in during the interim window: measured over 7
days (1,541 final results, 2026-09-01), every `final` result carried settled prices and
none was missing them. If you hit null prices, you are inside that window — poll again
or wait for `final`, which is already the only status you should settle against.

`sp` on `runners` and on `placings` is the OFFICIAL starting price — not our consensus, and
not the fixed price the bookmaker settled at. The two differ, and both are returned so you
can compare them. Thoroughbred SP is Racing Australia's (its favouritism marker lands in
`sp_note`), greyhound SP is our own collection's (Victorian tracks via FastTrack, every other
state via the national results pages, from 2026-09-13), and harness SP is always null because
no source we hold publishes it. A null `sp` means unknown — never zero.

`race_id` joins a result to /v1/racing/price-history, and to /v1/racing/next-to-go while the
race was still open — use it rather than matching on venue + race number + start time.

**Joining one RACE across endpoints: `runner_ref`. Joining one HORSE across meetings:
`horse_ref`.** Every thoroughbred entry in `runners` carries `runner_ref` (`ra:<code>`),
the code Racing Australia published for the runner on race day — the same value the
closing-line, price-path and markets archives carry and the live board carried that day,
so it joins this result to every other row about the race. Racing Australia issues a new
code for the same horse each day, so grouping a career on `runner_ref` returns one row per
start. For a career, every thoroughbred entry in `runners` and `placings` carries
`horse_ref`, `pe:<name>` — the registered name folded to letters and digits — the same
value on the live board, the closing-line and price-path archives, /v1/racing/horses/form
and /v1/racing/horses/runs. `horse_ref` is derived at serve time from the name already on
the row, so it is on every historical result too. Since 2026-10-16 it is on EVERY racing
code, not thoroughbreds alone: greyhound and harness entries carried a null before that.
Where a greyhound also has a `runner_ref` (`grv:`/`grsa:`) that per-dog registry id remains
the stronger identity and `horse_ref` sits beside it rather than replacing it — but those
refs are attached when a race is archived and only on Australian tracks, so a GB or IE dog,
an AU harness runner and every international thoroughbred have `horse_ref` and nothing
else.

`country` splits the feed: `country=AU` and `country=NZ` are the two-URL form of an AU/NZ
split. NZ rows come from the bookmaker results feed, which publishes placegetters, so they
carry `placings` without the full `runners` finishing order the Racing Australia collector
adds to AU thoroughbred results.

`dividends` carries the declared win/place dividends per placegetter per product and the
exotic dividends (quinella, exacta, trifecta, first four), each as a return on a $1 stake.
Exotics are declared a few minutes after the placings, so they land on a later poll than the
result itself — see the field description for the full shape.

`market_closed_at` and `inplay_at` are the race lifecycle, captured live and kept here
permanently: when the market was first seen shut, and when the race was first seen in-play —
the actual off, where `start_time` is the advertised one and moves. Both are POLL-OBSERVED
(polling passes are most frequent in the minutes before the jump), so each is the first
poll at which the state was seen rather than the exact instant, and both are SET ONCE.
They are copied from
the live race row when the result is written. That row is deleted 10 minutes after the jump
and results land a median 4.9 minutes after the start (p90 7.4), so a late result keeps
null. Null never means the market stayed open. Capture began 2026-09-01, so every result
before that date carries null.

**Who reports market state, and what you see.** Two feeds do. PointsBet closes its book at
the real jump — measured 2026-09-03 over 9 live races followed across their jump, 0.2 to
1.4 minutes AFTER the advertised start on AU/NZ cards — and that is what populates `market_closed_at` for most races. The
Betfair Exchange reports state while it quotes the market, which ends about two minutes
BEFORE the jump (measured 2026-09-01 on Horsham R3, advertised 05:24 UTC: betfair last seen
119s before the off), which is why results between 2026-09-01 and 2026-09-03 carry null.
Exchange-sourced market state is withheld from customer plans pending a data licence, so a
race whose state only the exchange saw reads null for customers; bookmaker-sourced state is
served to everybody. `market_state_source` names the feed behind the values you receive.
`inplay_at` stays exchange-only — bookmakers do not run racing in-play.

**Coverage:** results collection began 2026-08-15, so `hours_back` cannot reach further than
that no matter what you pass. Every row carries `coverage_from`, the earliest result
currently retained, so an empty window is distinguishable from an unsupported one.

**Field coverage, which is a different question, and `runners: null` is not its answer.**
Every row now carries `field_status`, `field_source` and `field_note`. `field_status` is
`complete` | `pending` | `overdue` | `unavailable` | `unsupported` | `not_applicable`: a
field that is here; one the pass that NORMALLY fills this race has not reached yet, with
that instant in `field_note`; one where that pass has been and gone without writing a
field but the collector still re-attempts the row; one no scheduled pass still selects,
its collector's own candidate window having closed; a code no collector has ever existed
for; and a race that did not run. The scheduled progression is pending -> overdue ->
unavailable, one direction only, and every step is bounded by a collector's real candidate
query rather than by its next run — so nothing here asks you to poll forever. Read
`overdue` carefully: it is NOT terminal. Most rows that reach it stay null, so do not build
on a field arriving, but do not write the race off either. And any of those three can
become `complete`, `unavailable` INCLUDED — nothing un-writes a `runners` array, so a
hand-run collector, a widened window or a late upstream publication can still fill a row we
have stopped scheduling passes for. Treat `unavailable` as 'nothing further is scheduled',
never as 'this can never arrive', and re-read rather than caching it as final. The full
per-state contract is on the `field_status` field itself. AU thoroughbred fields come from
Racing Australia and AU greyhound fields from our own collection; AU harness, NZ
thoroughbred and NZ harness fields have come from TAB New Zealand since 2026-10-04
(`field_source: tabnz`, hourly, seven-day window). An entry carrying `origin: board` is a
copy of the pre-race board (driver, trainer, barrier, form, positions for placegetters
only, no times, SP or margins), not an official finishing field. Collection floors are the
2026-08-21 Sydney card for AU greyhound and the 2026-09-01 card for AU thoroughbred; no
collector pass reaches back beyond its own floor, so a race older than one carries
`unsupported` and will not fill.
`runners` is JSON null in every one of these cases and has never been an empty array (0 in
the whole store), so `runners is None` is the complete test. GET
/v1/racing/results/coverage answers the same question per Sydney day, category and venue
for 1 credit, so you can see what a day holds without paging its results.

`going` and `going_rating` (2026-09-10) are `track_condition` split into the vocabulary
/v1/racing/horses/form already uses — 'Good (4)' becomes `going: "Good"`,
`going_rating: 4` — parsed by the one shared helper so the two surfaces cannot disagree.
`track_condition` is unchanged and is still the raw string the source published.

## Query parameters

- `hours_back` integer: How far back to look, in hours (max 7 days)
- `date` string, nullable: Single-day alias: date=YYYY-MM-DD returns the races that start on that calendar date in Sydney time (AET), instead of a rolling hours_back window (YYYYMMDD is read the same way). It is the same meeting_date_aet that /v1/racing/closing-lines stores, so the two join on date, venue and race_number. Each race belongs to exactly one date: a race that starts after midnight Sydney time (the last races of a Perth night meeting, British and Irish afternoon and evening racing) is under the next date. Mutually exclusive with hours_back. Also accepted by /v1/racing/closing-lines and /v1/racing/acceptances. NOT by /v1/racing/events, which is forward-looking and takes hours_ahead.
- `categories` string: horse,greyhound,harness or omit for all
- `category` string: Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones.
- `venue` string: Case-insensitive exact venue match, comma-separated for several, e.g. 'Rosehill' or 'Randwick,Eagle Farm,Flemington' — the same spelling /v1/racing/next-to-go takes
- `country` string: ISO country codes, comma-separated, case-insensitive (e.g. AU or AU,NZ). Matched against the result's `country`. Which countries have results at all is published by /v1/racing/results/coverage and the results_scope note on /v1/racing/closing-lines/coverage; GB and IE thoroughbred results come from Betway's settlement feed from 2026-10-06, GB and IE greyhound results from PointsBet's settled race card from 2026-10-12, and ZA thoroughbred results from Easybet's settled race card from 2026-10-15 (the placed runners ranked, `sp` null because the feed publishes no official starting price); a country with prices but no results source returns no rows here. A result whose country was never resolved (null) is excluded by any value here, so omit the filter if you need those rows.
- `status` string: Filter to 'final', 'interim' or 'abandoned'
- `limit` integer: Rows per page, newest first, at most 200. A larger value is served as 200 rather than refused, and every response carries `X-Limit-Applied`, the page size actually used: page with offset += that value, and read only a page shorter than it as the last.
- `offset` integer

## Headers

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

## Response `200`

Successful Response

- RaceResultOut[]
  - `race_id` string, nullable: Race id, joinable to /v1/racing/price-history and (while the race is still open) /v1/racing/next-to-go. Null if the race cannot be resolved in the price-history store.
  - `coverage_from` string, nullable: Start time of the earliest result currently retained.
  - `venue` string, nullable, required
  - `venue_id` string, nullable
  - `venue_canonical` string, nullable
  - `venue_site` string, nullable
  - `race_number` integer, nullable, required
  - `category` string, nullable, required
  - `country` string, nullable, required
  - `start_time` string, required
  - `race_name` string, nullable
  - `distance_m` integer, nullable
  - `track_condition` string, nullable
  - `going` string, nullable: Track condition word, normalised: Good, Soft, Heavy, Firm, Fast, Synthetic. Parsed from `track_condition` with the same helper /v1/racing/horses/form uses, so the two surfaces agree. Null when `track_condition` is null or unparseable; `track_condition` remains the raw string exactly as the source published it.
  - `going_rating` integer, nullable: Numeric going rating 1-12 (the 4 in 'Good (4)'), or null. NULL IS THE MAJORITY CASE, not an exception for two rare words: most stored conditions are a bare word carrying no number at all. Measured 2026-09-10 across all 5,579 stored results, a bare 'Good' is 3,954 rows (71%) and yields null, so a rating is present on roughly 29% of rows — greyhound and other non-thoroughbred meetings publish an unrated word as a rule. A rating is never guessed from the word ('Synthetic', 67 rows, and 'Firm', 0 rows, never carry one). Separately, 205 rows store the four-character STRING 'None' in `track_condition`: `going` and `going_rating` are both null on those, while `track_condition` still serves that literal string, because `track_condition` is the raw source value passed through untouched.
  - `weather` string, nullable
  - `status` string, required: interim | final | abandoned. `interim` until the placings have been stable for 30 minutes, then `final`. `abandoned` (2026-09-06) is a race that did NOT run: Racing Australia's official Abandoned & Transferred Meetings notice for that venue and date, applied to every race of the meeting — placings are empty and runners null. Settle only against `final`; treat `abandoned` as a void for settlement purposes.
  - `status_note` string, nullable: Only on `abandoned` rows: RA's own word for the non-run — "abandoned", "postponed", or "transferred to <venue>". A postponed or transferred meeting did not run at THIS venue on THIS date; if it ran elsewhere or later it appears as its own result.
  - `placings` object[]: The placegetters: {position, number, name, win_price, place_price, sp, sp_note, horse_ref}. `win_price` and `place_price` are the SETTLING BOOKMAKER'S FIXED prices. `horse_ref` (added 2026-09-21) is the same `pe:<name>` horse identity the `runners` entries carry, derived at serve time from the name on the placing and present on every placing — null off thoroughbred. It is the key that joins this placing to the same horse on another meeting; `runner_ref` on the matching `runners` entry joins the rows about this race and will not, because Racing Australia issues a new code for the same horse each day. `sp` is the OFFICIAL STARTING PRICE as a decimal return on a $1 stake (4.6 means $4.60), null where it is unknown — never 0. It is the settling market's own price, not our consensus and not a bookmaker's fixed price: thoroughbred SP comes from the Racing Australia results table, greyhound SP from our own collection of published results (Victorian tracks via FastTrack, every other state via the national results pages, both from 2026-09-13), and HARNESS SP IS ALWAYS NULL — no source we hold publishes it. `sp_note` carries Racing Australia's favouritism marker on thoroughbred runners ('F' favourite, 'EF' equal favourite) and is null everywhere else. Coverage RE-MEASURED 2026-09-08 over the whole retained store, counting only rows whose `status` is `ran` (a barrier scratching is not a starter and its price is not in this denominator): 88.6% of thoroughbred starters carry `sp` (2,105 of 2,375); on the greyhound races collected since 2026-09-13, 2,054 of 2,077 Victorian starters do (98.9%) and 4233 of 4596 of the other states' starters do (92.1%), both measured 2026-09-20. The Racing Australia block that held thoroughbred SP at 0% when this was last measured on 2026-09-03 has lifted and the collector has been landing meetings since. A null `sp` is unknown, never zero — RA prints no SP at all on some rows. On placings, `sp` is copied from the matching `runners` entry when the response is built, so the two can never disagree. On a race with no `runners` payload the `sp` and `sp_note` KEYS ARE ABSENT from each placing — not present and null. The copy step is what creates them and it is skipped when there is nothing to copy from, so a schema-validating consumer sees a different object shape, not a null: read them with `.get()`. The keys are deliberately NOT normalised in, because adding them here would change the shipped payload of every COMPLETE race too, and nothing may move under a customer mid-integration. `field_status` tells you which case you are in before you look.
  - `runners` object[], nullable: One entry per runner: {number, box, name, position, status, abnormal, margin, time_s, dead_heat}. Presence in this list is NOT proof the runner started. `status` is what says so, and `status` is what you settle on. ⚠️ COVERAGE. Read `field_status` on the row FIRST — it is the per-race answer, and it is what separates a field that has not been filled yet from one that never will be. Two different facts live here. (1) WHETHER THERE IS A FIELD AT ALL. Measured 2026-09-10 across the whole retained store — 5,577 results, 2,902 of them serving `runners: null`: AU greyhound carried a field on 2,360 of 3,219 races, AU thoroughbred on 315 of 1,104, and AU harness, NZ thoroughbred and NZ harness on none, as no collector covered those three codes then. Since 2026-10-04 TAB New Zealand fills them (`field_source: tabnz`, hourly, seven-day window) and has replaced the board copies described next wherever it could match the race; /v1/racing/results/coverage gives the current share per day, code and venue. FROM 2026-09-29 harness and NZ fields come from the pre-race board (origin: board): driver, trainer, barrier, form; positions only for placegetters; no times, SP or margins. Such a field is copied from the live board when the result is first written, before the board is purged 10 minutes after the jump, so a race whose result landed after the purge still has none. On these entries `"origin": "board"` is set on every entry, `box` holds the barrier (as on thoroughbred entries), `jockey` is the harness DRIVER, `weight_kg` and `form` are what the board showed, `status` is `scratched` for a runner the race's scratchings or `deductions` name and `ran` otherwise, and `position` is set for placegetters ONLY — a null `position` on an `origin: board` entry means 'not a placegetter, or not known', never 'did not finish'. Inside the two supported codes it is not a clean ramp either: each is filled by a scheduled pass, and where that pass cannot match a race with certainty it writes nothing rather than another race's field, so past the fill window 104 of 2,201 AU greyhound races (4.7%) and 44 of 288 AU thoroughbred races (15.3%) keep `runners: null` for good — `field_status: unavailable`, and polling them will not help. `runners` is JSON null in every one of these cases and has never once been an empty array (0 empty arrays in the whole store, measured the same day), so `runners is None` is a safe test and an empty field is not a shape you have to handle. (2) THE SHAPE OF A FIELD THAT IS PRESENT, which is where the 2026-09-08 date belongs. An AU thoroughbred field WRITTEN from 2026-09-08 carries every starter plus every runner Racing Australia printed as scratched; one written before that date carries STARTERS ONLY, because the collector dropped RA's scratched rows and a stored field is never rewritten. Measured 2026-09-08, all 244 thoroughbred fields stored at that point were the old shape and 198 of them publish a `deductions` row for a horse absent from `runners`. So on an older race `deductions` is the list of scratchings and `runners` is the starters; do not read a short `runners` array as a small field. Greyhound fields have carried the whole field since greyhound collection began on the 2026-08-21 Sydney card — but 859 AU greyhound races carry no field at all, which is question (1) and not this one. `position` is the finishing position for every runner that completed the race — not just the placegetters — and is null whenever there is none: the runner did not start, or it started and did not finish (except on an `origin: board` entry, which carries placegetters' positions only — see above). `status` is ran | scratched | late_scratching | reserve (an emergency that never got a start). `abnormal` is the official reason cell, passed through VERBATIM from the source — on AU thoroughbred it is Racing Australia's own Finish cell whenever that cell is not a number. THERE IS NO CLOSED LIST OF CODES and you should not write a match statement that assumes one: the source invents them, and a code nobody here has seen before will reach you unchanged rather than be dropped or renamed. Everything stored to date, with counts as at 2026-09-14: Fell (101), TailedOff (32), SB (25), PulledUp (8), FF (1, Rosehill R4 on 2026-09-12), LP (1). Treat `abnormal` as the authoritative explanation for a missing `position`, and treat an unfamiliar value as a did-not-finish unless `status` says otherwise — which is exactly what this API does: only codes PROVEN to mean the runner never left the barrier flip `status` to `scratched`, and that list currently holds one entry, 'SB'. Anything else stays `ran`, deliberately, because misreading a starter as a non-starter turns a losing bet into a refund. It carries TWO families that settle in OPPOSITE directions, so read it with `status` and never alone. (1) A DID-NOT-FINISH reason on a row whose `status` is `ran` — Fell, PulledUp, Disqualified, TailedOff, StayedInBox, FF and LP are the ones seen so far: the runner started and simply has no finishing position, and a bet on it lost. (2) A DID-NOT-START reason on a row whose `status` is `scratched` — 'SB', Racing Australia's code for scratched at the barrier: the runner was taken out of the market before the jump, so `position` is null, it is in no `placings` entry, and bets on it were refunded while the rest of the race was deducted. An 'SB' row may still carry `sp` — the last price Racing Australia printed for it, kept rather than blanked because it is a real market fact about the withdrawn runner. It is NOT the basis of the deduction, and you must never derive one from the other: `sp` comes from RA's results table while a deduction is struck by the settling bookmaker, and they do not agree. Measured 2026-09-08 across all five stored 'SB' rows that carry a price, 1/`sp` equals the published win deduction on NONE of them — Varejao, Sunshine Coast R4 2026-09-06, saddlecloth 5, withdrawn about 2 minutes 20 before the jump: `sp` 3.70, so 1/3.70 is 0.27 in the dollar, against a published `deductions` row of 0.18 win and 0.15 place; Dubai Dancer, Warrnambool R5: 1/5.50 is 0.18 against a published 0.10. Settle from `deductions`, which is authoritative. None of the five carries a `position` or a `margin`. Telling a barrier scratching from one made days earlier is CODE-SPECIFIC, and the two collectors do not spell it the same way — do not write one rule across categories. On AU thoroughbred (Racing Australia) both are `status: scratched`, and `abnormal` is the discriminator ONLY in one direction: a barrier scratching carries 'SB'; every other scratching carries `abnormal` null. Both carry their saddlecloth `number`, both carry `box` null (the runner has no barrier it jumped from — RA prints 0 there and we do not pass that sentinel on), and an ordinary scratching carries `sp` null while an 'SB' may carry one. So `abnormal == 'SB'` identifies a barrier scratching; `abnormal` null tells you only that it was not one, not how early it happened — use `deductions[].scratched_at` for that. On greyhound (our own collection since 2026-09-13: FastTrack for Victorian tracks, the national results pages for every other state) `abnormal` is null on every scratching and `status` carries three values: `ran`, `scratched` (merged in from our own `deductions`, so it never says how late), and `reserve` for a rug 9/10 reserve that never got a box. `late_scratching` was the retired interim feed's greyhound-only value — a thoroughbred barrier scratching does NOT use it — and it is no longer emitted; `scratched` does not carry identical meaning in the two codes. `deductions` carries `scratched_at` for either, and is the one cross-code way to see how close to the jump a runner came out. `dead_heat` is true on each runner sharing a position. IDENTITY. `runner_ref` (`ra:<code>`) is the code Racing Australia published for this runner on race day. It is the same value on the closing-line, price-path and markets archives, on the live board on race day, and on /v1/racing/acceptances once that day's field has been read, so it joins this runner across every endpoint for this race. Racing Australia issues a new code for the same horse each day, so it is not a horse identity: the same horse carries a different `ra:` code at each meeting. `horse_ref` (`pe:<name>`) is: the registered name folded to letters and digits (trailing country parenthetical off), the same value on results, the live board, the closing-line and price-path archives, /v1/racing/horses/form, /v1/racing/horses/runs and the backfill, and it is the key to join a horse across meetings. It is derived at serve time from the name on the row, never stored, so every historical row carries it. On every racing code since 2026-10-16 — greyhound and harness runners carried a null before that and now carry the same `pe:<name>` fold, which for them is the only identity that joins a runner to itself across meetings. On GREYHOUNDS `runner_ref` IS the dog: `grv:<id>` for Victorian tracks as FastTrack numbers the dog and `grsa:<id>` elsewhere as the national results pages number it — per-dog registry ids that DO persist across meetings, in two namespaces, so a dog racing in both is two refs until mapped. `runner_ref` is null while a code's registry source is not yet wired. `trainer` and `jockey` come from the same official results source and may be null where that source is not yet live for the code. GREYHOUND-ONLY, ADDITIVE FROM 2026-09-21, five measured columns the results pages print. `first_split_s` is the first sectional in seconds exactly as the source page printed it, null where the page printed none. `second_split_s` is the second sectional, and is null on EVERY Victorian run because FastTrack publishes one split and not two. `weight_kg` is the greyhound's race-day weight in kilograms, null where the page carries no weight column at all — Western Australian meetings carry none (0 of 36 runner rows sampled 2026-09-21). `pir` is position-in-running text passed through as a STRING and never a number ("21" means 2nd at the first mark then 1st at the second): FastTrack's own column on Victorian runs and, since 2026-09-29, the national page's In Race Positions card elsewhere, which only some tracks print (see /v1/racing/greyhounds/form `pir` for the per-state rates) — the marks are each site's own, so compare PIR within one source; null where the page printed none. `weather` (added 2026-09-29) is the meeting's weather in the national page's OWN words ("Fine", "Light showers", "Overcast"), stamped on every entry of the field exactly as `grade` is; null on Victorian (FastTrack) fields, which print none, and ABSENT on fields written before it existed — measured 2026-09-29, 170 of the 2,012 stored greyhound fields carry it, from the 2026-09-27 card on. It is not on /v1/racing/greyhounds/form. `grade` is the race's grade in the SOURCE'S OWN WORDS — "M", "4/5" or "FFA" on the national pages, a race type such as "Tier 3 - Maiden" on FastTrack — and the two vocabularies are NOT mapped onto each other, so do not compare a Victorian grade with an interstate one. ⚠️ A STORED FIELD IS NEVER REWRITTEN, so a greyhound field written before 2026-09-21 carries none of these five keys AT ALL — absent, not null — and no thoroughbred field carries them in any era: read them with `.get()`. Measured on 46 freshly read race pages on 2026-09-21, a grade appeared on 46/46 races and 362/362 runner rows, a first split on 288/362 and a weight on 295/362; the rows without them are overwhelmingly rug 9/10 reserves, which never started and for which the page prints nothing. `sp` is the OFFICIAL STARTING PRICE as a decimal return on a $1 stake (4.6 means $4.60), null where it is unknown — never 0. It is the settling market's own price, not our consensus and not a bookmaker's fixed price: thoroughbred SP comes from the Racing Australia results table, greyhound SP from our own collection of published results (Victorian tracks via FastTrack, every other state via the national results pages, both from 2026-09-13), and HARNESS SP IS ALWAYS NULL — no source we hold publishes it. `sp_note` carries Racing Australia's favouritism marker on thoroughbred runners ('F' favourite, 'EF' equal favourite) and is null everywhere else. Coverage RE-MEASURED 2026-09-08 over the whole retained store, counting only rows whose `status` is `ran` (a barrier scratching is not a starter and its price is not in this denominator): 88.6% of thoroughbred starters carry `sp` (2,105 of 2,375); on the greyhound races collected since 2026-09-13, 2,054 of 2,077 Victorian starters do (98.9%) and 4233 of 4596 of the other states' starters do (92.1%), both measured 2026-09-20. The Racing Australia block that held thoroughbred SP at 0% when this was last measured on 2026-09-03 has lifted and the collector has been landing meetings since. A null `sp` is unknown, never zero — RA prints no SP at all on some rows. Null `runners` means the full field is not available for this race — never an empty field; every AU and NZ code carries one (AU thoroughbred, AU greyhound, AU harness, NZ harness, NZ thoroughbred). `placings` remains the source for the settling bookmaker's own fixed prices, and now also carries `sp` copied from here.
  - `field_status` string: Why `runners` is or is not there, per race — the field a caller was missing when `runners: null` was the only signal and it collapsed several different futures into one. SIX values. `complete`: `runners` is present. It is set whenever a field is stored, including on an abandoned race that unexpectedly carries one, so `field_status == 'complete'` is a safe gate before parsing. An AU harness result settled between 2026-09-29 and 2026-10-04 that the TAB New Zealand pass could not match may still hold the PRE-RACE BOARD copy — every entry `origin: board`, `field_source` null, positions for placegetters only and no times, SP or margins — so check `origin` before treating it like a collected field. On GB and IE thoroughbred (from 2026-10-06) `complete` means Betway's field (`field_source: betway_uk`, every entry `origin: betway_uk`): every runner with its starting price in `sp` and `sp_fraction` and a `position` wherever Betway publishes one — every finisher on the British races measured; on the Irish ones Betway ranks the first four first and the rest of the order within about 25 minutes of the off, which the interim result picks up, so until then an unplaced Irish starter is `ran` with `position` null — and non-runners as `scratched`; no margins, times, barriers, jockeys or dividends, and no Rule 4 figure on the deductions. On AU harness, NZ harness and NZ thoroughbred (from 2026-10-04) `complete` means the TAB New Zealand field (`field_source: tabnz`, every entry `origin: tabnz`): the full finishing order with margins in lengths, each runner's time (NZ racing only — TAB New Zealand publishes no times on Australian races), barrier and gate, driver in `jockey` (gallops add `weight_kg`, the total carried), trainer, scratchings with `scratched_at`, and TAB New Zealand's own pool win and place dividends on the placegetters; `sp` is null because TAB NZ publishes no official starting price. A board copy written before 2026-10-04 is replaced by the first pass. On ZA thoroughbred (from 2026-10-15) `complete` means the Ambernet settled race card (`field_source: easybet_za`; each entry's `origin` is the book it was read from, easybet_za or topbet_za): every runner, with a `position` on the runners the book ranks (the placed runners; unplaced starters stay `ran` with `position` null), jockey and trainer, non-runners as `scratched`; no margins, times, SP or dividends, and no deduction figure on the deductions. `pending`: a collector exists for this code and the pass that NORMALLY fills this race has not been yet; `field_note` names the UTC instant by which it will have run. `overdue`: that pass has been and gone without writing a field, but the collector still selects this row as a candidate and keeps re-attempting it — this state is NOT terminal. For AU greyhound the hourly pass for its state (FastTrack for Victoria, the national results pages elsewhere) re-reads every field-less race inside its seven-day window, and so does the hourly TAB New Zealand pass for AU harness, NZ harness and NZ thoroughbred; for AU thoroughbred a bounded re-fetch reaches six days but is run by hand rather than on a schedule. Most rows that reach `overdue` stay null, so do not build on a field arriving — but do not write the race off either, and `field_note` gives the horizon. `unavailable`: no scheduled pass still selects this row. Its collector's own candidate window closed at the instant in `field_note` and no field was written — the pass either never matched the race or refused an ambiguous match, which it does rather than write another race's field. `unsupported`: no collector exists for this category/country. Since 2026-10-04 that is only a foreign race that appears because an AU book listed the meeting: AU harness, NZ harness and NZ thoroughbred (1,254 of the 5,577 stored results on 2026-09-10) all gained their TAB New Zealand collector that day — and no amount of waiting will produce one. `not_applicable`: the race did not run (abandoned, postponed or transferred) and carries no field, so there is nothing to collect. TRANSITIONS, stated explicitly because acting on the wrong one costs you data. The scheduled progression is pending -> overdue -> unavailable, one direction only. It is bounded at every step by a collector's own window rather than by hope: a race can sit matched-and-never-resulted forever (The Gardens, 2026-09-05, 12 races), and an open-ended `pending` would have you polling it forever. Any of those three becomes `complete` the moment a field is written, AND THAT INCLUDES `unavailable` — nothing un-writes a `runners` array, so a collector re-run by hand, a widened window or a late upstream publication can still fill a row we have stopped scheduling passes for. So `unavailable` means 'nothing further is scheduled', never 'this can never arrive'; if you reconcile, re-read rather than caching it as final. `unsupported` and `not_applicable` are properties of the racing code and of the race itself, not of a schedule, and do not change on their own.
  - `field_source` string, nullable: Which collector owns `runners` for this race: `racing_australia` (AU thoroughbred), `fasttrack` (AU greyhound at Victorian tracks) and `grsa` (AU greyhound everywhere else), the last two our own collection, or null where no collector exists — including a `complete` AU harness row settled between 2026-09-29 and 2026-10-04 whose `runners` is still the pre-race board copy (`origin: board`), which no collector owns; `tabnz` (TAB New Zealand) owns AU harness, NZ harness and NZ thoroughbred; `betway_uk` (Betway's settlement feed) owns GB and IE thoroughbred from 2026-10-06; `pointsbetau` (PointsBet's settled race card) owns GB and IE greyhound from 2026-10-12. Set on `pending`, `overdue`, `unavailable` and `complete` rows alike — it names who WOULD fill the field, not who did.
  - `field_note` string, nullable: Prose for the non-`complete` states: which pass fills this code, when its normal pass for this race has been and gone, how long the collector keeps re-attempting the row, or that the code has no source at all. Null on `complete`. Written for a human reading a response — branch on `field_status`, never on this text.
  - `completeness` object, nullable: Added 2026-10-20, additive. Result and field completeness in one object, so a caller does not have to combine `status` and `field_status` itself. `state` is one of four values: `provisional` (the result is not final yet), `final_placings` (final placings, and dividends where the settling book declares them, but no full field — `field_status` says whether one is still coming), `complete_full_field` (final, and `runners` holds the whole field) and `not_run` (abandoned, postponed or transferred). `result_final` is `status == 'final'`; `result_source` names the feed that settled the race (`betright`, `pointsbetau`, `betway_uk`, `ambernet_za`, `racing_australia` for an abandonment, or null when unknown). For a race whose field is not in yet: `field_pending` / `field_overdue` mirror `field_status`, `field_due_by` is when the pass that normally fills it will have run, `overdue_since` is that same instant once it has passed, `retrying` is true while a scheduled pass still re-attempts the row and `retry_until` is the collector's retry horizon. `source_refused_since` (UTC) is set while the field's source is refusing our collector for this race (FastTrack for Victorian greyhounds since 2026-10-13). `last_successful_collection_at` is the field collector's latest served run where the collector records one (FastTrack only; null for every other source). The four `state` values are counted per lane by /v1/racing/results/coverage (`result_completeness`). Read it with `.get()`.
  - `deductions` object[]: Scratched runners and the deduction applied to bets already struck. `win` and `place` are fractions of the dollar in the range 0-1 (0.34 = 34 cents in the dollar), as published by the settling bookmaker.
  - `dividends` object, nullable: Dividends declared for the race. `straight` holds one row per placegetter per product: {position, number, name, market ('WIN' or 'PLC'), product, amount}. `exotics` holds one row per exotic product: {type, code ('QN', 'EX', 'TF', 'FF'), selection, product, amount}, where `selection` is the winning saddlecloth numbers in finishing order, e.g. '5,1'. Every `amount` is the return on a $1 stake, the same convention as the odds fields — a 'FIXED' straight dividend equals that runner's own settled fixed price. `product` names the settling pool: 'FIXED' is the bookmaker's own price, the rest are tote products (SP, MIDDIV, VIC, RD+, BT+SP). A product that declared no dividend is omitted rather than returned as 0. Exotic dividends are only declared once the race fully settles, a few minutes after the placings, so a just-resulted race can carry `straight` while `exotics` is still empty — it fills in on a later poll. `multi_race` (from 2026-09-29) holds the meeting-level products — Quaddie, Giant 6, Daily Double and the like — on the race that is the product's FINAL leg and on no other race: one row per declared pool, {code (the book's own, e.g. 'RC-QUADDIE'), type ('Quaddie'), product ('VIC', 'BT', ...), selection (the winning saddlecloth in each leg, in leg order, e.g. '1,4,2,11'), amount (the return on $1), legs (the race numbers the product spans, e.g. [7, 8, 9, 10])}. Each row is published as the settling book declares it: Rosehill R10 on 2026-09-26 closed the Quaddie (R7-R10, $128.40 VIC and BT), a Giant 6 (R5-R10, $544.40 VIC) and a second Giant 6 row whose selection names five legs, at $1.00. A result stored before 2026-09-29 carries NO `multi_race` key at all — read it with `.get()` — and on a newer row it is a list, empty when the race closes no product. `straight_types` is the book's raw list of product codes, retained for backwards compatibility. `note` (rare, from 2026-10-03) is a plain-text caveat on the dividends, absent on almost every race: e.g. Randwick R1 on 2026-10-03, where the settling book paid on the first horse past the post and a protest then changed the official placings, so these dividends name a different winner from `placings`. Read it with `.get()`.
  - `sectionals` object, nullable: NSW harness sectional times, or null. Present on AU harness races at NSW tracks once Harness Racing NSW has published the meeting's Sectional Times sheet, usually a few days after the meeting, and it has been matched to this race by venue, Sydney date, race number and the winner's saddlecloth. Race level: `gross_time_s` (the winning time), `mile_rate_s`, `lead_time_s` (null where none was taken), `quarters_s` (the leader's four quarters, in order), `distance_m`. `runners` lists every starter by saddlecloth `number` with `name`, `position` (the official placing; dead heats share it and set `dead_heat`), `margin_m` (metres behind the winner), `time_s` (the runner's own time), `behind_800m_m` and `wide_800m`, `behind_400m_m` and `wide_400m` (metres behind the leader and how many horses wide, at the 800 m and the 400 m), and `q3_s` and `q4_s` (the runner's own third and fourth quarters). `source` is `hrnsw` and `source_url` is the sheet it was read from. Times are in seconds and distances in metres. Read it with `.get()`.
  - `resulted_at` string, nullable
  - `market_closed_at` string, nullable: UTC time at which the market was FIRST seen shut — the book stopped taking bets on this race. Poll-observed: this is the first poll that saw the state, not the exact instant, so it lands up to one polling pass after it, and passes are most frequent in the minutes before the jump. Set once: a market that flickers SUSPENDED -> OPEN -> SUSPENDED keeps the FIRST suspension. TWO SOURCES report market state. PointsBet closes its book at the real jump — measured 2026-09-03 over 9 live races followed across their jump, 0.2 to 1.4 minutes AFTER the advertised start on AU/NZ cards — and the Betfair Exchange reports it while it quotes the market, which is until about two minutes before the jump. `market_state_source` names the one that produced this value. Exchange-sourced stamps are withheld from customer plans, so a race seen shut only by the exchange reads null. Bookmaker-sourced stamps are served to everybody. Null also means no feed observed a transition — it never means the market stayed open. Copied from the live race row when the result is written; that row is deleted 10 minutes after the jump and results land a median 4.9 minutes after the start, so a late result keeps null.
  - `inplay_at` string, nullable: UTC time at which the race was FIRST seen in-play — the actual off, as opposed to `start_time`, which is the ADVERTISED off and moves. Same poll-observed, set-once semantics as `market_closed_at`, and withheld on the same source rule. Reported by the Betfair Exchange only: bookmakers do not run racing in-play, so a bookmaker feed says when it CLOSED its book, not when the race went. For a race the exchange has stopped quoting, `market_closed_at` from a bookmaker is the closest measured instant to the off. Copied from the live race row when the result is written; that row is deleted 10 minutes after the jump and results land a median 4.9 minutes after the start, so a late result keeps null.
  - `market_state_source` string, nullable: Which feed's market state produced `market_closed_at` / `inplay_at` on this race — 'pointsbetau' for the bookmaker, 'betfair_ex_au' for the exchange. Set once, alongside the first stamp. Null when no transition has been observed, and null for customer plans whenever the stamps themselves are withheld, so a non-null value here is always the provenance of values you can see.

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