API reference / Racing Archive
GET /v1/racing/closing-linesPermanent closing-line and result archive
The permanent closing-line archive: one row per race, runner and bookmaker, carrying the last price seen before the jump, the first price seen after the market opened, and the finishing position where one is known. **Plans.** Plus and above read the whole archive back to its first race; `window_days` comes back null for them. Standard reads a rolling 90-day window: `window_days` is 90, the response carries `X-Archive-Window-Days`, and a range entirely older than the window is a free 403 that says so. Free and Hobby get a free 403 that carries a `sample`: five of this endpoint's rows, for one recent race. Standard keys issued before 2026-09-20 keep the whole archive they were sold; the `window_days` field tells you which case yours is. Legacy `starter`, `pro` and `growth` keys keep their access but cannot be bought. **This is not the same data as `/v1/racing/price-history`.** That endpoint reads the live 45-day snapshot store and returns every price move for one race. This one reads a separate, permanent table and returns one collapsed row per series across any date range. Price history disappears at 45 days; the archive does not. **Read the flags before you model on this.** Measured 2026-08-18 over 348,889 archived series: * `is_closing_line` — only 85.5% of series have their final observation within 300s of the jump. The rest stopped being quoted early and their close is a last-seen price. Filtered in by default. * `open_is_baseline` — only 42.5% of series start at the 60-minute window entry. The rest joined mid-window, so their "open" is not a market open. * `finish_position` **NULL does not mean the runner lost.** Harness and NZ results name the placegetters only, so most runners in a resulted race there have no published position. AU thoroughbred and greyhound results carry the whole field, and there every finisher has its position (5th, 6th, …) — applied back to 2026-09-14 on 2026-09-29; before that only 1-4 were stored. Check `result_status`: NULL there means the race has no result at all. * `venue_split_suspect` / `name_fragment_suspect` — the archive's record of its own known contamination. Excluded by default. * `scratched` — the result marked this runner scratched, yet the row holds a price: four books (Betfair, Sportsbet, Ladbrokes, Neds) keep quoting a scratched runner and the archive kept their last price as its "close". Excluded by default since 2026-09-28 (`include_scratched=true` to see them). Only AU thoroughbred and greyhound results name their scratchings, so on harness and NZ rows false means not known, never ran. **Three columns appended LAST on 2026-09-29, after `scratched`** — every earlier column keeps its position: * `open_place_price` / `close_place_price` — the book's PLACE price at the first and the last observation of the series. NULL on rows archived before 2026-09-29 (the capture began storing place that day) and at books that quote no place price (Betfair, and any book that prices win only). The place price is recorded at the moments the WIN price moved, not at every place move, so a close_place_price can be older than the book's final place price by up to the time since its last win move. * `scratched_at` — when the runner was scratched, as the scratching book stamped it on the live board, captured at the jump from 2026-09-29. NULL for a runner that ran, for a scratching the board carried without a time, and for every race before that date. **Six Top-N columns appended LAST on 2026-10-05, after `scratched_at`:** `open_top2_price`, `open_top3_price`, `open_top4_price`, `close_top2_price`, `close_top3_price`, `close_top4_price` — the book's Same Race Multi Top 2 / Top 3 / Top 4 price at the first and the last observation of the series. Only **sportsbet, ladbrokes_au, neds and pointsbetau** publish these legs, so they are NULL at every other book; NULL on every race before 2026-09-29, when the capture began holding them. Like the place price, they are recorded at the moments the WIN price moved, not at every Top-N move. **Five consensus columns appended LAST on 2026-10-05, after `close_top4_price`:** `consensus_close_prob`, `consensus_close_price`, `books_in_consensus_close`, `consensus_open_price`, `books_in_consensus_open` — the market's margin-free probability for the runner at the close, the price it corresponds to, and how many books it rests on, plus the same price at the 60-minute open. The arithmetic is GET /v1/racing/consensus's default (power method): each included book's implied probabilities raised to the exponent that makes them sum to one, the median across books per runner, the field scaled to one. The archive is one method throughout — the rows written under the multiplicative default on 5 October 2026 were recomputed when power became the default on 6 October. A book is included at the close when its row is a genuine closing line (`is_closing_line`), it prices the complete field, it is not withheld from customers and the runner was not scratched; placeholder quotes leave on the best-odds rule. The open figure counts only series that began at the baseline (`open_is_baseline`), so it rests on fewer books and is NULL where fewer than two qualify. The value is the same on every bookmaker row of a runner, like `finish_position`, and it is recomputed by the hourly archiver whenever a row changes. It describes the market, not the outcome: it is what the panel implied, with margin removed, not a probability of winning. **Result coverage is thin and forward-growing.** The results feed began on 2026-08-15 and is AU/NZ only, so on 2026-08-18 only 5.3% of archived races carry a result. `resulted_rows` on the response is the measured count for your actual selection, not a marketing figure. **`runner_ref` joins a row to everything about its race; `horse_ref` joins a HORSE across meetings.** `runner_ref` (the column after `venue_site`, since 2026-09-05) is the registry id stored on the row — `ra:<horsecode>` for thoroughbreds, `grv:<dogId>` for greyhounds — the same value `/v1/racing/results` carries on every `runners[]` entry and `/v1/racing/markets` carries for the race, so an archive row joins to its result and its market rows without matching names. On a THOROUGHBRED it is the code Racing Australia published on race day; Racing Australia issues a new code for the same horse each day, so it changes from meeting to meeting and a season keyed on it splits into one group per start — `horse_ref` is the key for that. On greyhounds `grv:`/`grsa:` are per-dog registry ids and do persist. Filled from Racing Australia acceptances for AU thoroughbreds and from the result itself for every code once it lands, so it can be null on a row whose race has not resulted yet, on harness (no registry feed), and on rows archived before the identifiers existed (late August 2026). Appended after `venue_site` so positional CSV readers keep working. **`horse_ref`** (the column after `runner_ref`, added 2026-09-21) is the horse identity: `pe:<name>`, the registered name folded to letters and digits with any trailing country parenthetical removed. It is DERIVED at serve time from `runner_name`, not stored, so every row the archive has ever held carries it from today — no backfill, and nothing already exported moved: it is appended after `runner_ref`, exactly as `runner_ref` was appended after `venue_site`. It is the same value on `/v1/racing/results` runners and placings, the live board, `/v1/racing/horses/form`, `/v1/racing/horses/runs` and the form backfill, so a horse's whole price history across meetings is one `GROUP BY horse_ref`. Null on greyhound and harness rows, and on any row whose `runner_name` is missing. Betfair Exchange rows are withheld from customer responses pending a data licence, so `close_lay_price` — which only an exchange quotes — is NULL for customers. **Dates.** `from`/`to` bound the range. `date=YYYY-MM-DD` is a convenience alias for a single day and is exactly `from=YYYY-MM-DD&to=YYYY-MM-DD`; it exists because `/v1/racing/results` and `/v1/racing/acceptances` take `date` and callers reasonably expect the same spelling here. `/v1/racing/events` does NOT — it is forward-looking and takes `hours_ahead`. Passing `date` together with `from` or `to` is a free 422 rather than a silent precedence rule. An unknown bookmaker key, an unknown parameter, a reversed date range or a bad date is an error and costs nothing.
X-API-Key
5 credits (JSON) / 20 credits (CSV)
curl 'https://api.puntersedge.online/v1/racing/closing-lines?from=2026-08-10&to=2026-08-17&category=horse&format=csv' \
-H 'X-API-Key: YOUR_KEY'
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from |
query | string | no | ISO date/time on the race start. Defaults to the archive floor. |
to |
query | string | no | ISO date/time. Defaults to now. |
date |
query | string | no | Single-day alias: date=YYYY-MM-DD is exactly from=YYYY-MM-DD&to=YYYY-MM-DD. Mutually exclusive with from/to. Also accepted by /v1/racing/results. NOT by /v1/racing/events, which is forward-looking and takes hours_ahead — this line claimed it did until 2026-08-25, and that false claim is how a Racing subscriber learned to send date= to /results, where it was undeclared and silently dropped for 143 requests. |
venue |
query | string | no | Venue name, case-insensitive, exact match. |
bookmakers |
query | string | no | Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing. |
category |
query | string | no | horse, harness or greyhound — comma-separated for more than one. `categories=` is accepted as an alias (2026-09-24); an unknown value is a free 422. |
categories |
query | string | no | Alias of `category`. Comma-separated horse, harness, greyhound. Both spellings are accepted on every racing endpoint since 2026-09-24; before that, using one family's spelling on the other was a hard 422. |
country |
query | string | no | Two-letter country code, e.g. AU, NZ |
race_id |
query | string | no | Single race, joinable to /v1/racing/price-history while that race is still inside the 45-day snapshot window. |
closing_only |
query | boolean | no | Only rows whose last observation was within 300s of the jump. True by default because 14.5% of series are NOT closing lines and silently mixing them in is how a CLV study goes wrong. |
resulted_only |
query | boolean | no | Only rows from races that have a result. |
include_flagged |
query | boolean | no | Include rows flagged venue_split_suspect or name_fragment_suspect. False by default; set true if you want the contaminated rows and intend to handle them. |
include_scratched |
query | boolean | no | Include runners the result marked scratched. False by default: four books keep pricing a scratched runner, so the archive holds a "close" for a runner that never started (rows carry scratched=true). Only AU thoroughbred and greyhound results name their scratchings, so on harness and NZ rows scratched=false means not known, never ran. |
format |
query | string | no | csv streams a flat table with a stable column order — the format a modeller actually wants. |
limit |
query | integer | no | |
offset |
query | integer | no |
Status codes: 200, 401, 402, 422, 429, 500. Response bodies are JSON; the full schema is in /openapi.json.
200 application/json
— this call costs 5 credits (JSON) / 20 credits (CSV).
Field names and types are as the API returns them; values are a real sample, trimmed to a few items.
{
"rows_returned": 117,
"total_rows": 117,
"limit": 500,
"offset": 0,
"archive_from": "2026-08-04T09:22:00Z",
"window_days": null,
"resulted_rows": 117,
"rows": [
{
"race_id": "81db7342-2e44-482a-bf62-5d5166d764f5",
"start_time": "2026-09-29T03:30:00Z",
"meeting_date_aet": "2026-09-29",
"venue": "Tatura",
"race_number": 1,
"category": "horse",
"country": "AU",
"runner_key": "theshyster",
"runner_name": "The Shyster",
"runner_number": 8,
"bookmaker_key": "betgold",
"open_win_price": 2.45,
"open_secs_to_jump": 3597,
"open_is_baseline": true,
"close_win_price": 2.3,
"close_lay_price": null,
"close_secs_to_jump": 80,
"is_closing_line": true,
"points_observed": 6,
"finish_position": 1,
"result_status": "final",
"venue_split_suspect": false,
"venue_split_peer_race_id": null,
"name_fragment_suspect": false,
"venue_id": "tatura",
"venue_site": "tatura",
"runner_ref": "ra:NDIzMjAzNDI2NDA",
"horse_ref": "pe:theshyster",
"scratched": false,
"open_place_price": 1.3,
"close_place_price": 1.26,
"scratched_at": null,
"open_top2_price": null,
"open_top3_price": null,
"open_top4_price": null,
"close_top2_price": null,
"close_top3_price": null,
"close_top4_price": null,
"consensus_close_prob": 0.388705,
"consensus_close_price": 2.573,
"books_in_consensus_close": 14,
"consensus_open_price": 2.754,
"books_in_consensus_open": 13
}
]
}
GET /v1/racing/closing-lines/coverage — What the closing-line archive actually holdsGET /v1/racing/market-summary — The market's path per runner, stored at the jump: consensus at fixed marks, moves, breadth, first mover, signalGET /v1/racing/price-paths — Bulk export of the permanent price-movement archiveThe free tier needs no credit card, and the sandbox endpoints need no key at all.
Get a free API key Quickstart