---
title: "Races and prices that changed since a timestamp"
method: GET
path: "/v1/racing/changes"
tags: ["Racing"]
---

# Races and prices that changed since a timestamp

`GET /v1/racing/changes`

## Request

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

Cost: 2 credits. Everything that moved on the live racing board since `since` — one
poll instead of re-downloading the whole card to diff it yourself. Never cached; serving
a cached delta would hand you changes you already have and hide the ones you do not.

**Poll contract.** Call with a recent `since`, then send the `server_time` from each
response as the `since` of the next call. `server_time` is deliberately set back 30
seconds from the server clock: ingest stamps a price change in Python and commits the
batch afterwards, so a cursor taken at the exact clock would step over a write that was
in flight and lose that price change permanently. The setback covers two racing poll
cycles, which makes this feed AT LEAST ONCE — a change may arrive twice, never zero
times. Deduplicate on (race_id, name, bookmaker_key, updated_at); that tuple is stable
across a redelivery. Measured over two live polls 75 seconds apart, 2026-09-01: 487 of
530 runner deltas on the second poll were new and 43 were the deliberate replay.

**`since` older than 30 minutes returns 422 and is not charged.** The live board holds
upcoming races only and purges a race once it jumps, so a gap wider than the board's own
lifetime cannot be served from it. Do a full fetch from /v1/racing/next-to-go and resume
polling here from the `server_time` it hands back.

**What drives `changed_runners`.** `race_runners.updated_at`, which is a genuine
last-price-CHANGE stamp, not a poll stamp. Ingest rewrites every runner row on every poll
but carries the previous timestamp forward when the win and lay prices are unchanged, so
a steady price does not appear here. Measured on the live board 2026-09-01: 97.4% of
runner rows were re-polled inside 2 minutes while 22.4% had an `updated_at` inside the
same window. A move in `place_price` alone does not advance the column and is delivered
on that runner's next win-price move — poll /v1/racing/next-to-go instead if place prices
are your primary signal.

**A race with an empty `changed_runners` is expected.** A race is also returned when its
own row was written since `since`, and that row's timestamp advances on Betfair market
volume as well as on real state changes, so the race-level arm is a superset. On the
first live poll of this endpoint, 46 of 61 races arrived on that arm alone. Read each
race object as the CURRENT state of the race and compare against what you hold:
`scratchings` is always the complete current list for the race, never a delta, so a
runner leaving it is visible as an absence.

`status` is not a state signal and is documented as such on the response model: it is
written once at row creation and never revised while a race is upcoming, so it reads
"open" for every race here.

`market_status`, `inplay` and the `market_closed_at` / `inplay_at` lifecycle stamps are
gated on WHICH FEED reported the state, the same rule /v1/racing/events applies. PointsBet
closes its book at the real jump and its state is served to everybody; Betfair Exchange
state is withheld from customer responses pending a data licence, so a race whose state
only the exchange saw reads null for customers. `market_state_source` names the feed
behind the values you receive. The race-level facts a customer can act on are
`scratchings`, a revised `start_time`, and the lifecycle stamps.

Betfair prices are withheld from customer keys here exactly as they are on every other
racing endpoint, so a Betfair-only price move produces no `changed_runners` entry.

**Betslip links, Plus and above** (since 21 September 2026). A `changed_runners` entry
from a book with a runner-level betslip route (Unibet and TAB on the web, Ladbrokes and
Neds in their apps) also carries `betslip_win_url`, `betslip_place_url` and
`betslip_app_only` - the same fields, the same per-book rules and the same daily check as
/v1/racing/next-to-go - so a poller holds the move and the link that acts on it in one
object. The fields are absent on every other book, and on plans below Plus (a Standard or
Hobby key issued before that date keeps them). `source_url` is not carried here; take it
from next-to-go. With link templates stored through `PUT /v1/link-templates` (since
2026-10-14) the entry also carries `betslip_win_link` / `betslip_place_link`: the same
URLs built through your templates, absent where there is nothing to link.

`race_id` is the same stable identifier used by /v1/racing/next-to-go,
/v1/racing/price-history and /v1/racing/results.

## Query parameters

- `since` string, required: ISO 8601 UTC timestamp, e.g. 2026-09-01T05:06:38Z. On your first call use a recent timestamp; after that send back the `server_time` from the previous response. Older than 30 minutes is refused.
- `categories` string: horse, greyhound, harness — comma-separated. Omit for all three. An unknown value is refused rather than silently ignored.
- `category` string: Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones.
- `country` string: ISO country codes, comma-separated (e.g. AU or AU,NZ). Omit for every country. Races whose country is not resolved yet carry country=null and are EXCLUDED by any value here — pair with include_unresolved=true or you will lose most of an Australian card.
- `include_unresolved` boolean: Include races whose country is not resolved yet (country is null). OFF by default, matching /v1/racing/events. Most of an Australian card is unresolved until the meeting is confirmed — 58.8% of horse races in a measured window — so country=AU alone can return almost nothing.

## Headers

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

## Response `200`

Successful Response

