---
title: "Permanent closing-line and result archive"
method: GET
path: "/v1/racing/closing-lines"
tags: ["Racing Archive"]
---

# Permanent closing-line and result archive

`GET /v1/racing/closing-lines`

## Request

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

Cost: 5 credits (JSON) / 20 credits (CSV). 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.** Before 2026-10-04 harness and NZ
  results named the placegetters only, so most runners in a resulted race there have no
  published position; AU harness and NZ results carry the full field from 2026-10-04,
  and GB and IE thoroughbred results from 2026-10-06.
  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.
* `duplicate_suspect` / `duplicate_of_race_id` — the race appears to duplicate another race
  record (the same race filed twice); the second names the other race_id. These keys, and
  `meeting_date_local`, appear on a row once the archive carries them; an absent key means
  not assessed, not false.
* `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.

**Recommended filter for modelling.** Keep a row when `is_closing_line AND NOT scratched AND
NOT venue_split_suspect AND NOT name_fragment_suspect AND NOT duplicate_suspect` (treat an
absent `duplicate_suspect` as false). For GB/IE racing, key a race on `race_id` or on
`(venue, meeting_date_local, race_number)`, never on `meeting_date_aet`: a British or Irish
evening card falls on the next Australian Eastern date. Every row's fields are typed in the
`ClosingLineRow` schema.

**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 forward-growing and depends on the market.** Australian and New Zealand
results begin on 2026-08-15, British and Irish results on 2026-10-06 and South African
thoroughbred results on 2026-10-15; races in every other country carry closing prices but no
result, and archived races before those dates carry none. `resulted_rows` on the response is
the measured count for your actual selection, not a marketing figure, and
`/v1/racing/closing-lines/coverage` publishes the share by code.

**`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.

## Query parameters

- `from` string, nullable: ISO date/time on the race start. Defaults to the archive floor.
- `to` string, nullable: ISO date/time. Defaults to now.
- `date` string, nullable: 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.
- `venue` string, nullable: Venue name, case-insensitive, exact match.
- `bookmakers` string, nullable: Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing.
- `category` string, nullable: 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` string, nullable: Alias of `category`. Comma-separated horse, harness, greyhound. Both spellings are accepted on every racing endpoint.
- `country` string, nullable: Two-letter country code, case-insensitive; comma-separated for more than one, e.g. AU, or GB,IE for British and Irish racing.
- `race_id` string, nullable: Single race, joinable to /v1/racing/price-history while that race is still inside the 45-day snapshot window.
- `race_number` integer, nullable: Race number. With venue and date it names one race, as on /v1/racing/price-history and /v1/racing/markets: `date` here is the UTC day of the start, and when that day holds no row for the race the Australian Eastern meeting date (the date /v1/racing/results gives) is tried instead.
- `closing_only` boolean: 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` boolean: Only rows from races that have a result.
- `include_flagged` boolean: 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` boolean: 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` string: csv streams a flat table with a stable column order — the format a modeller actually wants.
- `limit` integer
- `offset` integer

## Headers

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

## Response `200`

Successful Response

