The best win, place and tote price for every runner in the next races, with the bookmaker offering it and a deep link to their race page. `market_percentage` is the sum of implied probabilities at the best available price. **Under 100 means the best prices across books beat the field** — a cross-book arbitrage. Any single book's market is always over 100. `price_spread_pct` per runner is how much better the best price is than the worst currently quoted: how much a punter loses by not shopping around. **Win and place coverage are counted separately.** `books_quoting_win` is the number of bookmakers with a WIN price on that runner and `books_quoting_place` the number with a PLACE price. They are not the same set — a book can quote the win and not the place on the same runner — so a place bet sized off a win-market count is sized off the wrong number. `books_quoting` is retained unchanged for existing clients and equals `books_quoting_win`; read the explicit names in new code. `books_compared`, at race level, is the number of distinct books contributing any win price to the whole race. ⚠️ `best_tote` is **not comparable to `best_win`**. A pre-race tote dividend is an estimate that moves as the pool fills, and taking the highest across books and products is upward biased by construction — the noisiest estimate wins. Greyhound pools are thinnest and swing hardest: measured 2026-08-15, greyhound tote-to-fixed ratios reached 8.8x on betright and 9.9x on tabtouch, against averages near 1.0 for horse and harness. A tote figure far above the fixed price is usually a thin pool, not an edge. Settle against a FINAL dividend, and read the `product` code — `PROV` is indicative, `SP`/`MIDDIV`/`RD+`/`BT+SP` are named products. The comparison covers the Australian fixed-odds bookmakers, on New Zealand races as well as Australian ones. Not every book quotes New Zealand, so an NZ race is compared across fewer books than an AU race; the live per-country book counts are published at https://puntersedge.online/coverage-report. Betfair Exchange prices are withheld pending a Betfair Exchange data licence, and non-AU reference books are excluded because an Australian punter cannot bet them — so `betfair_ex_au` never appears as a `best_win` bookmaker, and `market_percentage` is the fixed-odds market rather than an exchange-inclusive one. **Hong Kong** (`country=HK`, feature_requests #29, 2026-09-09). Sha Tin and Happy Valley are compared across whichever Australian books quote the meeting — win and place, the same `best_win` / `best_place` shape as an Australian race. PRICES ONLY: no results, no form, no race conditions, no `runner_ref` — `horse_ref` alone is present, because it is derived from the runner's name and needs no Racing Australia read. Unlike the rest of the foreign card this is a real cross-book comparison rather than one book against itself, but it is a smaller panel than an Australian race and only exists on the HKJC's nights — Happy Valley on Wednesday evenings, Sha Tin on Saturday or Sunday afternoons (AEST). **Scratched runners are never priced here.** They appear in `scratchings` only, and are left out of `market_percentage`. **Placeholder prices are kept out of the best line (since 2026-09-10).** A quote of $21 or more that is at least 4x the median of three or more other books for the same runner is treated as a placeholder, not a price: it is excluded from `best_win` / `best_place`, `market_percentage` and `price_spread_pct`, and listed on the runner as `suspect_win` / `suspect_place` (`bookmaker`, `price`, `consensus`) so nothing is hidden. A book that has not priced a runner yet can publish $101 against a $9 field; before this that was the best price. **Every published price carries its own age.** `best_win`, `best_place` and `best_tote` each include `last_update`, `age_seconds`, `stale` and `refresh_tier`, because the book with the best price is not always the book that answered most recently. `best_win` and `best_place` both carry the offering book's race-page `source_url` (best_place since 2026-09-02: it used to lack one, and a client requiring a link on every quote silently rejected every place price). On Plus and above both also carry `betslip_url`, the runner-level betslip link of that same book - null when the book has no such route, and absent on plans below Plus. See the table below. Race level, `data_age_seconds` is the oldest of those winning quotes and `stale_bookmakers` names any past its own `refresh_tier` threshold (120s on `live`, 1800s on `card`; each published quote carries its own tier, and a Betr quote 30-180 minutes from the jump is `card` on a `live` race — see /v1/racing/next-to-go for what the tiers mean). Responses are cached for 20s and say so via `cached` / `cache_age_seconds`; ages are recomputed at serve time, so a cached payload reports its real age rather than the age it had when it was stored. **Same Race Multi legs** (`best_top2`, `best_top3`, `best_top4`, since 2026-09-05). The best price across books for the runner to finish in the first two, three or four, in the same shape as `best_place` (price, bookmaker, source_url, age fields) plus `books_quoting`, the number of books offering that leg. Present only when at least one book quotes the leg: Ladbrokes, Neds and PointsBet carry all three, Sportsbet where its racecard offers them. Every published leg satisfied `win >= top2 >= top3 >= top4 > 1.0` at its source. **Runner-level betslip links** (`betslip_win_url`, `betslip_place_url`, `betslip_app_only`). Where `source_url` opens the book's race PAGE, these open the book's BETSLIP with this runner already selected. **Plus and above** (since 21 September 2026): the betslip fields are served on Plus, Business, Platform and Scale, and a Standard or Hobby key issued before that date keeps them - a key never loses what it was sold. On every other plan the fields are simply absent, exactly as on a book with no route; `source_url`, the race-page link, is on every plan and is unaffected. Coverage, honestly: | book | opens in | preselects | |---|---|---| | `unibet` | web browser | runner **and** market (fixed win or fixed place) | | `tab` | web browser | runner only - the slip shows a win box and a place box, so `betslip_win_url` and `betslip_place_url` are deliberately the same URL | | `ladbrokes_au` | **native app only** (iOS/Android 8.69.0+) | runner and market | | `neds` | **native app only** | runner and market | Every other bookmaker has no such route and carries none of these fields. `betslip_app_only` is `true` for Ladbrokes and Neds: in a desktop browser those URLs redirect to the app-download page, so send them to a phone with the app installed, or fall back to `source_url`. Measured 2026-09-03. The Ladbrokes pattern is vendor documented; the Neds one is **inferred** from it (same platform, same entrant ids) and has not been tap-tested on a Neds install. These routes are read from the bookmakers' own site code. **None of them is published or supported by the bookmaker**, so any can change without notice. A daily automated check loads a live Unibet link and a live TAB link in a real browser and confirms the betslip populates; when a book fails that check its betslip fields go **null** until it passes again, so a broken route degrades to "no link" rather than sending a member to a dead page. Ladbrokes and Neds cannot be checked this way - a headless browser cannot open a native app - and ship on the vendor's documentation instead. The fields are **absent** on a book that has no link, exactly as `source_url` is; treat absent and `null` alike as "no betslip link". Nothing about `source_url` changed. Like `source_url` these are plain URLs, and extra query parameters you append are passed through untouched. A link that opens a native app leaves the browser, so cookie-based attribution set by your own site does not travel with it.
X-API-Key
3 credits
curl 'https://api.puntersedge.online/v1/racing/best-odds' \
-H 'X-API-Key: YOUR_KEY'
| Name | In | Type | Required | Description |
|---|---|---|---|---|
hours_ahead |
query | integer | no | How many hours ahead to scan for priced races, from now. Default 6, which is what this endpoint has always used. The ceiling is your plan horizon, returned as X-Horizon-Hours: 24 Free and Hobby, 48 Standard, 168 Plus and above. Raising it past about 12h returns races no book has opened yet -- the first price lands a median 10.1h before the jump on AU thoroughbreds -- so a wide window is for building tomorrow's board, not for finding more prices today. |
date |
query | string | no | Single-day alias: date=YYYY-MM-DD scans that Australian Eastern meeting day instead of a rolling hours_ahead window. Mutually exclusive with hours_ahead, bounded by the same X-Horizon-Hours ceiling, forward-looking only. num_races still caps the rows returned, so pair a date with num_races=150 for a full card. |
num_races |
query | integer | no | Up to 150. Pass 150 to compare every race on the day's card in one call instead of paging. |
limit |
query | integer | no | Alias for `num_races`, accepted because it is the near-universal REST spelling and because /v1/racing/results already takes it — learning `limit` on one racing endpoint and being refused on the next is the inconsistency this closes. Same bounds and same meaning; when both are given, `limit` wins. Measured 2026-09-14: callers sent `limit` here 28 times and were refused every time. |
categories |
query | string | no | horse,greyhound,harness or omit for all |
category |
query | string | no | Alias of `categories`. 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. An unknown value is a free 422 naming the valid ones. |
venue |
query | string | no | Venue names, comma-separated and case-insensitive (e.g. Randwick or Randwick,Flemington). Matched exactly against the race's `venue` after case folding, so pass the venue as this API spells it — read `venue` off an unfiltered call rather than guessing. Omit for every venue. Same rule and same spelling as /v1/racing/next-to-go. This narrows the window the endpoint already serves; it does not look beyond it, so a venue with no race inside the window returns an empty list rather than an error. There is no race-number filter: `venue` selects the meeting and you read `race_number` off the races it returns. |
bookmakers |
query | string | no | Restrict the comparison to these bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing — it used to return a billed empty response. |
country |
query | string | no | ISO country codes, comma-separated (e.g. AU). Omit for every country. Foreign races are quoted by a median of one book, so a cross-book comparison on them compares nothing — Hong Kong (HK) is the exception among foreign countries, carrying several Australian books on the HKJC race nights, win and place, prices only. Pair with include_unresolved=true or you will silently lose races whose meeting is not confirmed yet. |
include_unresolved |
query | boolean | no | Include races whose country is not resolved yet (country is null). OFF by default. 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 far fewer races than are running, or none at all. Pass true to get them, and read `country` per race to see which are still unresolved. |
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 3 credits.
Field names and types are as the API returns them; values are a real sample, trimmed to a few items.
[
{
"books_compared": 11,
"cache_age_seconds": 0,
"cached": false,
"category": "horse",
"country": "AU",
"data_age_seconds": 41,
"freshest_age_seconds": 3,
"market_percentage": 106.4,
"race_id": "race_123",
"race_name": "TAB ROSEBUD",
"race_number": 8,
"refresh_tier": "live",
"runners": [
{
"barrier": 3,
"best_place": {
"age_seconds": 4,
"betslip_url": "https://www.tab.com.au/racing/2026-09-03/REDCLIFFE/RED/R/4?runner=5",
"bookmaker": "tab",
"last_update": "2026-08-15T05:44:21Z",
"price": 2.15,
"source_url": "https://www.tab.com.au/racing/2026-09-03/REDCLIFFE/RED/R/4",
"stale": false
},
"best_tote": {
"bookmaker": "betright",
"price": 7.1,
"product": "RD+"
},
"best_win": {
"age_seconds": 6,
"bookmaker": "betright",
"last_update": "2026-08-15T05:44:19Z",
"price": 6.5,
"refresh_tier": "live",
"source_url": "https://...",
"stale": false
},
"books_quoting": 11,
"books_quoting_place": 8,
"books_quoting_win": 11,
"horse_ref": "pe:chillygirl",
"jockey": "J Mcdonald",
"name": "Chilly Girl",
"number": 7,
"price_spread_pct": 18.2,
"trainer": "C Waller"
}
],
"scratchings": [],
"stale": false,
"stale_bookmakers": [],
"start_time": "2026-08-15T05:45:00Z",
"track_condition": "Good (4)",
"venue": "Rosehill"
}
]
GET /v1/racing/acceptances — Full-day AU thoroughbred acceptance card: every meeting, race and runnerGET /v1/racing/changes — Races and prices that changed since a timestampPOST /v1/racing/clv — Score bets against the closing line (CLV)GET /v1/racing/events — Upcoming race listGET /v1/racing/form — Every runner in one race with its recent runs, from our own resultsGET /v1/racing/greyhounds/form — Greyhound form history for one dog, from our own resultsGET /v1/racing/greyhounds/stats — Greyhound record by track, distance, box or grade, from our own resultsPOST /v1/racing/horses/backfill — Queue a batch of horses for paced form collectionThe free tier needs no credit card, and the sandbox endpoints need no key at all.
Get a free API key Quickstart