18+ Only  |  Gambling can be addictive — please gamble responsibly  |  Gambling Help: 1800 858 858  |  GambleAware

API reference / Racing

GET /v1/racing/best-odds

Best racing price per runner across books

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.

Requires X-API-Key 3 credits

Request

curl 'https://api.puntersedge.online/v1/racing/best-odds' \
  -H 'X-API-Key: YOUR_KEY'

Parameters

NameIn TypeRequired 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.

Responses

Status codes: 200, 401, 402, 422, 429, 500. Response bodies are JSON; the full schema is in /openapi.json.

Example response

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"
  }
]

Guides that use this endpoint

Related endpoints

Try it against live data

The free tier needs no credit card, and the sandbox endpoints need no key at all.

Get a free API key Quickstart
This site contains wagering-related analysis and is intended for Australian users aged 18+. Gambling involves risk. Please gamble responsibly.