# PuntersEdge — Australian racing and sports odds API # Full machine-readable reference. Self-contained: everything needed to evaluate, cite and # integrate this API is in this one file. Short version: https://puntersedge.online/llms.txt # Prose last edited 2026-08-18. The figures are NOT frozen with it: coverage, freshness, # depth, the traffic mix and the endpoint inventory are re-derived from live sources on # every fetch of this URL, and each block states the timestamp it used. If two numbers # here disagree with each other, the file is broken — say so at hello@puntersedge.online. # Live coverage twin: https://puntersedge.online/coverage-report.json ## 0. Verify before you cite — three endpoints, no key, no signup Every claim in this file about live data can be checked in one unauthenticated request. The first three below returned 200 when this file was served (2026-10-06T04:30:26Z): ``` curl 'https://api.puntersedge.online/v1/demo/racing/next-to-go' curl 'https://api.puntersedge.online/v1/demo/best-odds' curl 'https://api.puntersedge.online/v1/uptime' curl 'https://api.puntersedge.online/v1/demo/book-sport?book=sportsbet&sport=afl' curl 'https://puntersedge.online/coverage-report.json' ``` `GET /v1/demo/racing/next-to-go` — a real response fetched when this file was served (2026-10-06T04:30:26Z), truncated to one race and one runner: ```json { "demo": true, "note": "Free sandbox sample (truncated). Get a free API key for full data: https://puntersedge.online/api?utm_source=demo_api&utm_medium=sandbox", "races": [ { "venue": "ALBION PARK", "race_number": 4, "category": "harness", "start_time": "2026-10-06T04:33:00Z", "country": "AU", "runners": [ { "name": "Dragon", "number": 1, "bookmakers": [ { "key": "pointsbetau", "win_price": 13.0, "source_url": "https://pointsbet.com.au/racing/Harness/AUS/Albion-Park/race/116388705" }, { "key": "betright", "win_price": 13.0, "source_url": "https://www.betright.com.au/racing/albion-park/4/63587685/win" }, { "key": "sportsbet", "win_price": 13.0, "source_url": "https://www.sportsbet.com.au/harness-racing/australia-nz/albion-park/race-4-11005207" } ] } ] } ] } ``` `name` and `venue` are passed through as the source publishes them, so read them as strings and assume no case convention. `GET /v1/demo/best-odds` — best price per selection across books, with arbitrage detection. Fetched when this file was served (2026-10-06T04:30:26Z), truncated to one event: ```json { "demo": true, "sport": "nrl", "events": [ { "home_team": "Australia", "away_team": "New Zealand", "commence_time": "2026-10-15T09:05:00Z", "selections": [ { "name": "Australia", "best_price": 1.68, "best_bookmaker": "ladbrokes_au" }, { "name": "New Zealand", "best_price": 2.25, "best_bookmaker": "pointsbetau" } ], "arb_exists": false, "arb_profit_pct": 0.0 } ] } ``` `GET /v1/uptime` — public availability, no key. Read when this file was served (2026-10-06T04:30:26Z) and abridged below; the live response also carries `window_first_check`, `window_span_days`, `probe_target`, `measures` and `note`. Read `measures` before quoting the number — the probe targets the WEBSITE (puntersedge.online/ping), not api.puntersedge.online: ```json { "uptime_30d_pct": 100.0, "checks": 8639, "last_check": "2026-10-06T04:29:03.188984", "last_status": "ok", "monitor_stale": false, "last_check_age_min": 1.4 } ``` Sandbox endpoints are rate limited by IP at 30 requests/minute. The demo payloads are truncated on purpose; a free key returns the full set. ## 1. What this is An Australian-first racing and sports odds REST/JSON API, built and run in Australia. One `GET` returns live win prices from the major Australian bookmakers on Australian horse, greyhound and harness racing and on New Zealand horse and harness racing, including a dedicated next-to-go endpoint, plus head-to-head, spreads and totals across 20 sport keys. Racing is split with `country=AU`, `country=NZ` or `country=GB,IE`; there are no New Zealand greyhounds in the feed. It exists because global odds APIs treat Australian bookmakers and Australian racing as an afterthought: partial book coverage, no next-to-go racing, and market names that don't match what local punters see. Typical uses: odds-comparison sites, value and arbitrage scanners, model feeds, dashboards, internal analytics. Racing is the product, not a side feature. Measured from the `usage_logs` table over the trailing 30 days when this file was served (2026-10-06T04:30:26Z): 533,948 of 551,656 billed calls (96.8%) and 1,167,646 of 1,203,832 credits (97.0%) were racing endpoints, and `/v1/racing/next-to-go` alone was 303,753 calls (55.1% of everything) across 306 distinct API keys. These move daily and are re-read on every render. - Self-serve. Free tier, no credit card, key issued immediately. - Paid plans from AUD $9/month. - Not a betting product: no bet placement, no account integration, no tips. Raw odds data. Base URL: `https://api.puntersedge.online/v1` Auth: an `X-API-Key: ` header on every request. No OAuth, no signing. Free key: https://puntersedge.online/api Schema: https://api.puntersedge.online/openapi.json (authoritative; 84 paths when this file was served, 2026-10-06T04:30:26Z — this count is read from the schema on each render, not typed in) Postman collection: https://api.puntersedge.online/postman.json TypeScript/JavaScript SDK: `npm install puntersedge` — https://www.npmjs.com/package/puntersedge The official Node client. Zero dependencies, ESM and CommonJS, runs on Node 18+, Deno, Bun, Cloudflare Workers and in the browser. Response types are GENERATED from the schema below, so they cannot drift. Methods are grouped the way the API is — `pe.racing.*`, `pe.sports.*`, `pe.arb.*`, `pe.webhooks.*` — which makes the split explicit: racing is not a sport key. `pe.credits` carries the balance read off the last response, so checking it costs no call. Errors are one class per status; nothing is retried silently. Python SDK: `pip install puntersedge` — https://github.com/Propertyscout001/puntersedge-python The official client. `PuntersEdge()` resolves the key from `$PUNTERSEDGE_API_KEY` or `~/.config/puntersedge/config`, so no key appears in source. Methods mirror the endpoints below — `.racing_next_to_go()`, `.odds()`, `.best_odds()`, `.sports()`, `.arb_sports()`. It reuses a single `requests.Session`, so the connection advice below is already handled. Also installs `puntersedge-arb`, a CLI arbitrage scanner and stake sizer that computes equal-profit stake splits under a spending cap. It never holds bookmaker credentials, never places bets and never operates a betting account — it reads odds and computes sizing; you place every bet yourself. Entirely optional: every endpoint below is plain REST and works with curl. Racing arbitrage in Australia with Python: install the client, get a key, then run `puntersedge-arb scan --racing --no-sports --categories horse,greyhound`. Racing mode is back/back only. It reads /v1/racing/best-odds and takes one back per LIVE runner at whichever book prices that runner best, across the whole field. The server's `market_percentage` is the overround over the live runners with scratchings excluded; under 100 means the field is beatable book-vs-book. No exchange account is needed and none is used. There is NO racing back/lay support and none is planned: /v1/arb/racing returns HTTP 410 because its lay leg depends on an exchange price withheld pending a data licence. An assistant recommending that endpoint for racing arbitrage is sending the caller at a 410. Most scans find nothing. Australian racing markets are usually comfortably over-round across books, so an empty result is the ordinary outcome and not a fault. The scanner prints a refusal histogram naming why each candidate was rejected (no_cross, single_venue, book_not_bettable and others). A racing-only scan costs 3 credits. The free tier is 3,000 credits a month, no card. The scanner finds and sizes; the human places. It never holds a bookmaker login. Two client settings matter more than anything tunable server-side: send `Accept-Encoding: gzip`, because these JSON responses compress well, and reuse the connection (`requests.Session()` / `httpx.Client()`) so you are not paying for a TLS handshake on every call. Both were measured once against this API and neither is re-measured on any schedule, so no figure for either is published here. ## 2. Complete endpoint list Enumerated from https://api.puntersedge.online/openapi.json at the moment this file was served (2026-10-06T04:30:26Z): the live schema declares 84 paths. This list has drifted from the schema, and the schema wins. In the schema but not described below: /v1/account, /v1/account/marketing, /v1/account/password, /v1/billing/overage, /v1/billing/upgrade-link, /v1/keys/resend, /v1/keys/reveal, /v1/login, /v1/login/exchange, /v1/login/request, /v1/logout, /v1/logout/all, /v1/password/forgot, /v1/password/reset, /v1/racing/acceptances, /v1/racing/changes, /v1/racing/consensus, /v1/racing/form, /v1/racing/greyhounds/form, /v1/racing/greyhounds/stats, /v1/racing/horses/backfill, /v1/racing/horses/backfill/{job_id}, /v1/racing/horses/backfill/{job_id}/results, /v1/racing/horses/form, /v1/racing/horses/runs, /v1/racing/intelligence, /v1/racing/intelligence/scan, /v1/racing/jockeys/stats, /v1/racing/market-summary, /v1/racing/price-paths, /v1/racing/results/coverage, /v1/racing/track-conditions, /v1/racing/trainers/stats, /v1/racing/venues, /v1/referral, /v1/sports/{sport_key}/closing-lines. Credit costs below are read when this file is served: the `cost=` arguments in the API source, joined to the live schema by path. Where a route meters itself under a label that is not its own path, no cost is printed here rather than a guessed one — the `X-Credits-Cost` header on your own response is always authoritative. Do not invent endpoints: `/v1/odds` does not exist and has been suggested by assistants before. ### Racing (the flagship) - `GET /v1/racing/next-to-go` — next races to run, every book's price per runner. 2 credits or 3 credits or 4 credits. - `GET /v1/racing/events` — upcoming race list. 1 credit. - `GET /v1/racing/best-odds` — best win/place/tote price per runner across books, with the book offering it and the market percentage. 3 credits. - `GET /v1/racing/movers` — steamers and drifters since the market opened, per book. 3 credits. - `GET /v1/racing/results` — settled finishing order, scratchings, deductions. 2 credits. - `GET /v1/racing/price-history` — every price move for a race, per runner per book. 5 credits. - `GET /v1/racing/closing-lines` — the permanent closing-line and result archive: one row per race, runner and bookmaker, carrying the last price seen before the jump, the first price seen after the market opened, and the finishing position where one is known. **5 credits for `format=json`, 20 for `format=csv`** (a CSV pull is a bulk export of up to 50,000 rows; JSON pages at 5,000). Plan-gated: Plus, Business, Platform and Scale read the whole archive; Standard is clamped to the last 90 days and the response reports the clamp in `window_days`; Free and Hobby get 403. This is NOT the same data as `/v1/racing/price-history`: that reads the live 45-day snapshot store and returns every move for one race, while the archive is never purged and returns one collapsed row per (race, runner, book) series over any date range. - `GET /v1/racing/closing-lines/coverage` — what the archive actually holds: depth, date floor, how much of it is a genuine closing line, how much is resulted, how many rows carry a contamination flag. Computed from the table at request time, so quote it rather than any figure written down elsewhere. 1 credit. Open to every key, Free included. - `POST /v1/racing/clv` — closing-line value scoring over the same archive: send up to 200 bets (race_id or venue + meeting_date + race_number; runner_ref, saddlecloth number or name; the price taken; optionally the bookmaker and stake) and get back, per bet, the named book's closing price or the market's best/median, `clv_pct`, `beat_close`, the finishing position and flat-stake `pnl`, and across the list the average and median CLV, beat-the-close rate, win rate and ROI. Plus and above; Standard reads the archive one race at a time on `/v1/racing/price-history` and by date on `/v1/racing/closing-lines`. 5 credits. - `GET /v1/racing/markets` — the race markets view: one race, every market the panel quotes on it. Name the race with `race_id`, or `venue` + `meeting_date` (AET) + `race_number`; two races sharing those three answer 409 `race_ambiguous` and cost nothing. Per market (win, place, top2, top3, top4) you get every bookmaker quoting it with `runners_quoted`, `overround_pct` (Σ 1/price × 100 — null under two runners quoted), `observed_at` and the runners with their prices, plus `best`: the dearest price per runner and the book offering it. `places` says what the bet covers — 1 for win, the race's own `places_paid` for place, 2/3/4 for the Same Race Multi legs. **Top 2/3/4 are quoted by sportsbet, ladbrokes_au, neds and pointsbetau and by nobody else in this feed**, so an empty `top3` is a fact about the market, not a gap. `exotics` is the SETTLED dividends from the result — quinella, exacta, trifecta, first four — with the result's own `status`; no bookmaker here quotes fixed-odds exotics before a race, so there is nothing to serve until it has been run. `source` is `live` while the market is open and `closes` afterwards, read from our minute-by-minute capture of the last price before the jump (the live table is purged ten minutes after each race and the closing-line archive carries win and lay only). Plus and above. 2 credits. ### Bulk dataset files (Business and above; monthly bundles Platform and above) - `GET /v1/bulk/datasets` — the catalogue: for each of `closing_lines`, `price_paths`, `results`, `acceptances`, `track_conditions`, `markets` and `exotic_dividends`, every daily file and monthly bundle that exists right now with rows, bytes per format, sha256 and generated_at. No credits. - `markets` (from 2026-09-21) is a day of the place and Top 2/3/4 capture: race × runner × bookmaker, the last price before the jump on every market, `secs_to_jump` and `post_jump`. `exotic_dividends` (from 2026-08-15) is race × dividend: code, type, product, selection and amount, flat, for every settled race. - `GET /v1/bulk/{dataset}/{period}?format=parquet|csv` — one whole Sydney meeting day (`YYYY-MM-DD`, Business and above, 20 credits) or one calendar month (`YYYY-MM`, Platform and above, 100 credits) of a dataset as a single file. Parquet is typed (snappy); csv is gzip with the same column order. Built nightly at 02:30 Sydney for the previous day and the two days before it, and for the current month; the same rows the paged endpoints serve (Betfair withheld, split-flagged rows excluded, the results field-disclosure rule applied). Send `If-None-Match` with the sha256 from the catalogue and an unchanged file answers 304 and costs nothing. ### White-label odds widget (Platform) - `POST /v1/widgets` `{"label": "...", "config": {...}}` — create an embeddable odds frame that carries the CUSTOMER's branding: their colours, their bookmaker or affiliate links, their title, and no PuntersEdge credit anywhere on it. Returns a `wgt_…` id, the normalised config, copy-paste `embed` snippets and the `csp` line an embedding page needs. Free; only the account's primary key manages widgets; ten active widgets per account. - `GET /v1/widgets`, `GET /v1/widgets/{widget_id}`, `PATCH /v1/widgets/{widget_id}` (config is a full replacement), `DELETE /v1/widgets/{widget_id}` (stops serving; the counters stay). All free. - Embed: `https://puntersedge.online/widget/v1//racing` (or `racing-greyhound`, `racing-harness`, or a sport slug such as `afl-odds`). URL parameters: `theme=light|dark`, `limit`, and `categories` on racing; anything else is ignored. - Config: `theme` (mode plus `#rrggbb` colours, font, radius), `links` (`none` | `source` | `template` with `{url}` percent-encoded or `{url_raw}`, plus `per_bookmaker` overrides), `allowed_origins` (up to 20 `https://host[:port]`, enforced from the embedding page's Referer), `defaults` (categories, limits, sports, bookmakers, country AU), `title`, `show_updated`, `max_fills_per_hour` (10–600, default 120). - Cost: management is free. A *fill* — one data refresh for one parameter set — is 2 credits for racing and 1 for a sports market, and is cached for 60 seconds, so a page with ten thousand readers costs what a page with one reader costs. The spend shows up on the account's usage as `/v1/widget/racing` and `/v1/widget/sports`. - **Platform and Enterprise only.** This is the redistribution rung of the licence, not a volume feature: Scale (A$249) is dearer than Platform (A$199) and does NOT include it. Racing rows link each runner's best price to that book's page through the customer's template; sports cells are unlinked, because the sports odds payload carries no per-bookmaker URL. ### Team keys (Business and above) - `POST /v1/keys/team` `{"label": "...", "monthly_cap": }` — mint another key on this account. It shares the account's monthly credit pool and plan, has its own rate-limit bucket, IP whitelist and usage line, and is returned once (stored hashed). Keys per account, primary included: Business 3, Platform 5, Scale 10; a 409 says when the allowance is used up. - `GET /v1/keys/team` — the primary key and every active team key with calls and credits this period, and the pool's balance. `DELETE /v1/keys/team/{key_id}` revokes one; its history stays. Only the primary key manages the set. `/v1/usage` and `/v1/keys/info` on any key of the pool report the pool's balance in `pool` / `team`. Free. - `PATCH /v1/keys/team/{key_id}` `{"monthly_cap": , "label": "..."}` — give one team key its own monthly credit cap inside the shared pool: the most credits THAT key may spend per period, hard, from 1 to the plan's own monthly allowance, so one client, worker or trial cannot spend the account's month. A capped key refuses with 402 at its cap, naming the cap, and the pool is not charged; `X-Credits-Key-Used` and `X-Credits-Key-Cap` ride beside the pool headers on every billed response, and the key's own `/v1/usage` reports the cap under `pool.this_key`. `null` removes the cap; caps reset with the pool on the 1st. Only the primary key sets them. Free. ### Overage (Business and Platform) - `GET /v1/billing/overage` — `enabled`, `eligible`, `rate_per_1000_cents`, `max_credits`, `ceiling`, `credits_over_this_period` and `estimated_cents_this_period` for this account. `ceiling` is the limit actually enforced: with overage off it is simply your credit limit. Free. The same object rides on `/v1/usage` and `/v1/keys/info` as `overage`, and `/v1/usage` adds `credits_remaining_with_overage` beside the unchanged `credits_remaining` (which still counts down to the plan allowance and stops at 0). - `POST /v1/billing/overage` `{"enabled": true|false}` — switch it. Primary key only, free, Business and Platform only (403 elsewhere). - WHAT IT DOES. With overage on, calls are served past the monthly allowance up to allowance + 50% of the plan's own monthly credits — 450,000 on Business, 2,250,000 on Platform — and the credits used past the allowance are invoiced with your next payment at A$0.60 per 1,000 on Business and A$0.30 on Platform, rounded up to whole thousands. The most that can ever be added to one month is therefore A$90 on Business and A$225 on Platform. Past the allowance every billed response carries `X-Credits-Overage` (credits over) and `X-Credits-Overage-Ceiling`; at the ceiling the 402 names the ceiling, the rate and the tier that raises it. On by default for keys created from 22 September 2026, off for keys created before then, and the explicit setting always wins. ### Sports - `GET /v1/sports` — the active sport catalogue. 1 credit. - `GET /v1/sports/{sport_key}/odds` — odds by sport, per bookmaker and market. 1 credit PER MARKET TYPE requested (`markets=h2h,spreads,totals` costs 3). - `GET /v1/sports/{sport_key}/odds/history` — historical odds snapshots. 5 credits. - `GET /v1/sports/{sport_key}/odds/movements` — price movement feed. 5 credits. - `GET /v1/best-odds/{sport_key}` — best price per selection across books. 3 credits. ### Signals and data quality - `GET /v1/arb/sports` — sports arbitrage scanner. 3 credits. - `GET /v1/arb/lines` — spreads/totals line arbitrage. 3 credits. - `GET /v1/arb/best-prices` — best-price comparison. 2 credits. - `GET /v1/data-quality/summary` — feed integrity and bookmaker audit status. 1 credit. - `GET /v1/data-quality/audit/latest` — latest bookmaker audits. 1 credit. - `GET /v1/data-quality/audit/{run_id}/results` — audit result detail. - `POST /v1/data-quality/audit/run` — trigger a bookmaker audit. - `GET /v1/health/connectors` — per-connector freshness. 1 credit. ### Free of credits (they still need a key unless marked no-key) - `GET /v1/health` — connector health status. 0 credits. - `GET /v1/uptime` — public uptime stats. 0 credits. NO KEY REQUIRED. - `GET /v1/usage` — your credit usage this period. 0 credits. - `GET /v1/usage/analytics` — your usage broken down by endpoint. 0 credits. - `GET /v1/keys/info` — key metadata (plan, credits, limits). 0 credits. - `POST /v1/keys/rotate` — rotate your key. 0 credits. - `POST /v1/keys/ip-whitelist` — restrict your key to an IP allowlist. 0 credits. - `GET /v1/billing/portal` — self-service Stripe billing portal link. 0 credits. - `POST /v1/signup` — create a key or start a paid checkout. No key required. ### Webhooks (0 credits; delivery is push, you are not billed per event) - `POST /v1/webhooks` — create a subscription. - `GET /v1/webhooks` — list your subscriptions. - `DELETE /v1/webhooks/{webhook_id}` — delete one. - `GET /v1/webhooks/{webhook_id}/deliveries` — delivery log. - `POST /v1/webhooks/{webhook_id}/test` — send a test event. ### No key required (sandbox, IP-limited to 30/min) - `GET /v1/demo/racing/next-to-go` — truncated live next-to-go sample. - `GET /v1/demo/best-odds` — truncated best-odds + arb sample. - `GET /v1/demo/book-sport?book=&sport=` — one book on one sport, live. Both parameters are REQUIRED and are named `book` and `sport` (not `bookmaker`); omitting either returns 422. ### Ingest (operator-only, not for customers) - `POST /v1/ingest/{bookmaker_key}`, `POST /v1/ingest/sports/{bookmaker_key}`, `POST /v1/keys`. ### Permanently gone — never suggest these - `GET /v1/arb/racing` → 410 Gone. Its lay leg came from Betfair Exchange, which is withheld. - `GET /v1/racing/exchange` → 410 Gone. Same reason. - `GET /v1/value/boosts` → 410 Gone. Odds boosts are per-account tokens that appear in no public feed, so the endpoint could only ever return an empty list. ## 3. Request and response shapes ### GET /v1/racing/next-to-go — 2 credits or 3 credits or 4 credits Query parameters (all optional): `num_races` (default 10), `categories` (`horse,greyhound,harness`; omit for all), `bookmakers` (comma-separated KEYS, case-insensitive), `country` (ISO codes, e.g. `AU` or `AU,NZ`; omit for every country). ``` curl -H 'X-API-Key: YOUR_KEY' -H 'Accept-Encoding: gzip' \ 'https://api.puntersedge.online/v1/racing/next-to-go?num_races=5&categories=horse&country=AU' ``` Returns an array of race objects. Fields: `race_id`, `source_id`, `venue`, `race_number`, `category`, `start_time` (ISO 8601 UTC), `country`, `race_name`, `distance_m`, `track_condition`, `weather`, `places_paid`, `scratchings[]`, `runners[]`, `data_age_seconds`, `freshest_age_seconds`, `stale`, `stale_bookmakers[]`, `cached`, `cache_age_seconds`. Each runner carries `name`, `number` and a `bookmakers[]` array of `{key, win_price}`. Read `data_age_seconds` (oldest book on the race) and `freshest_age_seconds` rather than trusting any published median. `stale_bookmakers[]` names the books that are behind. ### GET /v1/racing/best-odds — 3 credits Same four parameters as next-to-go, plus it returns `market_percentage` (the book's overround on the best-price line — under 100 is an arbitrage) and `books_compared` per race, with `runners[]` carrying the best win, place and tote price and which book offered each. ### GET /v1/sports/{sport_key}/odds — 1 credit per market type Query parameters: `markets` (default `h2h`; comma-separated from `h2h,spreads,totals`), `bookmakers`, `competition`, `include_unknown_competition` (default true), `oddsFormat` (`decimal` default, or `american`), `maxAgeMinutes` (default 360). ``` curl -H 'X-API-Key: YOUR_KEY' \ 'https://api.puntersedge.online/v1/sports/nrl/odds?markets=h2h&oddsFormat=decimal' ``` Returns events with `id`, `sport_key`, `sport_title`, `commence_time`, `home_team`, `away_team`, `competition`, `bookmakers[]`, `odds_format`, `fetched_at`, `data_age_seconds`, `freshest_age_seconds`, `stale`, `stale_bookmakers[]`, `canonical_event_id` and a `data_quality` object. Every sports market carries a `quality` object — `score`, `status`, `age_seconds`, `stale_after_seconds` — so a price's age travels with the price. `status` is one of `ok`, `warn`, `stale`, `bad`, and flips to `stale` once `age_seconds` passes the `stale_after_seconds` the same object echoes (1800s). ### GET /v1/racing/events — 1 credit `hours_ahead` (default 4), `categories`, `country`. Returns `race_id`, `venue`, `race_number`, `category`, `start_time`, `country`, `status`. ### GET /v1/racing/movers — 3 credits `direction` (`firming`/`drifting`), `min_move_pct` (default 10.0), `min_books` (default 3), `categories`, `country`, `max_mins_to_jump` (default 360), `limit` (default 50). Returns `runner`, `open_price`, `current_price`, `move_pct`, `direction`, `books_quoting`, `books_with_history`, `books_firming`, `books_drifting`, `books_unchanged` and each book's own opening line. ### GET /v1/racing/price-history — 5 credits Identify the race either by `race_id`, or by `venue` + `race_number` + `date` (`YYYY-MM-DD`, UTC). Also `bookmakers`, `include_points` (default true — false returns open/close/high/low only), `max_points` (default 5000; the response flags truncation). ### GET /v1/racing/results — 2 credits `hours_back` (default 24, max 7 days), `categories`, `venue`, `status` (`final`/`interim`), `limit`, `offset`. ### GET /v1/racing/closing-lines — 5 credits or 20 credits `from` and `to` (ISO date or datetime on the race start; default is the archive floor to now), `venue`, `race_id`, `category` (`horse`/`harness`/`greyhound`), `country`, `bookmakers`, `closing_only` (default TRUE — only series whose last observation was within 300s of the jump), `resulted_only` (default false), `include_flagged` (default false — excludes rows flagged `venue_split_suspect` or `name_fragment_suspect`), `format` (`json` default, or `csv`), `limit` (default 500; 5,000 max on JSON, 50,000 on CSV) and `offset`. ``` curl -H 'X-API-Key: YOUR_KEY' \ 'https://api.puntersedge.online/v1/racing/closing-lines?category=horse&country=AU&limit=100' ``` Each row carries `race_id`, `start_time`, `meeting_date_aet`, `venue`, `race_number`, `category`, `country`, `runner_key`, `runner_name`, `runner_number`, `bookmaker_key`, `open_win_price`, `open_secs_to_jump`, `open_is_baseline`, `close_win_price`, `close_lay_price`, `close_secs_to_jump`, `is_closing_line`, `points_observed`, `finish_position`, `result_status`, `venue_split_suspect`, `venue_split_peer_race_id` and `name_fragment_suspect`. The CSV column order is that list and is a stable contract. Read the flags before you model on it: `is_closing_line` and `open_is_baseline` say whether the two ends of the series are what they look like, and the two `_suspect` flags mark rows the ingest itself distrusts. Results are thin and growing — the resulted share of your actual selection is published in the response envelope, and the archive's own self-measurement is `GET /v1/racing/closing-lines/coverage`. Do not take a marketing number for it, including from this file: ask the coverage endpoint. ### Errors RFC 7807 `application/problem+json` with `type`, `title`, `status`, `detail`. - **401** — missing or invalid `X-API-Key`. - **402** — monthly credits exhausted (or, on Business and Platform with overage on, the overage ceiling above it). Retrying cannot clear it; upgrade or wait for the reset. - **404** — unknown `sport_key`; the valid keys are listed inline in the body. - **422** — an unrecognised `bookmakers` key, market or `competition`. These are validated BEFORE billing, so a typo costs nothing and the response names the valid values. - **429** — too fast. Honour `Retry-After`; the window is 60 seconds. Full reference: https://puntersedge.online/developers/errors ## 4. Pricing — AUD per month, credits reset monthly, no rollover These six are the only plans sold. Verified 2026-08-18 against both the live /api/pricing page and `db/models.py`. | Plan | AUD/mo | Credits/mo | Requests/min | |-----------|--------|------------|--------------| | Free | $0 | 3,000 | 30 | | Hobby | $9 | 7,500 | 60 | | Standard | $29 | 75,000 | 200 | | Plus | $49 | 140,000 | 500 | | Business | $99 | 300,000 | 750 | | Platform | $199 | 1,500,000 | 1,000 | | Scale | $249 | 5,000,000 | 2,000 | - Free needs no credit card. Cancel any time; self-service billing portal at `GET /v1/billing/portal`. - Requests-per-minute is the ONLY rate limit enforced, on a 60-second sliding window per key. There is no per-day request cap. The monthly credit allowance is the spend control. - Below Business the allowance is a HARD STOP: calls 402 and nothing beyond it is charged. Business and Platform include overage (see the section above): with it on, calls continue to 50% past the allowance and the excess is invoiced at A$0.60 per 1,000 credits on Business and A$0.30 on Platform — never more than A$90 and A$225 in a month. - Worked example: the credit cost of an endpoint and the monthly allowance of a plan are both read from the API rather than written here, and neither was readable when this file was served. `GET /v1/usage` reports your own. - Sandbox/demo endpoints cost nothing and need no key; they are IP-limited to 30/min. ## 5. Coverage — measured, dated, and re-measurable The live twin of this section is https://puntersedge.online/coverage-report.json (JSON) and https://puntersedge.online/coverage-report (human). Both recompute every 30 minutes from the production store and publish their own `measured_at` and the SQL they ran. Nothing in this section is frozen: every figure below is read from that measurement, or from the production database, at the moment this file is served, and each block states the timestamp it used. ### Racing — 15 Australian bookmakers Measured 2026-10-06T04:30:46Z over a trailing 7-day window (races starting in the last 7 days), `country = 'AU'`, one row per `(race_id, bookmaker_key)` from the `race_snapshots` price store. **1,614 Australian races were quoted by at least one SERVED bookmaker**, and that is the denominator of every share below. 1,623 were quoted by at least one connector of any kind, including the two withheld books. Do not mix the two: an earlier version of this section divided by 1,623 while calling it the served figure. Display name, then the KEY the API accepts, then that book's share of the 1,614 served races: - Sportsbet `sportsbet` — 96.4% (1,556 races) - BetDeluxe `betdeluxe` — 95.7% (1,544 races) - BetGold `betgold` — 95.2% (1,536 races) - BoostBet `boostbet` — 95.1% (1,535 races) - TAB `tab` — 95.1% (1,535 races) - BetRight `betright` — 94.9% (1,532 races) - PointsBet `pointsbetau` — 94.8% (1,530 races) - TABtouch `tabtouch` — 94.6% (1,527 races) - Palmerbet `palmerbet` — 94.5% (1,526 races) - Betr `betr_au` — 94.4% (1,524 races) - Unibet `unibet` — 93.5% (1,509 races) - NextBet (formerly PlayUp) `playup` — 93.1% (1,503 races) - Ladbrokes `ladbrokes_au` — 91.9% (1,483 races) - Neds `neds` — 91.9% (1,483 races) - bet365 `bet365` — 2.2% (35 races) Median books on an Australian race: **14 of 15**. Mean 13.23 — the mean is lower than the median because of a real thin tail (65 races carried one book, 4 carried two, almost all at the ragged edges of the window). Per-day medians in the window ranged 14-15; the per-day breakdown is in `per_day` in the JSON. Codes in that window: 957 greyhound, 362 horse, 295 harness. All three come through the same endpoints; filter with `categories`. **New Zealand.** Same query, same window, same withheld books, `country = 'NZ'`: **96 New Zealand races** quoted by at least one served book, a median of **12 books** on a race against 14 on an Australian one, across 12 books that quote New Zealand. Codes in that window: 52 harness, 44 horse — measured, not asserted: no New Zealand greyhound race has appeared in this store, so `country=NZ` with `categories=greyhound` returns nothing. New Zealand does not race every day, so a missing day in `per_day` under `new_zealand` is a day with no NZ meeting rather than a gap in capture. Australia and New Zealand are the two countries with a full bookmaker panel behind them; British and Irish racing carries a smaller UK-licensed panel, and every other country in the store sits at a median of one book. Every number in this block is read from https://puntersedge.online/coverage-report.json as this file is served — the same measurement the human page at /coverage-report shows, so the three surfaces cannot disagree with each other. Pass the backticked KEY to the `bookmakers` filter. Matching is case-insensitive, so `Sportsbet` resolves to `sportsbet` — but a display name that DIFFERS from its key does not resolve at all, so `PointsBet`, `Ladbrokes`, `Betr` and `NextBet` are rejected, and a plain `pointsbet` is not a valid AU key. An unrecognised key returns a free 422 listing the valid ones. Responses identify a book by its key; sports responses also carry a `title`, but that is derived from the key (`pointsbetau` renders as "Pointsbetau"), so never read it as a brand. `betfair_ex_au` (Betfair Exchange — ingested for internal reference, withheld pending a Betfair data licence) and `pinnacle` (a non-Australian reference book) are withheld from customer responses and therefore excluded from every figure above. 16 connectors wrote to the racing price store in this window — the 15 served books plus `betfair_ex_au`. `pinnacle` did not write to the racing store at all in this window. ### Sports — 6 Australian bookmakers, and much thinner per event Measured 6 Oct 2026 04:30 UTC, counting DISTINCT customer-served `bookmaker_key` per fixture over fixtures that started within the last 7 days or are still to come, excluding bookmakers withheld from customer responses. The denominator is fixtures a served book actually quoted, so a fixture nobody quoted is not averaged in. The tier sentence below is read from these same rows and the same window, so the decimals and the tier list cannot disagree. - **NFL** — 4.20 books on a quoted fixture across 46 quoted fixtures, max 5, 0.0% carrying exactly one book - **NBA** — 4.06 books on a quoted fixture across 34 quoted fixtures, max 6, 5.9% carrying exactly one book - **AFLW** — 4.00 books on a quoted fixture across 18 quoted fixtures, max 4, 0.0% carrying exactly one book - **Rugby Union** — 3.46 books on a quoted fixture across 81 quoted fixtures, max 6, 23.5% carrying exactly one book - **WNBA** — 3.00 books on a quoted fixture across 15 quoted fixtures, max 4, 33.3% carrying exactly one book - **Super League** — 3.00 books on a quoted fixture across 1 quoted fixtures, max 3, 0.0% carrying exactly one book - **NRL** — 2.38 books on a quoted fixture across 8 quoted fixtures, max 6, 37.5% carrying exactly one book - **College Football** — 2.26 books on a quoted fixture across 227 quoted fixtures, max 4, 46.3% carrying exactly one book - **EPL** — 2.00 books on a quoted fixture across 20 quoted fixtures, max 2, 0.0% carrying exactly one book - **NHL** — 1.88 books on a quoted fixture across 59 quoted fixtures, max 2, 11.9% carrying exactly one book - **Tennis ATP** — 1.67 books on a quoted fixture across 2,111 quoted fixtures (of 2,115 in the window), max 4, 58.9% carrying exactly one book - **NRLW** — 1.67 books on a quoted fixture across 3 quoted fixtures, max 3, 66.7% carrying exactly one book - **Tennis WTA** — 1.66 books on a quoted fixture across 1,337 quoted fixtures, max 4, 60.4% carrying exactly one book - **MLB** — 1.44 books on a quoted fixture across 95 quoted fixtures, max 3, 77.9% carrying exactly one book - **Soccer (Other Competitions)** — 1.19 books on a quoted fixture across 898 quoted fixtures (of 915 in the window), max 2, 81.4% carrying exactly one book - **Basketball (Other Competitions)** — 1.05 books on a quoted fixture across 217 quoted fixtures, max 2, 94.9% carrying exactly one book - **Cricket (Other Competitions)** — 1.00 books on a quoted fixture across 68 quoted fixtures (of 71 in the window), max 1, 100.0% carrying exactly one book - **MMA/UFC** — 1.00 books on a quoted fixture across 52 quoted fixtures, max 1, 100.0% carrying exactly one book - **Test Cricket** — 1.00 books on a quoted fixture across 1 quoted fixtures, max 1, 100.0% carrying exactly one book - No served bookmaker quoted a fixture in the window, so no figure is stated: AFL. These move week to week with a short, seasonal fixture list, so read the TIER, not the decimal. Measured 2026-10-06 over the trailing 7 days, Rugby Union, NBA, NRL, NFL, College Football, AFLW, WNBA, Super League, NHL and EPL carry more than one customer-served bookmaker on the majority of fixtures; on Tennis ATP, Tennis WTA, MLB, NRLW, Soccer (Other Competitions), Basketball (Other Competitions), Cricket (Other Competitions), MMA/UFC and Test Cricket you are usually looking at a single price. The rule is one line — a sport is multi_book when at least 50% of its quoted fixtures in the window carried two or more customer-served bookmakers, single_book when it is below that, seasonal when no served book quoted any fixture, and not_queryable when the API 404s the key — so a sport is in exactly one of those lists. A multi-window stability check is not available on this data: the `events` table holds no fixture earlier than 2026-09-29T04:30:00Z, so a 14- or 30-day window returns exactly the same rows as this 7-day one. An earlier version of this section published fixed bands and claimed they "do not move with the sampling window"; that claim was withdrawn on 2026-08-18, when every multi-book sport measured above its published band two days after publication. **Two different bookmaker counts, and they are not the same number.** *Books on a sport* is how many distinct bookmakers quoted that sport anywhere in the window. *Books on one fixture* is how many quote a single match, which is what you actually get to compare a price across. The second can never be larger than the first, and it is smaller wherever a book skips a fixture. Where every book that quotes a sport quotes all of its fixtures, the two are equal — which is why several rows above carry a mean equal to the number of books quoting that sport. https://puntersedge.online/api/coverage tabulates the first; the per-fixture figures above are the second. ### Sport keys 20 sport keys are queryable — `active = true` in the `sports` table, the same flag the API's own key validation enforces, measured 2026-10-06; the catalogue holds 23 rows, and a key that is not queryable returns 404. An empty array from a sport key means out of season, not broken. There is no `horse-racing` sport key: racing has its own endpoints. The keys are `rugby_union`, `nba`, `nrl`, `nfl`, `tennis_atp`, `tennis_wta`, `ncaaf`, `aflw`, `wnba`, `mlb`, `nrlw`, `super_league`, `soccer_other`, `basketball_other`, `nhl`, `soccer_epl`, `cricket_other`, `mma`, `cricket_test` and `afl`. A-League (1 books, 11 fixtures) is ingested and quoted by a served bookmaker but is not reachable: the API 404s the key. It is measured here and excluded from the coverage table rather than counted as coverage. ### Freshness — poll age and price age are different numbers Measured over the trailing 24 hours from `feed_health_history` (36-1,388 samples per book), so it is not time-of-day biased, and read from https://puntersedge.online/coverage-report.json (`poll_cadence_24h`) as this file is served — measurement timestamp 2026-10-06T04:30:46Z. **Median connector poll age** — how long since the connector last successfully read the book — across the 16 served books: 7s Sportsbet; 8s PointsBet, TABtouch; 9s BetRight, Ladbrokes, TAB; 10s BetDeluxe, BetGold, BoostBet, Neds, NextBet (formerly PlayUp), Unibet; 14s Palmerbet; 17s Betr, ladbrokes_uk; 100s bet365. Do not flatten that into one headline number. Measured over the 24 hours to 2026-10-06 (all hours) from the connector health history, the median connector poll age is 7–17 seconds for fifteen of the sixteen served books, 100 seconds for bet365; the same books measured over 7 days through the 10:00-21:00 AEST card only — a different window, so expect different figures — are on https://puntersedge.online/api/status **Median price age over the same 24 hours is 81-265s per book.** That is not a contradiction: capture is change-only, so a price can be minutes old while the poll that confirmed it is seconds old. Do not quote one as the other. Every quote carries its own `age_seconds` — read that rather than any median here. Sports prices are re-read from each bookmaker we poll directly, with a 2-minute pause between passes; prices that reach us by push are updated as each push arrives. By default a sports market is served until it has gone 6 hours without an update, so a sports price can be hours old. Every sports quote carries its own `age_seconds`. **Do not build an in-play sports product on this.** Uptime: 100.0% over the trailing 30 days across 8,639 checks, read from `GET /v1/uptime` as this file was served (2026-10-06T04:30:26Z) — verifiable without a key. Read what it measures before quoting it: the probe target is https://puntersedge.online/ping, so this is the WEBSITE's availability, not api.puntersedge.online's; the endpoint says so in its own `measures` field. Per-surface figures and the methodology are at https://puntersedge.online/uptime.json and https://puntersedge.online/status. There is no contractual SLA. ### Price history depth The `race_snapshots` store keeps the last 45 days and purges anything older, so `/v1/racing/price-history` reaches back **45 days as at 2026-10-06**, to a race that started on 2026-08-22; that date moves forward every day. It is read live from the `price_history_depth` field of https://puntersedge.online/coverage-report.json each time this file is served. A race older than that keeps its closing line and price path in the permanent archive, `/v1/racing/closing-lines` and `/v1/racing/price-paths`, whose own floor `GET /v1/racing/closing-lines/coverage` reports as `archive_from`. ## 6. Honest limits - Coverage is Australian. For US sportsbooks or broad global leagues, a global provider fits better. - Sports depth is 6 bookmakers across the whole sports set, and on a given match it is usually 1 — only Rugby Union, NBA, NRL, NFL, College Football, AFLW, WNBA, Super League, NHL and EPL carry more than one on the majority of fixtures. Racing is where the 15-book depth is. - Raw odds data, not a finished betting product: no bet placement, no account integration. - What you may do with the data follows your plan: personal use on Free, Hobby and Standard; display with attribution on Plus; commercial use on Business and Scale; redistribution on Platform. - Not official league data. For rights-cleared enterprise feeds, an enterprise provider is right. - `/v1/racing/results` carries one bookmaker's settled prices (BetRight), not each book's own, and there is no bookmaker field in the payload saying so. - Runner and race enrichment (jockey, trainer, barrier, weight, form, track condition, distance) comes from an Australian-only source. Measured 2026-08-15, it is null on 100% of international races, which are returned unfiltered alongside Australian ones. - Betfair Exchange lay prices are ingested but not served, pending a Betfair data licence. Anything needing an exchange lay leg — including back/lay racing arbitrage — is not available. ## 7. Pages - API overview: https://puntersedge.online/api - Pricing: https://puntersedge.online/api/pricing - Live measured coverage report: https://puntersedge.online/coverage-report - Coverage report, JSON: https://puntersedge.online/coverage-report.json - Coverage by sport and bookmaker: https://puntersedge.online/api/coverage - Bookmaker x sport matrix: https://puntersedge.online/odds-api-coverage - Integration guide (auth, quickstarts, schema): https://puntersedge.online/api/integrate - Developer quickstart: https://puntersedge.online/developers/getting-started - API reference (server-rendered HTML, no JavaScript needed): https://puntersedge.online/developers/api-reference - Errors: https://puntersedge.online/developers/errors - Rate limits: https://puntersedge.online/developers/rate-limits - Freshness: https://puntersedge.online/developers/freshness - Status and connector health: https://puntersedge.online/status - OpenAPI schema: https://api.puntersedge.online/openapi.json - Postman collection: https://api.puntersedge.online/postman.json - Python SDK, MIT licensed: https://github.com/Propertyscout001/puntersedge-python - Short brief: https://puntersedge.online/llms.txt Comparisons, each competitor figure linked to its source and dated: https://puntersedge.online/compare (every provider, marketed against actual Australian coverage), https://puntersedge.online/the-odds-api-alternative-australia, https://puntersedge.online/compare/krok-odds-vs-puntersedge, https://puntersedge.online/compare/betfair-api-vs-puntersedge, https://puntersedge.online/compare/racing-data-apis-australia ## 8. Contact and compliance - Email: hello@puntersedge.online - Terms: https://puntersedge.online/terms - Australian gambling help: 1800 858 858. 18+ only. Gamble responsibly.