- ClosingLinesOut
  - `rows_returned` integer, required
  - `total_rows` integer, required: Rows matching the filter before limit/offset
  - `limit` integer, required
  - `offset` integer, required
  - `archive_from` string, nullable: Earliest start_time in the whole archive. The archive only grows; this is its true floor as of right now, not a promise.
  - `window_days` integer, nullable: Set when the caller's plan clamps the lookback. NULL means the full archive was searched.
  - `resulted_rows` integer, required: Rows in the matching set that have a result_status. Published because result coverage is thin and growing (5.3% of archived races on 2026-08-18) — do not assume a NULL finish_position means the runner lost. It means no position was published for the runner: harness and NZ results named the placegetters only before 2026-10-04 and carry the full field from then, while AU thoroughbred and greyhound results position every finisher (from 2026-09-14).
  - `rows` ClosingLineRow[], required
    - `race_id` string, nullable: Race id; joins /v1/racing/results and /v1/racing/markets. Never null.
    - `start_time` string, nullable: Advertised start, UTC, ISO 8601 with Z. Never null.
    - `meeting_date_aet` string, nullable: Meeting date, YYYY-MM-DD, Australian Eastern calendar. Never null.
    - `venue` string, nullable: Venue name as the books list it. Never null.
    - `race_number` integer, nullable: Race number at the meeting.
    - `category` string, nullable: horse, harness or greyhound.
    - `country` string, nullable: Two-letter country code.
    - `runner_key` string, nullable: Normalised runner name; the runner's key within the race. Never null.
    - `runner_name` string, nullable: Runner name as published. Never null.
    - `runner_number` integer, nullable: Saddlecloth (box for greyhounds).
    - `bookmaker_key` string, nullable: Bookmaker key. Never null.
    - `open_win_price` number, nullable: Win price at the first observation of the series.
    - `open_secs_to_jump` integer, nullable: Seconds to the jump at the first observation.
    - `open_is_baseline` boolean, nullable: FLAG. True when the series began at the 60-minute window entry, so open_win_price is a market open; false when the book joined mid-window. Never null.
    - `close_win_price` number, nullable: Win price at the last observation before the jump.
    - `close_lay_price` number, nullable: Exchange lay price at the close; null for customers (exchange rows are withheld).
    - `close_secs_to_jump` integer, nullable: Seconds to the jump at the last observation.
    - `is_closing_line` boolean, nullable: FLAG. True when the last observation was within 300 seconds of the jump: a genuine closing line. False rows carry a last-seen price. Never null.
    - `points_observed` integer, nullable: Price points in the series. Never null.
    - `finish_position` integer, nullable: Finishing position where the result publishes one. Null does NOT mean the runner lost; read result_status.
    - `result_status` string, nullable: The race's result status; null when the race has no result.
    - `venue_split_suspect` boolean, nullable: FLAG. The row may belong to a race the feed split across two venue spellings. Excluded by default. Never null.
    - `venue_split_peer_race_id` string, nullable: FLAG companion. The other race_id of a suspected venue split.
    - `name_fragment_suspect` boolean, nullable: FLAG. The runner name looks like a fragment of another runner's name. Excluded by default. Never null.
    - `venue_id` string, nullable: Canonical venue id from the venue registry.
    - `venue_site` string, nullable: Canonical venue site from the venue registry.
    - `runner_ref` string, nullable: Registry id for the runner on race day (ra:, grv:, grsa:).
    - `horse_ref` string, nullable: Horse identity across meetings (pe:<name>); thoroughbreds only.
    - `scratched` boolean, nullable: FLAG. The result marked this runner scratched, yet a book kept pricing it. Excluded by default. On harness and NZ rows false means not known. Never null.
    - `open_place_price` number, nullable: Place price at the first observation (from 2026-09-29).
    - `close_place_price` number, nullable: Place price at the last observation (from 2026-09-29).
    - `scratched_at` string, nullable: When the runner was scratched, UTC, as the board stamped it (from 2026-09-29).
    - `open_top2_price` number, nullable: Same Race Multi Top 2 price at the first observation.
    - `open_top3_price` number, nullable: Same Race Multi Top 3 price at the first observation.
    - `open_top4_price` number, nullable: Same Race Multi Top 4 price at the first observation.
    - `close_top2_price` number, nullable: Same Race Multi Top 2 price at the last observation.
    - `close_top3_price` number, nullable: Same Race Multi Top 3 price at the last observation.
    - `close_top4_price` number, nullable: Same Race Multi Top 4 price at the last observation.
    - `consensus_close_prob` number, nullable: Market consensus probability at the close, margin removed.
    - `consensus_close_price` number, nullable: The price consensus_close_prob corresponds to.
    - `books_in_consensus_close` integer, nullable: Books the closing consensus rests on.
    - `consensus_open_price` number, nullable: Consensus price at the 60-minute open.
    - `books_in_consensus_open` integer, nullable: Books the opening consensus rests on.
    - `duplicate_suspect` boolean, nullable: FLAG. The row's race appears to duplicate another race record (the same race filed twice); duplicate_of_race_id names the other. The key is absent until the archive carries the flag.
    - `duplicate_of_race_id` string, nullable: FLAG companion. The race_id this row's race duplicates. Absent until the archive carries it.
    - `meeting_date_local` string, nullable: Meeting date in the venue's own time zone, YYYY-MM-DD. For GB and IE racing key on race_id or (venue, meeting_date_local, race_number). Absent until the archive carries it.

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