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. `race_id` is the same stable identifier used by /v1/racing/next-to-go, /v1/racing/price-history and /v1/racing/results.
X-API-Key
2 credits
curl 'https://api.puntersedge.online/v1/racing/changes?since=2026-09-01T05' \
-H 'X-API-Key: YOUR_KEY'
| Name | In | Type | Required | Description |
|---|---|---|---|---|
since |
query | string | yes | 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 |
query | string | no | horse, greyhound, harness — comma-separated. Omit for all three. An unknown value is refused rather than silently ignored. |
country |
query | string | no | 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 |
query | boolean | no | 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. |
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 2 credits.
Field names and types are as the API returns them; values are a real sample, trimmed to a few items.
{
"server_time": "2026-09-07T03:11:30Z",
"races": [
{
"race_id": "2f0e2df8-5386-4aae-bbdb-7b784dee3e43",
"venue": "Grafton",
"race_number": 1,
"category": "horse",
"country": "AU",
"start_time": "2026-09-07T03:20:00Z",
"status": "open",
"market_status": null,
"inplay": null,
"market_closed_at": null,
"inplay_at": null,
"market_state_source": null,
"scratchings": [
{
"name": "Airhawk",
"number": 1,
"barrier": 9,
"scratched_at": "2026-09-03T11:47:44Z"
},
{
"name": "Sweet September",
"number": 9,
"barrier": 3,
"scratched_at": "2026-09-06T18:58:41Z"
}
],
"track_condition_changed_at": null,
"track_condition": "Soft (5)",
"weather": "Clear Sky",
"changed_runners": [
{
"name": "Fabulous Fiano",
"number": 2,
"bookmaker_key": "ladbrokes_au",
"win_price": 7.0,
"place_price": 2.6,
"top2_price": 2.6,
"top3_price": 1.6,
"top4_price": 1.22,
"updated_at": "2026-09-07T03:11:44Z"
},
{
"name": "Fabulous Fiano",
"number": 2,
"bookmaker_key": "sportsbet",
"win_price": 7.5,
"place_price": 2.5,
"top2_price": 2.5,
"top3_price": 1.53,
"top4_price": 1.2,
"updated_at": "2026-09-07T03:11:52Z"
},
{
"name": "Foxwedge Arrow",
"number": 3,
"bookmaker_key": "tab",
"win_price": 51.0,
"place_price": 11.0,
"top2_price": null,
"top3_price": null,
"top4_price": null,
"updated_at": "2026-09-07T03:11:58Z"
}
]
}
]
}
GET /v1/racing/acceptances — Full-day AU thoroughbred acceptance card: every meeting, race and runnerGET /v1/racing/best-odds — Best racing price per runner across booksGET /v1/racing/events — Upcoming race listGET /v1/racing/greyhounds/form — Greyhound form history for one dogGET /v1/racing/greyhounds/stats — Greyhound record by track, distance, box or gradePOST /v1/racing/horses/backfill — Queue a batch of horses for paced form collectionDELETE /v1/racing/horses/backfill/{job_id} — Cancel the horses a job has not collected yetGET /v1/racing/horses/backfill/{job_id} — Progress of a backfill jobThe free tier needs no credit card, and the sandbox endpoints need no key at all.
Get a free API key Quickstart