Racing

Best racing price per runner across books

Cost: 3 credits. 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. The same treatment, with a reason on the entry, covers a book quoting before the race's newest scratching (pre_scratching), a book whose whole win book sums under 95% (book_under_round), and since 2026-10-20 a book that has not re-priced the race for an hour while the other books have, on a runner where it is 1.6x or more the median of eight or more others (book_not_repriced, with consensus and behind_market_s).

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. 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:

bookopens inpreselects
unibetweb browserrunner and market (fixed win or fixed place)
tabweb browserrunner 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_aunative app only (iOS/Android 8.69.0+)runner and market
nedsnative app onlyrunner 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.

Your own links (source_link and betslip_link on every quote object, since 2026-10-14, Plus and above). Store your bookmaker link templates once with PUT /v1/link-templates (for example your affiliate tracking link per bookmaker, with {url} where the bookmaker's URL goes) and each quote also carries a ready-made twin of its URLs, built through them: put the *_link value on your page. A twin is absent where there is nothing to link, and an account with no stored templates sees none. The *_url fields are unchanged.

get/v1/racing/best-odds

Request

  • Base URL: https://api.puntersedge.online
  • URL: https://api.puntersedge.online/v1/racing/best-odds
  • Auth: API key in header X-API-Key

Query parameters

hours_aheadinteger

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.

datestring nullable

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_racesinteger

Up to 150. Pass 150 to compare every race on the day's card in one call instead of paging.

limitinteger

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.

categoriesstring

horse,greyhound,harness or omit for all

categorystring

Alias of categories. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones.

venuestring

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.

bookmakersstring

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.

countrystring

ISO country codes, comma-separated (e.g. AU; Britain is GB, not UK). Omit for every country. Outside AU, NZ, GB/IE and ZA a race is usually quoted by a single book, so a cross-book comparison on it compares nothing — Hong Kong (HK) is the exception among those 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_unresolvedboolean

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.

Headers

X-API-Keystring
X-RapidAPI-Proxy-Secretstring

Response

Successful Response

race_idstring required
venuestring nullable required
race_numberinteger nullable required
categorystring nullable required
countrystring nullable required
start_timestring required
race_namestring nullable
track_conditionstring nullable
market_percentagenumber nullable
books_comparedinteger required
scratchingsobject[]
data_age_secondsinteger nullable

Age in seconds of the OLDEST bookmaker quote behind this race.

freshest_age_secondsinteger nullable
refresh_tierstring

Which refresh rule this race is on, and therefore what the race-level stale means. live = inside 3 hours of the jump: books re-price it on every 8-20s poll, and stale fires past 120 seconds. card = further out: books re-price it about every 15 minutes so a full day fits inside their rate limits, and stale fires past 1800 seconds. Every published quote carries ITS OWN refresh_tier, and it can be card on a live race: Betr's upstream rate-limits us, so that book stops re-pricing every poll 30 minutes before the jump rather than 3 hours (PlayUp, on a 6-requests-a-minute budget, at 6 minutes), and its quotes between there and 3 hours are refreshed every 5-15 minutes, published as card, and judged against 1800 seconds (since 2026-09-07; before that they were marked stale). A book's tier is never tighter than its race's. age_seconds is the TRUE age in every case — only the threshold changes.

staleboolean
stale_bookmakersstring[]
cachedboolean nullable
cache_age_secondsinteger nullable
runnersobject[] required

Example response

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

Changes

No recorded changes to this operation across all 1 revision of this API.