- RacingChangesOut
  - `server_time` string, required: Send this back as `since` on your next poll. Deliberately set back 30 seconds from the server clock so no in-flight write is skipped — the feed is at-least-once, not exactly-once.
  - `races` ChangedRaceOut[]
    - `race_id` string, required: Stable race identifier — the same id /v1/racing/next-to-go, /v1/racing/price-history and /v1/racing/results use.
    - `venue` string, nullable
    - `race_number` integer, nullable
    - `category` string, nullable: horse, greyhound or harness.
    - `country` string, nullable: ISO country code, or null while the meeting is unresolved.
    - `start_time` string, required: Advertised start, UTC. It is revised as the meeting firms, and a revision bumps this race into the feed.
    - `status` string, nullable: Set when the race row is created and revised exactly once, if at all: to 'abandoned' when Racing Australia posts an official abandoned, postponed or transferred notice for the meeting — that revision bumps every runner of the race into this feed (2026-09-06). Otherwise it reads 'open' for every race here. It is not a live market-state signal — do not poll this endpoint for it.
    - `market_status` string, nullable: Betting-market state (OPEN, SUSPENDED, CLOSED). Reported by PointsBet, which sends CLOSED when it shuts its book at the jump, and by the Betfair Exchange, which is withheld from customer responses pending a data licence — so a value last written by the exchange reads null for customers. Same shape as /v1/racing/events.
    - `inplay` boolean, nullable: True once the market is in-play. Exchange-only — bookmakers do not run racing in-play — so it carries the same licence hold as `market_status` and reads null for customers.
    - `market_closed_at` string, nullable: UTC time the market was FIRST seen shut. Poll-observed and set once — see /v1/racing/events for the full semantics. Served when a BOOKMAKER reported the transition; null for customers when the exchange was the only feed that saw it. Null never means the market stayed open.
    - `inplay_at` string, nullable: UTC time the race was FIRST seen in-play — the actual off, as opposed to `start_time`, which is advertised and moves. Exchange-only, so null for customer plans.
    - `market_state_source` string, nullable: Which feed's market state produced `market_closed_at` / `inplay_at` — 'pointsbetau' or 'betfair_ex_au'. Set once, with the first stamp, and null whenever those stamps are withheld.
    - `scratchings` object[]: The COMPLETE current scratchings for this race, merged across every book that reports one — not a delta, so a runner leaving the list is visible as an absence. Greyhound cards also carry box-vacancy placeholders here (a literal '....' or 'Vacant Box'), which name an empty box rather than a withdrawn runner; match on `number` and treat a name from that set as a vacant box.
    - `track_condition_changed_at` string, nullable: UTC instant of the most recent logged track-condition, weather or rail transition for this race's meeting (2026-09-05). Null when nothing has changed since we began watching it. Read it with `track_condition`: the value says WHAT, this says WHEN.
    - `track_condition` string, nullable: The race's CURRENT track condition, not a change log — a poller diffing against what it holds sees a condition change the same way it sees a scratching. Null where no source reports it.
    - `weather` string, nullable: Current weather for the meeting, same read-as-current rule as track_condition.
    - `changed_runners` ChangedRunnerOut[]: Runner prices that moved inside the window. An empty list means this race arrived on the race-level arm alone — expected, and explained in the endpoint description.
      - `name` string, required: Runner name as the bookmaker publishes it.
      - `number` integer, nullable: Saddlecloth / rug number, where the book supplies one.
      - `bookmaker_key` string, required: Bookmaker this price belongs to.
      - `win_price` number, nullable: Current fixed win price at this bookmaker.
      - `place_price` number, nullable: Current fixed place price. A place-only move does not advance `updated_at` — see the endpoint description.
      - `top2_price` number, nullable: Current Top 2 (first-two) price at this bookmaker, where quoted.
      - `top3_price` number, nullable: Current Top 3 price at this bookmaker, where quoted.
      - `top4_price` number, nullable: Current Top 4 price at this bookmaker, where quoted.
      - `updated_at` string, required: UTC instant this runner's win or lay price last CHANGED at this bookmaker. Not the last poll: ingest carries this timestamp forward while the price is steady.
      - `betslip_win_url` string, nullable: Opens this bookmaker's betslip with this runner's fixed WIN selected. Plus and above. Present only for the books with a runner-level route (unibet, tab, ladbrokes_au, neds) — see /v1/racing/next-to-go for the per-book table and the daily check behind it.
      - `betslip_place_url` string, nullable: Same, fixed PLACE. Null when the book has a win link but is not quoting the place; on TAB it is the same one-shot URL as the win link.
      - `betslip_app_only` boolean, nullable: True for Ladbrokes and Neds: those URLs open the native app and redirect to the app-download page in a desktop browser.
      - `betslip_win_link` string, nullable: Your own link for betslip_win_url, built through the link templates stored with PUT /v1/link-templates. Absent when your account stores none, or there is nothing to link.
      - `betslip_place_link` string, nullable: Your own link for betslip_place_url, built the same way. Absent when there is nothing to link.

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