{"openapi":"3.1.0","info":{"title":"PuntersEdge Odds API","description":"Australian sports odds, and Australian and New Zealand racing odds, from 14 Australian bookmakers in one API. Sports and racing are SEPARATE endpoint families: sports odds live under /v1/sports/{sport_key}/odds, and racing under /v1/racing/next-to-go and /v1/racing/events — there is no 'horse-racing' sport_key. \n\n**Two client settings worth more than any server tuning.** Send `Accept-Encoding: gzip` — responses are JSON and compress about 9x, and a caller that omits the header downloads roughly 55KB where 6KB would do. And reuse the connection (`requests.Session()` or `httpx.Client()`) — measured against this API, a cold TLS handshake costs 137ms versus 40.5ms on a reused connection, so connection reuse alone is a ~70% cut in observed latency. Neither can be fixed from our side.\n\n**Try every endpoint before you write code.** [The Postman collection](https://api.puntersedge.online/postman.json) ships ready-to-run requests, grouped into folders, with query parameters pre-listed and switched off — paste that URL into Postman's Import → Link. Set the `apiKey` collection variable once and every authenticated request picks it up in the `X-API-Key` header; the Sandbox folder needs no key at all.\n\n**What each call costs.** Credits are deducted per successful request. A malformed request — unknown sport_key, bad date, unrecognised bookmaker key — is refused free, before billing. Every response carries `X-Credits-Used` and `X-Credits-Remaining` headers, so checking your balance never costs a call.\n\n| Endpoint | Credits |\n| --- | --- |\n| `/v1/usage`, `/v1/billing/portal`, sandbox (`/v1/demo/*`) | 0 |\n| `/v1/sports` · `/v1/racing/events` · `/v1/racing/venues` · `/v1/racing/track-conditions` · `/v1/racing/closing-lines/coverage` · `/v1/data-quality/summary` · `/v1/data-quality/audit/latest` · `/v1/data-quality/audit/results` · `/v1/health/connectors` | 1 |\n| `/v1/sports/{sport_key}/odds` | 1 per market requested |\n| `/v1/racing/next-to-go` · `/v1/racing/results` · `/v1/racing/changes` · `/v1/racing/jockeys/stats` · `/v1/racing/trainers/stats` · `/v1/arb/best-prices` · `/v1/racing/acceptances` | 2 |\n| `/v1/racing/results/coverage` | 1 |\n| `/v1/racing/best-odds` · `/v1/racing/movers` · `/v1/racing/greyhounds/form` · `/v1/racing/greyhounds/stats` · `/v1/racing/horses/form` · `/v1/racing/horses/backfill` · `/v1/best-odds/{sport_key}` · `/v1/arb/sports` · `/v1/arb/lines` | 3 |\n| `/v1/racing/price-history` · `/v1/sports/{sport_key}/odds/history` · `/v1/sports/{sport_key}/odds/movements` | 5 |\n| `/v1/racing/closing-lines` · `/v1/racing/price-paths` | 5 (JSON) / 20 (CSV) |\n\nThe bulk form backfill bills 3 credits per horse when that horse is DELIVERED, never at submit: a horse we cannot resolve, or that Racing Australia has no page for, costs nothing. Polling a job's status, collecting its results and cancelling it are all free.\n\nWithheld endpoints (`/v1/racing/exchange` and `/v1/arb/racing`, pending a Betfair Exchange data licence) and the retired `/v1/value/*` board answer 410 and charge nothing.\n\n**Coverage.** Race and bookmaker counts measured 2026-09-02; the full-field figures in this paragraph were re-measured 2026-09-10. Racing covers two countries. **Australia**: thoroughbred, harness and greyhound, about 217 races a day, a median of 13 bookmakers quoting each race. **New Zealand**: thoroughbred and harness, about 14 races on a day NZ races (13 racing days in the last 19), a median of 10 bookmakers a race — eleven of the fourteen AU books quote NZ, and there are no New Zealand greyhounds in the feed. Results land a median 4.9 minutes after the jump on AU and 7.3 on NZ (30-day p90: 7.7 and 10.0) and carry placings with settled win and place prices plus the exotic dividends on both; NZ results are placegetters-only. The full-field `runners` finishing order is AU thoroughbred and AU greyhound, and neither is universal — measured 2026-09-10 over 5,577 stored results, AU greyhound carries a field on 2,360 of 3,219 races and AU thoroughbred on 315 of 1,104, while AU harness and both NZ codes carry one on none of their 1,254. Every result row says which case it is in `field_status` (complete/pending/overdue/unavailable/unsupported/not_applicable), and /v1/racing/results/coverage counts them per day, code and venue. `overdue` and `unavailable` are the distinction worth knowing: `overdue` means the pass that normally fills the race has been and gone but the collector is still re-attempting the row (ten days for greyhounds), while `unavailable` means no scheduled pass still selects it — a statement about our schedule, not a promise the field can never arrive. Races in other countries appear because an AU book lists the meeting — they carry a median of ONE bookmaker and are not coverage. Live figures: https://puntersedge.online/coverage-report\n\nCovering AFL, NRL, NBA, Tennis, Cricket, Rugby + AU and NZ thoroughbred and harness racing, and AU greyhounds.","termsOfService":"https://puntersedge.online/terms","contact":{"name":"PuntersEdge","url":"https://puntersedge.online/api","email":"hamish@punters-edge.com"},"license":{"name":"Proprietary — see Terms of Service","url":"https://puntersedge.online/terms"},"version":"1.0.0"},"servers":[{"url":"https://api.puntersedge.online","description":"Production"}],"paths":{"/v1/sports":{"get":{"tags":["Sports"],"summary":"List available sports","description":"Cost: 1 credit. Returns the active sports catalogue as an array of objects containing key, title, group and active fields.\n\nRacing is not in this catalogue. Horse, harness and greyhound racing have their own\nendpoints: GET /v1/racing/next-to-go and GET /v1/racing/events.","operationId":"list_sports_v1_sports_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SportOut"},"title":"Response List Sports V1 Sports Get"},"example":[{"key":"afl","title":"AFL","group":"Australian Rules","active":true,"cached":false,"cache_age_seconds":0},{"key":"nrl","title":"NRL","group":"Rugby League","active":true,"cached":false,"cache_age_seconds":0},{"key":"nba","title":"NBA","group":"Basketball","active":true,"cached":false,"cache_age_seconds":0},{"key":"soccer_epl","title":"EPL","group":"Soccer","active":true,"cached":false,"cache_age_seconds":0}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/sports/{sport_key}/odds":{"get":{"tags":["Sports"],"summary":"Get odds by sport","description":"Cost: 1 credit per requested market. Parameters: sport_key selects the sport (for example afl or nrl); markets is a comma-separated list such as h2h,spreads,totals; bookmakers optionally filters bookmaker keys; oddsFormat accepts decimal or american.\n\n`markets` is deduplicated and validated before billing, so a repeated market is charged\nonce and an unknown one is a free 422 rather than a paid empty list.\n\nThere is no racing sport_key. For horse, harness or greyhound racing use\nGET /v1/racing/next-to-go (2 credits) or GET /v1/racing/events instead — passing\n'horse-racing' here returns 404, because racing is a separate endpoint family.\n\n**Pre-match only. There is no in-play feed.** An event is listed until its `commence_time`\nand leaves this endpoint the moment it starts; no bookmaker connector reads in-play\nmarkets, so nothing here updates during a match. The last pre-match prices for an event\nthat has started are kept in GET /v1/sports/{sport_key}/odds/history and the move feed\nat GET /v1/sports/{sport_key}/odds/movements. (Documented 2026-09-10 after a customer\npolled a game through kick-off expecting live prices.)","operationId":"get_sport_odds_v1_sports__sport_key__odds_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated: h2h,spreads,totals","default":"h2h","title":"Markets"},"description":"Comma-separated: h2h,spreads,totals"},{"name":"bookmakers","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated bookmaker keys, or omit for all. 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.","title":"Bookmakers"},"description":"Comma-separated bookmaker keys, or omit for all. 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."},{"name":"competition","in":"query","required":false,"schema":{"type":"string","description":"Filter to one competition, case-insensitive exact match (e.g. NRLW). A value this sport has never carried is a 422 listing the ones it has, not an empty 200 — see `include_unknown_competition` for why that distinction matters. See the `competition` field on each event.","title":"Competition"},"description":"Filter to one competition, case-insensitive exact match (e.g. NRLW). A value this sport has never carried is a 422 listing the ones it has, not an empty 200 — see `include_unknown_competition` for why that distinction matters. See the `competition` field on each event."},{"name":"include_unknown_competition","in":"query","required":false,"schema":{"type":"boolean","description":"When filtering by `competition`, ALSO return events whose competition is null — not every bookmaker supplies one, and on some sports most of the card is unlabelled (ncaaf 40 of 47 upcoming events, tennis_atp 21 of 72). Because of this the filter is 'this competition OR unlabelled', so a result set is not proof that the competition matched anything: an unrecognised value is rejected with 422 rather than answering with the unlabelled events alone. Set false for a strict exact match that returns only events explicitly labelled with this competition.","default":true,"title":"Include Unknown Competition"},"description":"When filtering by `competition`, ALSO return events whose competition is null — not every bookmaker supplies one, and on some sports most of the card is unlabelled (ncaaf 40 of 47 upcoming events, tennis_atp 21 of 72). Because of this the filter is 'this competition OR unlabelled', so a result set is not proof that the competition matched anything: an unrecognised value is rejected with 422 rather than answering with the unlabelled events alone. Set false for a strict exact match that returns only events explicitly labelled with this competition."},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","description":"decimal or american","default":"decimal","title":"Oddsformat"},"description":"decimal or american"},{"name":"maxAgeMinutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":5,"description":"Exclude bookmaker markets older than this many minutes; default 360 allows 3-hour supplemental feeds while hiding multi-day stale prices.","default":360,"title":"Maxageminutes"},"description":"Exclude bookmaker markets older than this many minutes; default 360 allows 3-hour supplemental feeds while hiding multi-day stale prices."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SportOddsEventOut"},"title":"Response Get Sport Odds V1 Sports  Sport Key  Odds Get"},"example":[{"id":"6b1608c5-a9b8-4235-9a12-6db9656fef74","sport_key":"nrl","sport_title":"NRL","competition":"NRL","odds_format":"decimal","commence_time":"2026-09-09T02:00:00Z","home_team":"Cronulla Sharks","away_team":"Nth Queensland Cowboys","bookmakers":[{"key":"sportsbet","title":"Sportsbet","last_update":"2026-09-07T00:34:45.060482","age_seconds":41,"stale":false,"quality":{"score":99.9,"status":"ok","age_seconds":41,"issues":[],"source_count":5,"stale_after_seconds":1800},"markets":[{"key":"h2h","quality":{"score":99.9,"status":"ok","age_seconds":41,"issues":[],"source_count":5,"stale_after_seconds":1800},"outcomes":[{"name":"Cronulla Sharks","price":1.44},{"name":"Nth Queensland Cowboys","price":2.81}]},{"key":"spreads","quality":{"score":99.9,"status":"ok","age_seconds":41,"issues":[],"source_count":5,"stale_after_seconds":1800},"outcomes":[{"name":"Cronulla Sharks","price":1.87,"point":-7.5},{"name":"Nth Queensland Cowboys","price":1.93,"point":7.5}]},{"key":"totals","quality":{"score":99.9,"status":"ok","age_seconds":41,"issues":[],"source_count":5,"stale_after_seconds":1800},"outcomes":[{"name":"Over","price":1.89,"point":47.5},{"name":"Under","price":1.91,"point":47.5}]}]},{"key":"pointsbetau","title":"Pointsbetau","last_update":"2026-09-07T00:48:33.497304","age_seconds":12,"stale":false,"quality":{"score":100.0,"status":"ok","age_seconds":12,"issues":[],"source_count":5,"stale_after_seconds":1800},"markets":[{"key":"h2h","quality":{"score":100.0,"status":"ok","age_seconds":12,"issues":[],"source_count":5,"stale_after_seconds":1800},"outcomes":[{"name":"Cronulla Sharks","price":1.42},{"name":"North Queensland Cowboys","price":2.85}]},{"key":"spreads","quality":{"score":100.0,"status":"ok","age_seconds":12,"issues":[],"source_count":5,"stale_after_seconds":1800},"outcomes":[{"name":"Cronulla Sharks","price":1.9,"point":-7.5},{"name":"North Queensland Cowboys","price":1.9,"point":7.5}]}]},{"key":"tab","title":"Tab","last_update":"2026-09-07T00:48:33.220409","age_seconds":12,"stale":false,"quality":{"score":100.0,"status":"ok","age_seconds":12,"issues":[],"source_count":5,"stale_after_seconds":1800},"markets":[{"key":"h2h","quality":{"score":100.0,"status":"ok","age_seconds":12,"issues":[],"source_count":5,"stale_after_seconds":1800},"outcomes":[{"name":"Cronulla","price":1.43},{"name":"Nth Qld","price":2.85}]},{"key":"totals","quality":{"score":100.0,"status":"ok","age_seconds":12,"issues":[],"source_count":5,"stale_after_seconds":1800},"outcomes":[{"name":"Over","price":1.87,"point":47.5},{"name":"Under","price":1.87,"point":47.5}]}]}],"canonical_event_id":"nrl:cronulla-sharks:nth-queensland-cowboys:2026-09-09","data_quality":{"score":99.9,"status":"ok","source_count":5,"bookmaker_count":3},"fetched_at":"2026-09-07T00:48:33.497304","data_age_seconds":41,"freshest_age_seconds":12,"stale":false,"stale_bookmakers":[],"cached":false,"cache_age_seconds":0}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/next-to-go":{"get":{"tags":["Racing"],"summary":"Next races to run","description":"Cost: 2 credits. Returns the next AU and NZ races to run with runners and prices; categories can filter horse, greyhound and harness racing. NZ runs thoroughbred and harness only — there are no New Zealand greyhounds in the feed.\n\n**One call can be the whole priced card.** `num_races=200` returns every race currently\ncarrying odds — the practical \"bulk odds\" pull. Since 2026-09-03 this endpoint looks 24\nhours ahead rather than six, so on an Australian morning it returns the whole day's card\nwhere it used to stop mid-afternoon. Which races carry prices that far out is a property\nof the bookmakers, not of this API: several open their fixed-odds markets the night\nbefore (TAB's own price history on a race probed 2026-09-03 ran back to the previous\nevening) while others open close to the jump, so read `refresh_tier` and each\nbookmaker's `age_seconds` rather than assuming every race has a full panel.\n/v1/racing/events (hours_ahead up to 24) is the day's CARD, priced or not. To stop\nschedule-polling entirely, subscribe a webhook to `race.odds_open` — it fires the moment\na race's first prices land, and this endpoint (or price-history by race_id) serves them\nfrom then on.\n\n**`venue` selects a specific meeting.** There is no race-class or black-type field anywhere\nin this API, so \"give me the Group 1s\" cannot be expressed directly. The published\nGroup 1 calendar names the meeting and race number ahead of time, so\n`venue=Randwick` plus the race number off `race_number` is how that selection is actually\nmade. Combine with `categories=horse` to drop the harness and greyhound cards that share\na venue name at some tracks.\n\n**Freshness is reported worst-first.** `data_age_seconds` is the age of the OLDEST bookmaker\nquote in the race and `stale` is true when that exceeds the race's `refresh_tier`\nthreshold, so the flag describes the\nstaleness you are exposed to rather than the best leg in the payload. `stale_bookmakers`\nnames the affected books, `freshest_age_seconds` is the best-case age, and every entry in\n`bookmakers` carries its own `age_seconds`, `last_update`, `stale` and `refresh_tier`. A\nstale book does not invalidate the race — drop that leg.\n\n**`refresh_tier` says which staleness rule applies.** Inside three hours of the jump a race\nis `live`: every book re-prices it on every 8-20s poll and `stale` fires past 120 seconds.\nBeyond three hours it is `card`: books re-price it about every 15 minutes, which is what\nmakes a full day's coverage affordable within their rate limits, and `stale` fires past\n1800 seconds. `age_seconds` is the true age in both — only the threshold moves. The tier\nis also published on EVERY bookmaker entry, and a book's tier can be `card` on a `live`\nrace: Betr's upstream rate-limits us, so it stops re-pricing every poll 30 minutes out\nrather than three hours (PlayUp from 6 minutes out, on its request budget), and its\nquotes on races 30-180 minutes from the jump carry\n`refresh_tier: \"card\"`, are judged against 1800 seconds, and report their true\n`age_seconds` (typically 5-8 minutes). If you apply your own freshness rule, branch on\neach entry's `refresh_tier` rather than on a single number, or every price on the\nmorning card — and every Betr price outside half an hour — will read as stale.\n\nScratched runners are returned in `scratchings` only. They are never in `runners`, even\nwhile a book is still publishing a price for them.\n\n**Every bookmaker leg carries `source_url` — a deep link to that book's own page for this\nrace.** Built at ingest from the book's native race id, on the same route its iOS/Android\napp-links use, so on a phone it opens the bookmaker's app on the exact race. It is a plain\nrace-page URL: you may append your own query parameters (affiliate or campaign tags) —\nwhether a bookmaker's programme credits them is between you and that programme, and nothing\nhere promises attribution. The field is absent when no reliable URL can be built — a\nguessed link that lands a punter on the wrong page is worse than a missing one — which\ntoday means betdeluxe, boostbet and betgold (their sites publish no provable race-page\nroute; measured 2026-09-01, every other book links 100% of its quotes).\n\n**Same Race Multi legs** (`top2_price`, `top3_price`, `top4_price`, since 2026-09-05).\nThe price for the runner to finish in the first two, three or four — the legs a Same\nRace Multi is built from. Present on a bookmaker entry only when that book publishes\nthe leg: Ladbrokes, Neds and PointsBet carry all three, Sportsbet where its racecard\noffers the SRM markets; every other book omits the keys. Guaranteed\n`win_price >= top2_price >= top3_price >= top4_price > 1.0` — a leg that breaks the\norder at the source is dropped rather than published. A Top 3 on a race paying three\nplaces is the same bet as the place and prices the same; on a race paying two it is\nnot. These legs do not advance `updated_at` — /v1/racing/changes keys on the win price.\n\n**Runner-level betslip links** (`betslip_win_url`, `betslip_place_url`,\n`betslip_app_only`). Where `source_url` opens the book's race PAGE, these open the book's\nBETSLIP with this runner already selected. Coverage, honestly:\n\n| book | opens in | preselects |\n|---|---|---|\n| `unibet` | web browser | runner **and** market (fixed win or fixed place) |\n| `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 |\n| `ladbrokes_au` | **native app only** (iOS/Android 8.69.0+) | runner and market |\n| `neds` | **native app only** | runner and market |\n\nEvery other bookmaker has no such route and carries none of these fields.\n`betslip_app_only` is `true` for Ladbrokes and Neds: in a desktop browser those URLs\nredirect to the app-download page, so send them to a phone with the app installed, or\nfall back to `source_url`. Measured 2026-09-03. The Ladbrokes pattern is vendor\ndocumented; the Neds one is **inferred** from it (same platform, same entrant ids) and\nhas not been tap-tested on a Neds install.\n\nThese routes are read from the bookmakers' own site code. **None of them is published or\nsupported by the bookmaker**, so any can change without notice. A daily automated check\nloads a live Unibet link and a live TAB link in a real browser and confirms the betslip\npopulates; when a book fails that check its betslip fields go **null** until it passes\nagain, so a broken route degrades to \"no link\" rather than sending a member to a dead\npage. Ladbrokes and Neds cannot be checked this way - a headless browser cannot open a\nnative app - and ship on the vendor's documentation instead.\n\nThe fields are **absent** on a book that has no link, exactly as `source_url` is; treat\nabsent and `null` alike as \"no betslip link\". Nothing about `source_url` changed.\n\nLike `source_url` these are plain URLs, and extra query parameters you append are passed\nthrough untouched. A link that opens a native app leaves the browser, so cookie-based\nattribution set by your own site does not travel with it.\n\n**A foreign race usually has nothing to compare.** Measured over 13 days, a race outside\nAustralia and New Zealand carries a median of one bookmaker's price and at most three —\nabout one in nine has a second book. Australia and New Zealand are the only two countries\nwith a real panel: re-measured 2026-09-02 over 7 days across the books served to customers,\nan Australian race carries a median of 13 quoting bookmakers and a New Zealand race a median\nof 10. So a cross-book comparison on foreign racing is usually comparing one price with\nitself.\n\n**Hong Kong** (feature_requests #29, 2026-09-09). Sha Tin and Happy Valley are served\nunder `country=HK` whenever an Australian bookmaker quotes them. **Prices only** — win\nand place, from the books that carry the meeting. No results, no form, no race\nconditions and no `runner_ref`: those come from Racing Australia and cover AU\nthoroughbreds only, so every form field on a Hong Kong race is null. Coverage follows\nthe HKJC's own calendar rather than ours — Happy Valley on Wednesday evenings and Sha\nTin on Saturday or Sunday afternoons (AEST), and nothing at all on the days between.\nDepth is the foreign-race depth described above, not the Australian panel.\n\n**`country=AU` also drops Australian races that are not labelled yet.** A race's country\nis set when the meeting is resolved, so the later races on an Australian card sit in the\nwindow with `country` null for a while. In a sample window 33 of 65 races were unlabelled\nand every one was at an Australian venue — Angle Park, Menangle, Ballarat, Albury and the\nlike, most of them the same venues carrying labelled races earlier in the day. Filter on\n`country=AU` and you lose them; leave it off and read `country` per race instead.\n\n**An Australian race gathers books as it approaches the jump**, so depth depends on how far\nout you look rather than sitting at one number. Measured 2026-08-17: a median of 10\nbookmakers inside 30 minutes of the jump, 9 at 30-60 minutes, 5 at one to two hours, and 2\nbeyond that. Since this endpoint returns the NEXT races, expect the near-jump end of that\nrange. A window-wide average is not meaningful here — it just tracks whichever mix of\nstart times happens to be loaded.\n\nHow much of the card is foreign is NOT a fixed ratio: the foreign share of the window runs\nnear zero through the Australian afternoon and reaches 100% overnight. Filter on `country`\nrather than assuming a mix.\n\n**Form fields and race conditions are AU/NZ-only, and which of them you get is decided by\nthe racing code, not by the country.** For AU thoroughbreds, Racing Australia's own\nacceptance pages fill the gaps: runner `barrier`/`jockey`/`trainer`/`weight`/`form` where\nno book supplies them, `runner_ref` (the stable ra: horsecode — the join key to\n/v1/racing/results runners and /v1/racing/horses/form), and the race-level `race_class`,\n`conditions` (RA's line verbatim), `prize_total` and `rail`. Otherwise the runner fields\n`barrier`, `jockey`, `trainer`,\n`weight` and `form`, and the race-level `race_name`, `distance_m`, `track_condition`,\n`weather` and `places_paid`, are supplied by exactly one bookmaker in the panel, and that\nconnector fetches Australian and New Zealand meetings only. A race outside Australia and\nNew Zealand therefore carries none of them, and this is a structural ceiling rather than a\nthin sample: across 13 days of snapshots that book appears on 2,813 Australian and 166 New\nZealand races and on none of the 5,624 races held in the 31 other countries the panel\nquotes.\n\nOn Australian races the fields you get depend on the code. Measured 2026-08-17 over the\nsame runner merge this endpoint serves: greyhound runners carried `trainer` 99%, `form` 99%\nand `barrier` 99%, with `jockey` 0% and `weight` 16%; harness runners carried `trainer`\n100%, `form` 96% and `jockey` — the driver — 100%, with `barrier` and `weight` 0%. Those\nzeroes are what that book publishes per code, not gaps in Australian coverage. The measured\nwindow held no Australian thoroughbred meeting, so do not read these as fixed rates.\nNull-check every one of these fields rather than inferring its presence from `country`.","operationId":"next_to_go_v1_racing_next_to_go_get","parameters":[{"name":"num_races","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Up to 200. Pass 200 to bulk-pull every currently quoted race in one call instead of paging.","default":10,"title":"Num Races"},"description":"Up to 200. Pass 200 to bulk-pull every currently quoted race in one call instead of paging."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"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.","title":"Limit"},"description":"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."},{"name":"categories","in":"query","required":false,"schema":{"type":"string","description":"horse,greyhound,harness or omit for all","title":"Categories"},"description":"horse,greyhound,harness or omit for all"},{"name":"bookmakers","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated 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.","title":"Bookmakers"},"description":"Comma-separated 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."},{"name":"include_unresolved","in":"query","required":false,"schema":{"type":"boolean","description":"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.","default":false,"title":"Include Unresolved"},"description":"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."},{"name":"country","in":"query","required":false,"schema":{"type":"string","description":"ISO country codes, comma-separated (e.g. AU or AU,NZ). Omit for every country. HK serves Sha Tin and Happy Valley (prices only, no results or form) on the HKJC race nights. Most of an AU card is unresolved until the meeting is confirmed, so pair this with include_unresolved=true or you will silently lose them. A race outside AU/NZ carries a median of ONE bookmaker (max 3; about one in nine has a second), while an Australian race gathers books toward the jump (median 10 inside 30 minutes, 2 at 2-4 hours). NOTE that this filter also excludes Australian races whose country is not labelled yet — typically the later races on the card — so it is not a clean AU/foreign split. Omit it and read `country` per race if you need those.","title":"Country"},"description":"ISO country codes, comma-separated (e.g. AU or AU,NZ). Omit for every country. HK serves Sha Tin and Happy Valley (prices only, no results or form) on the HKJC race nights. Most of an AU card is unresolved until the meeting is confirmed, so pair this with include_unresolved=true or you will silently lose them. A race outside AU/NZ carries a median of ONE bookmaker (max 3; about one in nine has a second), while an Australian race gathers books toward the jump (median 10 inside 30 minutes, 2 at 2-4 hours). NOTE that this filter also excludes Australian races whose country is not labelled yet — typically the later races on the card — so it is not a clean AU/foreign split. Omit it and read `country` per race if you need those."},{"name":"venue","in":"query","required":false,"schema":{"type":"string","description":"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. This narrows the SAME window the endpoint always serves (24 hours ahead since 2026-09-03, six before that); it does not look beyond it, so a venue with no race inside the window returns an empty list rather than an error.","title":"Venue"},"description":"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. This narrows the SAME window the endpoint always serves (24 hours ahead since 2026-09-03, six before that); it does not look beyond it, so a venue with no race inside the window returns an empty list rather than an error."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NextRaceOut"},"title":"Response Next To Go V1 Racing Next To Go Get"},"example":[{"race_id":"2f0e2df8-5386-4aae-bbdb-7b784dee3e43","source_id":"src_9e2b7c41aa","venue":"Grafton","venue_id":"grafton","venue_canonical":"Grafton","venue_site":"grafton","race_number":1,"category":"horse","start_time":"2026-09-07T03:20:00Z","country":"AU","race_name":"TEAM AIDAN MARKETS @ CRJC 22 NOVEMBER CB MDN PLT","distance_m":1700,"track_condition":"Soft (5)","weather":"Clear Sky","places_paid":3,"track_condition_changed_at":null,"race_class":"MDN","conditions":"Maiden Plate","prize_total":27000,"rail":"True","track_name":"Grafton","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"}],"runners":[{"name":"Fabulous Fiano","number":2,"barrier":6,"jockey":"Matthew McGuren","trainer":"Matthew Dunn","bookmakers":[{"key":"tab","win_price":8.0,"place_price":2.6,"age_seconds":6,"stale":false,"refresh_tier":"live","source_url":"https://www.tab.com.au/racing/2026-09-07/GRAFTON/GRA/R/1"},{"key":"sportsbet","win_price":7.5,"place_price":2.5,"top2_price":2.5,"top3_price":1.53,"top4_price":1.2,"age_seconds":9,"stale":false,"refresh_tier":"live","source_url":"https://www.sportsbet.com.au/horse-racing/australia-nz/grafton/race-1-10895656"},{"key":"ladbrokes_au","win_price":7.0,"place_price":2.6,"top2_price":2.6,"top3_price":1.6,"top4_price":1.22,"age_seconds":11,"stale":false,"refresh_tier":"live","source_url":"https://www.ladbrokes.com.au/racing/grafton/1564cb51-fcbb-4038-b488-372027041b0e","betslip_win_url":"https://www.ladbrokes.com.au/racing/grafton/1564cb51-fcbb-4038-b488-372027041b0e/fw","betslip_place_url":"https://www.ladbrokes.com.au/racing/grafton/1564cb51-fcbb-4038-b488-372027041b0e/fp","betslip_app_only":true},{"key":"betright","win_price":7.0,"place_price":2.0,"age_seconds":14,"stale":false,"refresh_tier":"live","source_url":"https://www.betright.com.au/racing/grafton/1/62386616/win"},{"key":"unibet","win_price":6.0,"place_price":2.4,"age_seconds":12,"stale":false,"refresh_tier":"live","source_url":"https://www.unibet.com.au/racing#/event/202609070300.T.AUS.grafton.1"}]},{"name":"Foxwedge Arrow","number":3,"barrier":2,"jockey":"Ben Looker","trainer":"Kris Lees","bookmakers":[{"key":"tab","win_price":51.0,"place_price":11.0,"age_seconds":6,"stale":false,"refresh_tier":"live","source_url":"https://www.tab.com.au/racing/2026-09-07/GRAFTON/GRA/R/1"},{"key":"ladbrokes_au","win_price":46.0,"place_price":9.5,"age_seconds":11,"stale":false,"refresh_tier":"live","source_url":"https://www.ladbrokes.com.au/racing/grafton/1564cb51-fcbb-4038-b488-372027041b0e"},{"key":"sportsbet","win_price":41.0,"place_price":9.0,"age_seconds":9,"stale":false,"refresh_tier":"live","source_url":"https://www.sportsbet.com.au/horse-racing/australia-nz/grafton/race-1-10895656"}]}],"data_age_seconds":14,"freshest_age_seconds":6,"refresh_tier":"live","stale":false,"stale_bookmakers":[],"cached":false,"cache_age_seconds":0}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/events":{"get":{"tags":["Racing"],"summary":"Upcoming race list","description":"Cost: 1 credit. The CARD - every AU and NZ race scheduled in the requested window (up to\n24 hours ahead), optionally filtered by racing category and country. AU cards carry\nthoroughbred, harness and greyhound; NZ cards carry thoroughbred and harness.\n\nHong Kong (`country=HK`, feature_requests #29, 2026-09-09) is listed too, on the HKJC's\nown nights — Happy Valley on Wednesday evenings and Sha Tin on Saturday or Sunday\nafternoons, AEST — whenever an Australian bookmaker quotes the meeting. Thoroughbred\nonly, and PRICES ONLY on /v1/racing/next-to-go and /v1/racing/best-odds: a Hong Kong\nrace carries no results, form, race conditions or `runner_ref`, because those come from\nRacing Australia and cover AU thoroughbreds only. Foreign meetings other than Hong Kong\nstill appear here whenever a book in the panel lists them; this endpoint has never\nfiltered them out.\n\nThis is the schedule endpoint, and it lists a race before any book has priced it. Since\n2026-09-01 the feed reads the early markets (Sportsbet's full card 3-12h out, BetRight's\nearly lane, a PointsBet wide sweep), so the FIRST price on a race now lands well ahead of\nthe jump: measured over the 48h to 2026-09-03 11:00 UTC, median 10.1h before the jump on AU\nthoroughbreds (79 of 80 races priced 3h+ out), 5.3h on AU greyhounds, 6.8h on AU harness.\nThat first price is one or two books; the full board still fills inside about an hour of\nthe jump. A race listed here with no prices yet is one no book has opened -\n/v1/racing/next-to-go carries the priced window, and the `race.odds_open` webhook fires\nthe moment a race's first prices land.\n\n`scratchings` is served here as well as on /v1/racing/next-to-go, so a scratching is visible\nfrom the moment a book reports it rather than only once the race enters the priced window.\nIts entries are {name, number, barrier, scratched_at, late, emergency}, merged across\nevery reporting book — `scratched_at` (UTC) is the book's own stamp where it sends one\n(Ladbrokes, Neds, BetRight) and Racing Australia's official scratching sheet otherwise\nfor AU thoroughbreds, read every two hours; `late` and `emergency` come from the same\nsources. On greyhound\ncards some entries are box-vacancy placeholders - a literal '....' or 'Vacant Box' - and\nseveral books can report the same empty box separately, so count boxes by `number`, not by\nlist length.\n\n`status` is written when the race row is created and never revised, so every race in this\nwindow reads 'open'. `market_status` and `inplay` are the live betting-market state.\n\n`market_closed_at` and `inplay_at` are the race lifecycle: when the market was first seen\nshut, and when the race was first seen in-play — the actual off, where `start_time` is the\nadvertised one and moves. Both are POLL-OBSERVED: racing polls every 8s inside 120s of a\njump, 15s inside 10 minutes and 20s otherwise, so each is the first poll at which the state\nwas seen and lands within about one poll interval after it. Both are SET ONCE, so a market\nthat flickers SUSPENDED -> OPEN -> SUSPENDED reports the first suspension. Null never means\nthe market stayed open. This endpoint serves upcoming races and the live row is purged 10\nminutes after the jump; /v1/racing/results carries the same fields permanently.\n\n**Who reports market state, and what you see.** Two feeds do. PointsBet closes its book at\nthe real jump — measured 2026-09-03 over 9 live races followed across their jump, 0.2 to\n1.4 minutes AFTER the advertised start on AU/NZ cards — and that is what populates `market_closed_at` for most races. The\nBetfair Exchange reports state while it quotes the market, which ends about two minutes\nBEFORE the jump (measured 2026-09-01 on Horsham R3, advertised 05:24 UTC: betfair last seen\n119s before the off), which is why these fields read null on every race before 2026-09-03.\nExchange-sourced market state is withheld from customer plans pending a data licence, so a\nrace whose state only the exchange saw reads null for customers; bookmaker-sourced state is\nserved to everybody. `market_state_source` names the feed behind the values you receive, and\nis itself null whenever they are withheld.\n\n`inplay_at` stays exchange-only: bookmakers do not run racing in-play, so a bookmaker feed\ncan say when it CLOSED its book but not when the race went. For a race the exchange has\nstopped quoting, `market_closed_at` is the closest measured instant to the off.","operationId":"race_events_v1_racing_events_get","parameters":[{"name":"hours_ahead","in":"query","required":false,"schema":{"type":"integer","maximum":24,"minimum":1,"default":4,"title":"Hours Ahead"}},{"name":"categories","in":"query","required":false,"schema":{"type":"string","title":"Categories"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","description":"ISO country codes, comma-separated (e.g. AU). Omit for every country. HK lists Sha Tin and Happy Valley on the HKJC race nights (prices only, no results or form). Pair with include_unresolved=true or you will silently lose races whose meeting is not confirmed yet.","title":"Country"},"description":"ISO country codes, comma-separated (e.g. AU). Omit for every country. HK lists Sha Tin and Happy Valley on the HKJC race nights (prices only, no results or form). Pair with include_unresolved=true or you will silently lose races whose meeting is not confirmed yet."},{"name":"include_unresolved","in":"query","required":false,"schema":{"type":"boolean","description":"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.","default":false,"title":"Include Unresolved"},"description":"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."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RaceEventOut"},"title":"Response Race Events V1 Racing Events Get"},"example":[{"race_id":"2f0e2df8-5386-4aae-bbdb-7b784dee3e43","venue":"Grafton","venue_id":"grafton","venue_canonical":"Grafton","venue_site":"grafton","race_number":1,"category":"horse","start_time":"2026-09-07T03:20:00Z","country":"AU","race_name":"TEAM AIDAN MARKETS @ CRJC 22 NOVEMBER CB MDN PLT","distance_m":1700,"track_condition":"Soft (5)","weather":"Clear Sky","race_class":"MDN","conditions":"Maiden Plate","prize_total":27000,"rail":"True","track_name":"Grafton","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,"status":"open","market_status":null,"inplay":null,"market_closed_at":null,"inplay_at":null,"market_state_source":null},{"race_id":"c2cfc9b1-a037-4bcb-841f-5496bb0c9b82","venue":"Tatura","venue_id":"tatura","venue_canonical":"Tatura","venue_site":"tatura","race_number":1,"category":"horse","start_time":"2026-09-07T03:30:00Z","country":"AU","race_name":"Shepparton Club Maiden Plate","distance_m":1000,"track_condition":"Heavy (8)","weather":"Broken Clouds","race_class":"MDN","conditions":"Maiden Plate","prize_total":27000,"rail":"True Entire Circuit","track_name":"Tatura","scratchings":[],"track_condition_changed_at":null,"status":"open","market_status":null,"inplay":null,"market_closed_at":null,"inplay_at":null,"market_state_source":null}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/venues":{"get":{"tags":["Racing"],"summary":"Canonical venue directory","description":"Cost: 1 credit. Every venue this API has actually served recently, with its canonical\nidentity — built for joining our data against official sources (Racing Australia, form\nguides) that spell venues differently from bookmakers.\n\nTHE LIST IS MEASURED, the identity is curated. Rows come from the live race window plus\nthe trailing 60 days of results — a venue appears because races actually ran there, so\nthe directory can never advertise coverage that does not exist. Each observed spelling is\nthen resolved by the same registry that stamps venue_id / venue_canonical / venue_site on\nevery racing response (api/venue_registry.py), so this endpoint and those fields can\nnever disagree.\n\n`venue_site` is the join key for multi-track complexes: Sandown, Sandown Hillside,\nSandown Lakeside and the Sandown Park greyhound track all carry site \"sandown\";\nQ Straight / Q1 Lakeside / Q2 Parklands carry \"the-q\". `spellings` lists the raw venue\nstrings observed under each id, so you can map historical data you have already stored.","operationId":"racing_venues_v1_racing_venues_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VenueOut"},"title":"Response Racing Venues V1 Racing Venues Get"},"example":[{"venue_id":"grafton","venue_canonical":"Grafton","venue_site":"grafton","country":"AU","categories":["horse"],"spellings":["GRAFTON","Grafton"]},{"venue_id":"sandown-hillside","venue_canonical":"Sandown Hillside","venue_site":"sandown","country":"AU","categories":["horse"],"spellings":["Sandown - HILLSIDE","Sandown Hillside"]},{"venue_id":"sandown-park","venue_canonical":"Sandown Park","venue_site":"sandown","country":"AU","categories":["greyhound"],"spellings":["Sandown Park"]}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/results":{"get":{"tags":["Racing"],"summary":"Settled race results","description":"Cost: 2 credits. Settled AU/NZ race results — finishing order with the bookmaker's own\nsettled fixed prices, plus scratchings and their deductions.\n\n`status` is `interim` until the placings have been stable for 30 minutes, then `final`.\nA race that did not run is `abandoned` (Racing Australia's official notice, applied to every\nrace of the meeting, with `status_note` carrying RA's word: abandoned, postponed, or\ntransferred to another venue) — empty `placings`, null `runners`, filterable with\n`status=abandoned`.\nSettle only against `final`: an interim placing can still change on protest.\n\n**A `runners` entry is not necessarily a starter.** `status` says whether the runner ran:\n`ran`, or `scratched` / `late_scratching` / `reserve` for one that did not. Since\n2026-09-08 an AU thoroughbred scratched at the barrier arrives as `status: scratched`\ncarrying `abnormal: 'SB'`; before that it was published as `status: ran` with a null\n`position`, which told you the horse started when the market had refunded it. Such a row\ncan still carry `sp` — the last price RA printed for it. That price is NOT what its\n`deductions` row was struck from (measured: 1/`sp` matches the published win deduction on\nnone of the five stored priced rows), so take the deduction from `deductions` itself and\nnever derive it. Settle on `status`, not on presence in `runners`. The thoroughbred and\ngreyhound collectors do NOT spell a late scratching the same way; the `runners` field\ndescription says exactly how to tell them apart.\n\n**Settled prices can be null while `status` is `interim` — this is the SP timing, not a\ngap.** The source book publishes the finishing order before its settled dividends, so a\nresult fetched in the minutes after the jump can carry placings whose `win_price` /\n`place_price` are still null. They fill in during the interim window: measured over 7\ndays (1,541 final results, 2026-09-01), every `final` result carried settled prices and\nnone was missing them. If you hit null prices, you are inside that window — poll again\nor wait for `final`, which is already the only status you should settle against.\n\n`sp` on `runners` and on `placings` is the OFFICIAL starting price — not our consensus, and\nnot the fixed price the bookmaker settled at. The two differ, and both are returned so you\ncan compare them. Greyhound SP is the Topaz feed's, thoroughbred SP is Racing Australia's\n(its favouritism marker lands in `sp_note`), and harness SP is always null because no source\nwe hold publishes it. A null `sp` means unknown — never zero.\n\n`race_id` joins a result to /v1/racing/price-history, and to /v1/racing/next-to-go while the\nrace was still open — use it rather than matching on venue + race number + start time.\n\n`country` splits the feed: `country=AU` and `country=NZ` are the two-URL form of an AU/NZ\nsplit. NZ rows come from the bookmaker results feed, which publishes placegetters, so they\ncarry `placings` without the full `runners` finishing order the Racing Australia collector\nadds to AU thoroughbred results.\n\n`dividends` carries the declared win/place dividends per placegetter per product and the\nexotic dividends (quinella, exacta, trifecta, first four), each as a return on a $1 stake.\nExotics are declared a few minutes after the placings, so they land on a later poll than the\nresult itself — see the field description for the full shape.\n\n`market_closed_at` and `inplay_at` are the race lifecycle, captured live and kept here\npermanently: when the market was first seen shut, and when the race was first seen in-play —\nthe actual off, where `start_time` is the advertised one and moves. Both are POLL-OBSERVED\n(the price loop runs every 8s inside 120s of a jump), so each is the first poll at which the\nstate was seen rather than the exact instant, and both are SET ONCE. They are copied from\nthe live race row when the result is written. That row is deleted 10 minutes after the jump\nand results land a median 4.9 minutes after the start (p90 7.4), so a late result keeps\nnull. Null never means the market stayed open. Capture began 2026-09-01, so every result\nbefore that date carries null.\n\n**Who reports market state, and what you see.** Two feeds do. PointsBet closes its book at\nthe real jump — measured 2026-09-03 over 9 live races followed across their jump, 0.2 to\n1.4 minutes AFTER the advertised start on AU/NZ cards — and that is what populates `market_closed_at` for most races. The\nBetfair Exchange reports state while it quotes the market, which ends about two minutes\nBEFORE the jump (measured 2026-09-01 on Horsham R3, advertised 05:24 UTC: betfair last seen\n119s before the off), which is why results between 2026-09-01 and 2026-09-03 carry null.\nExchange-sourced market state is withheld from customer plans pending a data licence, so a\nrace whose state only the exchange saw reads null for customers; bookmaker-sourced state is\nserved to everybody. `market_state_source` names the feed behind the values you receive.\n`inplay_at` stays exchange-only — bookmakers do not run racing in-play.\n\n**Coverage:** results collection began 2026-08-15, so `hours_back` cannot reach further than\nthat no matter what you pass. Every row carries `coverage_from`, the earliest result\ncurrently retained, so an empty window is distinguishable from an unsupported one.\n\n**Field coverage, which is a different question, and `runners: null` is not its answer.**\nEvery row now carries `field_status`, `field_source` and `field_note`. `field_status` is\n`complete` | `pending` | `overdue` | `unavailable` | `unsupported` | `not_applicable`: a\nfield that is here; one the pass that NORMALLY fills this race has not reached yet, with\nthat instant in `field_note`; one where that pass has been and gone without writing a\nfield but the collector still re-attempts the row; one no scheduled pass still selects,\nits collector's own candidate window having closed; a code no collector has ever existed\nfor; and a race that did not run. The scheduled progression is pending -> overdue ->\nunavailable, one direction only, and every step is bounded by a collector's real candidate\nquery rather than by its next run — so nothing here asks you to poll forever. Read\n`overdue` carefully: it is NOT terminal. Most rows that reach it stay null, so do not build\non a field arriving, but do not write the race off either. And any of those three can\nbecome `complete`, `unavailable` INCLUDED — nothing un-writes a `runners` array, so a\nhand-run collector, a widened window or a late upstream publication can still fill a row we\nhave stopped scheduling passes for. Treat `unavailable` as 'nothing further is scheduled',\nnever as 'this can never arrive', and re-read rather than caching it as final. The full\nper-state contract is on the `field_status` field itself. Measured 2026-09-10 across the\nretained store (5,577 results): AU greyhound carries a field on 2,360 of 3,219 races and AU\nthoroughbred on 315 of 1,104, while AU harness, NZ thoroughbred and NZ harness carry one on\nNONE of their 1,254 — those are `unsupported`, permanently. Collection floors are the\n2026-08-21 Sydney card for AU greyhound and the 2026-09-01 card for AU thoroughbred; no\ncollector pass reaches back beyond its own floor, so a race older than one carries\n`unsupported` and will not fill.\n`runners` is JSON null in every one of these cases and has never been an empty array (0 in\nthe whole store), so `runners is None` is the complete test. GET\n/v1/racing/results/coverage answers the same question per Sydney day, category and venue\nfor 1 credit, so you can see what a day holds without paging its results.\n\n`going` and `going_rating` (2026-09-10) are `track_condition` split into the vocabulary\n/v1/racing/horses/form already uses — 'Good (4)' becomes `going: \"Good\"`,\n`going_rating: 4` — parsed by the one shared helper so the two surfaces cannot disagree.\n`track_condition` is unchanged and is still the raw string the source published.","operationId":"race_results_v1_racing_results_get","parameters":[{"name":"hours_back","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"description":"How far back to look, in hours (max 7 days)","default":24,"title":"Hours Back"},"description":"How far back to look, in hours (max 7 days)"},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Single-day alias: date=YYYY-MM-DD returns that meeting date (AET) instead of a rolling hours_back window. Mutually exclusive with hours_back. /v1/racing/events and /v1/racing/closing-lines also take date — this endpoint did not until 2026-08-25, and a caller who learned the spelling on one of them was served the trailing 24 hours instead, 143 times, with nothing in the response saying so.","title":"Date"},"description":"Single-day alias: date=YYYY-MM-DD returns that meeting date (AET) instead of a rolling hours_back window. Mutually exclusive with hours_back. /v1/racing/events and /v1/racing/closing-lines also take date — this endpoint did not until 2026-08-25, and a caller who learned the spelling on one of them was served the trailing 24 hours instead, 143 times, with nothing in the response saying so."},{"name":"categories","in":"query","required":false,"schema":{"type":"string","description":"horse,greyhound,harness or omit for all","title":"Categories"},"description":"horse,greyhound,harness or omit for all"},{"name":"venue","in":"query","required":false,"schema":{"type":"string","description":"Case-insensitive venue match, e.g. 'Rosehill'","title":"Venue"},"description":"Case-insensitive venue match, e.g. 'Rosehill'"},{"name":"country","in":"query","required":false,"schema":{"type":"string","description":"ISO country codes, comma-separated, case-insensitive (e.g. AU or AU,NZ). Matched against the result's `country`. A result whose country was never resolved (null) is excluded by any value here, so omit the filter if you need those rows.","title":"Country"},"description":"ISO country codes, comma-separated, case-insensitive (e.g. AU or AU,NZ). Matched against the result's `country`. A result whose country was never resolved (null) is excluded by any value here, so omit the filter if you need those rows."},{"name":"status","in":"query","required":false,"schema":{"type":"string","description":"Filter to 'final', 'interim' or 'abandoned'","title":"Status"},"description":"Filter to 'final', 'interim' or 'abandoned'"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RaceResultOut"},"title":"Response Race Results V1 Racing Results Get"},"example":[{"race_id":"104702cc-8a42-4b14-88b0-1fb6de91e913","coverage_from":"2026-08-15T05:53:00Z","venue":"Kalgoorlie","venue_id":"kalgoorlie","venue_canonical":"Kalgoorlie","venue_site":"kalgoorlie","race_number":8,"category":"horse","country":"AU","start_time":"2026-09-06T09:00:00Z","race_name":"COOLGARDIE DAY CELEBRATIONS HANDICAP","distance_m":1200,"track_condition":"Good (4)","going":"Good","going_rating":4,"weather":"Light Rain","status":"final","status_note":null,"field_status":"complete","field_source":"racing_australia","field_note":null,"placings":[{"name":"Vomo Island","number":8,"position":1,"win_price":4.6,"place_price":1.9,"sp":4.6,"sp_note":null},{"name":"Dublin Red","number":15,"position":2,"win_price":126.0,"place_price":17.0,"sp":101.0,"sp_note":null},{"name":"Lavish Charm","number":1,"position":3,"win_price":7.5,"place_price":2.5,"sp":8.5,"sp_note":null}],"runners":[{"name":"Vomo Island","number":8,"box":3,"position":1,"status":"ran","dead_heat":false,"jockey":"Austin Galati","trainer":"John Lugg","sp":4.6,"sp_note":null,"margin":null,"time_s":null,"abnormal":null,"runner_ref":"ra:NzkyMzc2NTE1MA"},{"name":"Dublin Red","number":15,"box":5,"position":2,"status":"ran","dead_heat":false,"jockey":"Alex Hearn","trainer":"Paul Tapper","sp":101.0,"sp_note":null,"margin":"2.75L","time_s":null,"abnormal":null,"runner_ref":"ra:NzkyNjU3OTg0Ng"},{"name":"Lavish Charm","number":1,"box":4,"position":3,"status":"ran","dead_heat":false,"jockey":"Jason Li","trainer":"Peter Fernie","sp":8.5,"sp_note":null,"margin":"3.27L","time_s":null,"abnormal":null,"runner_ref":"ra:Nzk1MjAxMDcxNA"}],"deductions":[{"name":"Bonny Rock","number":10,"win":0.0,"place":0.0,"scratched_at":"2026-09-06T08:39:45Z"}],"dividends":{"straight":[{"name":"Vomo Island","number":8,"position":1,"market":"WIN","product":"FIXED","amount":4.6},{"name":"Vomo Island","number":8,"position":1,"market":"WIN","product":"MIDDIV","amount":5.5},{"name":"Vomo Island","number":8,"position":1,"market":"PLC","product":"FIXED","amount":1.9},{"name":"Dublin Red","number":15,"position":2,"market":"PLC","product":"FIXED","amount":17.0},{"name":"Lavish Charm","number":1,"position":3,"market":"PLC","product":"FIXED","amount":2.5}],"exotics":[{"code":"QN","type":"Quinella","product":"VIC","selection":"8,15","amount":444.4},{"code":"EX","type":"Exacta","product":"VIC","selection":"8,15","amount":478.1},{"code":"TF","type":"Trifecta","product":"VIC","selection":"8,15,1","amount":4868.5},{"code":"FF","type":"First Four","product":"VIC","selection":"8,15,1,13","amount":58687.9}],"straight_types":["FIXED","RD+","BT+SP","MIDDIV"]},"resulted_at":"2026-09-06T09:05:44Z","market_closed_at":"2026-09-06T09:02:18Z","inplay_at":null,"market_state_source":"pointsbetau"}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/results/coverage":{"get":{"tags":["Racing"],"summary":"What the results field coverage actually is","description":"Cost: 1 credit. The results store's own measurement of its FULL-FIELD coverage, per\nSydney day, category and venue — the aggregate answer to the question `field_status`\nanswers per race on /v1/racing/results.\n\nIt exists because `runners: null` used to be the only signal, and it means several\ndifferent things: a fill pass that has not run yet (`pending`), a pass that has been and\ngone while the collector keeps re-attempting the row (`overdue`), a row past its\ncollector's retry horizon so that no scheduled pass still selects it (`unavailable`), a\nracing code no collector has ever existed for (`unsupported` — AU harness and both NZ\ncodes, 1,254 of the 5,577 stored results as at 2026-09-10), and a race that did not run\n(`not_applicable`). Counting them separately is what lets you decide between polling,\nreconciling and giving up, without paging the results themselves.\n\nEvery count comes from the same function that sets `field_status` on each race, applied\nto the rows in the window at request time, so this summary and the races it summarises\ncan never disagree. Nothing holds a race in `pending` indefinitely: `pending` ends at the\nnormal pass, `overdue` ends at the collector's own candidate horizon.\n\nREAD THE PERCENTAGES CAREFULLY. `complete_pct` and `supported_complete_pct` are NULL, not\n0.0, when their denominator is 0 — an empty window, or one holding only codes we do not\ncollect. A `days=1` window is empty for the first hours of every Sydney day, and a monitor\nthat treated a fabricated 0.0 as real coverage would page every night. `collectable` is\npublished alongside them so 0-of-0 is never mistaken for 0-of-many.\n\nDays are AUSTRALIAN EASTERN calendar days (Australia/Sydney), the same day a meeting is\nadvertised under, and `to_day` is today in Sydney — a part day whose races are mostly\nstill `pending`, which is normal and not a gap. Read `by_day` for the day-by-day picture\nand `venues_incomplete` for the venue-days that are genuinely short.\n\nSibling endpoint: /v1/racing/closing-lines/coverage does the same job for the price\narchive.","operationId":"results_coverage_v1_racing_results_coverage_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"description":"Sydney days to count, ending with today (a part day).","default":7,"title":"Days"},"description":"Sydney days to count, ending with today (a part day)."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultsCoverageOut"},"example":{"as_of":"2026-09-10T03:40:12Z","by_category":[{"category":"greyhound","complete":698,"country":"AU","field_source":"topaz","not_applicable":0,"overdue":25,"pending":19,"races":742,"unavailable":0,"unsupported":0},{"category":"horse","complete":108,"country":"AU","field_source":"racing_australia","not_applicable":0,"overdue":21,"pending":2,"races":137,"unavailable":6,"unsupported":0},{"category":"harness","complete":0,"country":"AU","not_applicable":0,"overdue":0,"pending":0,"races":131,"unavailable":0,"unsupported":131}],"by_day":[{"category":"greyhound","complete":0,"country":"AU","day":"2026-09-10","field_source":"topaz","not_applicable":0,"overdue":0,"pending":19,"races":19,"unavailable":0,"unsupported":0},{"category":"horse","complete":42,"country":"AU","day":"2026-09-09","field_source":"racing_australia","not_applicable":0,"overdue":7,"pending":0,"races":49,"unavailable":0,"unsupported":0}],"collectable":879,"complete":806,"complete_pct":75.54,"coverage_from":"2026-08-15T05:53:00Z","days":7,"field_scope":"Full fields are collected for AU thoroughbred (Racing Australia, from the 2026-09-01 Sydney card) and AU greyhound (Topaz, from 2026-08-21). AU harness, NZ thoroughbred and NZ harness have no full-field source and are counted as unsupported. No pass reaches back beyond its own floor.","from_day":"2026-09-04","not_applicable":0,"overdue":52,"pending":21,"races":1067,"supported_complete_pct":91.7,"to_day":"2026-09-10","unavailable":0,"unsupported":188,"venues_incomplete":[{"category":"horse","complete":0,"country":"AU","day":"2026-09-09","not_applicable":0,"overdue":7,"pending":0,"races":7,"settled":false,"unavailable":0,"unsupported":0,"venue":"Randwick","venue_id":"randwick"}],"venues_incomplete_truncated":false}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/price-history":{"get":{"tags":["Racing"],"summary":"Racing price history / closing line","description":"Cost: 5 credits. Every price move for a race, per runner per bookmaker, with the opening\nand closing line.\n\nIdentify the race either by `race_id`, or by `venue` + `race_number` + `date` — use the latter\nfor past races, because race ids are only resolvable from the live feed, which holds the next\n~6 hours.\n\n**The series is change-only.** Each point is a price MOVE, not a sample: a gap means the price\nheld steady. `move_pct` is open→close, positive meaning the price drifted out.\n\n**`open_price` and `close_price` are the first and last CAPTURED prices for that book, not\nguaranteed market open and close.** Capture starts when a race enters the 60-minute window,\nand a book that does not quote that early gets its first price whenever it starts quoting:\nmeasured over 30,448 series on 2026-08-15, 44% of series began at the 60-minute baseline and\n56% began later. At the other end 63% of series have a final point inside 120s of the jump\nand 81% inside 300s, so the rest are not closing lines either. Read `open_secs_to_jump`,\n`close_secs_to_jump` and `open_is_baseline` on each bookmaker before using a series for\nclosing-line value, and drop the ones that do not reach the jump.\n\n**History is permanent.** Races inside the ~45-day live window are served from the live\nstore; older races come from the permanent archive, which is never purged. `history_from`\non the response is the true floor — capture began 2026-08-04, and the archive only\ndeepens from there. (Identify past races by `venue` + `race_number` + `date`; both stores\nresolve that key.)","operationId":"price_history_v1_racing_price_history_get","parameters":[{"name":"race_id","in":"query","required":false,"schema":{"type":"string","description":"Race id. If omitted, give venue + race_number + date.","title":"Race Id"},"description":"Race id. If omitted, give venue + race_number + date."},{"name":"venue","in":"query","required":false,"schema":{"type":"string","description":"Venue name, case-insensitive (with race_number + date)","title":"Venue"},"description":"Venue name, case-insensitive (with race_number + date)"},{"name":"race_number","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":1,"title":"Race Number"}},{"name":"date","in":"query","required":false,"schema":{"type":"string","description":"Race date, YYYY-MM-DD (UTC)","title":"Date"},"description":"Race date, YYYY-MM-DD (UTC)"},{"name":"bookmakers","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated 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.","title":"Bookmakers"},"description":"Comma-separated 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."},{"name":"include_points","in":"query","required":false,"schema":{"type":"boolean","description":"False returns open/close/high/low only","default":true,"title":"Include Points"},"description":"False returns open/close/high/low only"},{"name":"max_points","in":"query","required":false,"schema":{"type":"integer","maximum":20000,"minimum":100,"description":"Cap on returned points; response flags truncation","default":5000,"title":"Max Points"},"description":"Cap on returned points; response flags truncation"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceHistoryOut"},"example":{"category":"horse","country":"AU","history_from":"2026-08-04T09:22:00Z","points_returned":2481,"race_id":"race_123","race_number":2,"runners":[{"bookmakers":[{"close_price":7.0,"close_secs_to_jump":41,"high":7.0,"key":"sportsbet","low":6.0,"move_pct":7.69,"open_is_baseline":true,"open_price":6.5,"open_secs_to_jump":3595,"points":[{"captured_at":"2026-08-15T02:00:04Z","secs_to_jump":3595,"win_price":6.5}],"points_count":10}],"name":"Aeolian","number":4}],"start_time":"2026-08-15T03:00:00Z","truncated":false,"venue":"Caulfield"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"x-curl-query":"race_id=YOUR_RACE_ID"}},"/v1/racing/movers":{"get":{"tags":["Racing"],"summary":"Racing steamers and drifters","description":"Cost: 3 credits. Runners whose price has moved materially since the market opened,\nacross upcoming races.\n\n`move_pct` is **negative for a firming runner** (the price shortened — money has arrived)\nand positive for a drifter.\n\n`open_price` is the FIRST PRICE CAPTURED inside the 60-minute pre-race window, not a market\nopen. Capture begins when the race enters that window, so how close that first price is to a\ntrue 60-minute open depends on the book — and it is not a clean two-way split. Measured over\n13 days of snapshots, the share of series whose first capture sits at the 60-minute mark\nfalls into three groups. `sportsbet`, `betright` and `betr_au` are there on 96-98% of series.\n`tab`, `ladbrokes_au` and `neds` are there on about 55%, but that figure swings between 16%\nand 77% from one day to the next, so for these three it is not a property you can rely on\nper book. `playup` (6%), `unibet` and `pointsbetau` (3%), `tabtouch` (1%) and `palmerbet`\n(0%) rarely or never are: `palmerbet` has not once been captured earlier than 33 minutes out\nacross 23,641 series.\n\nDo not infer any of this from the book. Read `open_secs_to_jump` on each bookmaker row, which\nsays how far out that book's open actually was; `/v1/racing/price-history` additionally\nreturns `open_is_baseline` per series.\n\nA move is reported at the RUNNER level and only when at least `min_books` bookmakers have\n**two or more captured prices**, because a book seen only once has evidenced nothing: its\nopen equals its current by construction. Those books are still listed, with `points: 1` and\n`counted_in_consensus: false`, and still counted in `books_quoting` — but they do not enter\nthe consensus or the gate. Use `books_with_history` as the confirmation count.\n\n**Broad-market support versus one book moving: read the breadth counters.** `books_firming`,\n`books_drifting` and `books_unchanged` partition `books_quoting`, so they say how many books\nactually agreed with the headline move. A steam the market backs shows most of\n`books_with_history` moving the same way; a single-book move shows one, with the rest\nunchanged, and can still clear `min_move_pct` because the consensus averages over the few\nbooks that carry history. Measured live on 2026-09-01, Geelong R4 greyhound \"Got Immunity\"\nreported +102.8% drifting on 13 books quoting, of which 5 had history: 3 drifting, 1 firming,\n9 unchanged - the headline number came from a minority. Gate on the counters, and on\n`bookmakers[].move_pct` where you need the per-book evidence, rather than on `move_pct`\nalone. Raising `min_books` raises how many books must carry history, not how many must agree.","operationId":"racing_movers_v1_racing_movers_get","parameters":[{"name":"direction","in":"query","required":false,"schema":{"type":"string","description":"firming, drifting, or omit for both","title":"Direction"},"description":"firming, drifting, or omit for both"},{"name":"min_move_pct","in":"query","required":false,"schema":{"type":"number","maximum":90.0,"minimum":1.0,"description":"Minimum consensus move, percent","default":10.0,"title":"Min Move Pct"},"description":"Minimum consensus move, percent"},{"name":"min_books","in":"query","required":false,"schema":{"type":"integer","maximum":12,"minimum":1,"description":"Only report a move confirmed by at least this many books","default":3,"title":"Min Books"},"description":"Only report a move confirmed by at least this many books"},{"name":"categories","in":"query","required":false,"schema":{"type":"string","description":"horse,greyhound,harness or omit for all","title":"Categories"},"description":"horse,greyhound,harness or omit for all"},{"name":"country","in":"query","required":false,"schema":{"type":"string","description":"ISO country codes, comma-separated (e.g. AU). Omit for every country. Pair with include_unresolved=true or you will silently lose races whose meeting is not confirmed yet.","title":"Country"},"description":"ISO country codes, comma-separated (e.g. AU). Omit for every country. Pair with include_unresolved=true or you will silently lose races whose meeting is not confirmed yet."},{"name":"include_unresolved","in":"query","required":false,"schema":{"type":"boolean","description":"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 — measured 3 labelled AU against 21 unresolved in a live window — so country=AU alone can return far fewer races than are running, or none at all.","default":false,"title":"Include Unresolved"},"description":"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 — measured 3 labelled AU against 21 unresolved in a live window — so country=AU alone can return far fewer races than are running, or none at all."},{"name":"max_mins_to_jump","in":"query","required":false,"schema":{"type":"integer","maximum":360,"minimum":5,"default":360,"title":"Max Mins To Jump"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MoverOut"},"title":"Response Racing Movers V1 Racing Movers Get"},"example":[{"bookmakers":[{"counted_in_consensus":true,"current_price":9.0,"key":"betright","move_pct":-18.18,"open_price":11.0,"open_secs_to_jump":3597,"points":4}],"books_drifting":0,"books_firming":9,"books_quoting":10,"books_unchanged":1,"books_with_history":9,"category":"horse","country":"AU","current_price":9.17,"direction":"firming","mins_to_jump":3,"move_pct":-18.3,"number":10,"open_price":11.22,"race_number":8,"runner":"Magnaburma","start_time":"2026-08-15T08:52:00Z","venue":"Broome"}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/best-odds":{"get":{"tags":["Racing"],"summary":"Best racing price per runner across books","description":"Cost: 3 credits. The best win, place and tote price for every runner in the next races,\nwith the bookmaker offering it and a deep link to their race page.\n\n`market_percentage` is the sum of implied probabilities at the best available price. **Under\n100 means the best prices across books beat the field** — a cross-book arbitrage. Any single\nbook's market is always over 100.\n\n`price_spread_pct` per runner is how much better the best price is than the worst currently\nquoted: how much a punter loses by not shopping around.\n\n**Win and place coverage are counted separately.** `books_quoting_win` is the number of\nbookmakers with a WIN price on that runner and `books_quoting_place` the number with a\nPLACE price. They are not the same set — a book can quote the win and not the place on\nthe same runner — so a place bet sized off a win-market count is sized off the wrong\nnumber. `books_quoting` is retained unchanged for existing clients and equals\n`books_quoting_win`; read the explicit names in new code. `books_compared`, at race\nlevel, is the number of distinct books contributing any win price to the whole race.\n\n⚠️ `best_tote` is **not comparable to `best_win`**. A pre-race tote dividend is an estimate\nthat moves as the pool fills, and taking the highest across books and products is upward\nbiased by construction — the noisiest estimate wins. Greyhound pools are thinnest and swing\nhardest: measured 2026-08-15, greyhound tote-to-fixed ratios reached 8.8x on betright and\n9.9x on tabtouch, against averages near 1.0 for horse and harness. A tote figure far above\nthe fixed price is usually a thin pool, not an edge. Settle against a FINAL dividend, and\nread the `product` code — `PROV` is indicative, `SP`/`MIDDIV`/`RD+`/`BT+SP` are named\nproducts.\n\nThe comparison covers the Australian fixed-odds bookmakers, on New Zealand races as well\nas Australian ones — eleven of the fourteen also quote NZ (TAB, BetGold and BoostBet do\nnot), so an NZ race is compared across a median of 10 books against 13 on an AU race\n(measured 2026-09-02 over 7 days). Betfair Exchange prices are\nwithheld pending a Betfair Exchange data licence, and non-AU reference books are excluded\nbecause an Australian punter cannot bet them — so `betfair_ex_au` never appears as a\n`best_win` bookmaker, and `market_percentage` is the fixed-odds market rather than an\nexchange-inclusive one.\n\n**Hong Kong** (`country=HK`, feature_requests #29, 2026-09-09). Sha Tin and Happy Valley\nare compared across whichever Australian books quote the meeting — win and place, the\nsame `best_win` / `best_place` shape as an Australian race. PRICES ONLY: no results, no\nform, no race conditions, no `runner_ref`. Unlike the rest of the foreign card this is a\nreal cross-book comparison rather than one book against itself, but it is a smaller panel\nthan an Australian race and only exists on the HKJC's nights — Happy Valley on Wednesday\nevenings, Sha Tin on Saturday or Sunday afternoons (AEST).\n\n**Scratched runners are never priced here.** They appear in `scratchings` only, and are left\nout of `market_percentage`.\n\n**Placeholder prices are kept out of the best line (since 2026-09-10).** A quote of $21 or\nmore that is at least 4x the median of three or more other books for the same runner is\ntreated as a placeholder, not a price: it is excluded from `best_win` / `best_place`,\n`market_percentage` and `price_spread_pct`, and listed on the runner as `suspect_win` /\n`suspect_place` (`bookmaker`, `price`, `consensus`) so nothing is hidden. A book that has\nnot priced a runner yet can publish $101 against a $9 field; before this that was the best price.\n\n**Every published price carries its own age.** `best_win`, `best_place` and `best_tote` each\ninclude `last_update`, `age_seconds`, `stale` and `refresh_tier`, because the book with the best price is not\nalways the book that answered most recently. `best_win` and `best_place` both carry the\noffering book's race-page `source_url` (best_place since 2026-09-02: it used to lack\none, and a client requiring a link on every quote silently rejected every place price).\nBoth also carry `betslip_url`, the runner-level betslip link of that same book - null\nwhen the book has no such route. See the table below. Race level, `data_age_seconds` is the oldest of\nthose winning quotes and `stale_bookmakers` names any past its own `refresh_tier`\nthreshold (120s on `live`, 1800s on `card`; each published quote carries its own tier, and\na Betr quote 30-180 minutes from the jump is `card` on a `live` race — see\n/v1/racing/next-to-go for what the tiers mean). Responses are cached for 20s\nand say so via `cached` / `cache_age_seconds`; ages are recomputed at serve time, so a cached\npayload reports its real age rather than the age it had when it was stored.\n\n**Same Race Multi legs** (`best_top2`, `best_top3`, `best_top4`, since 2026-09-05).\nThe best price across books for the runner to finish in the first two, three or four,\nin the same shape as `best_place` (price, bookmaker, source_url, age fields) plus\n`books_quoting`, the number of books offering that leg. Present only when at least one\nbook quotes the leg: Ladbrokes, Neds and PointsBet carry all three, Sportsbet where its\nracecard offers them. Every published leg satisfied\n`win >= top2 >= top3 >= top4 > 1.0` at its source.\n\n**Runner-level betslip links** (`betslip_win_url`, `betslip_place_url`,\n`betslip_app_only`). Where `source_url` opens the book's race PAGE, these open the book's\nBETSLIP with this runner already selected. Coverage, honestly:\n\n| book | opens in | preselects |\n|---|---|---|\n| `unibet` | web browser | runner **and** market (fixed win or fixed place) |\n| `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 |\n| `ladbrokes_au` | **native app only** (iOS/Android 8.69.0+) | runner and market |\n| `neds` | **native app only** | runner and market |\n\nEvery other bookmaker has no such route and carries none of these fields.\n`betslip_app_only` is `true` for Ladbrokes and Neds: in a desktop browser those URLs\nredirect to the app-download page, so send them to a phone with the app installed, or\nfall back to `source_url`. Measured 2026-09-03. The Ladbrokes pattern is vendor\ndocumented; the Neds one is **inferred** from it (same platform, same entrant ids) and\nhas not been tap-tested on a Neds install.\n\nThese routes are read from the bookmakers' own site code. **None of them is published or\nsupported by the bookmaker**, so any can change without notice. A daily automated check\nloads a live Unibet link and a live TAB link in a real browser and confirms the betslip\npopulates; when a book fails that check its betslip fields go **null** until it passes\nagain, so a broken route degrades to \"no link\" rather than sending a member to a dead\npage. Ladbrokes and Neds cannot be checked this way - a headless browser cannot open a\nnative app - and ship on the vendor's documentation instead.\n\nThe fields are **absent** on a book that has no link, exactly as `source_url` is; treat\nabsent and `null` alike as \"no betslip link\". Nothing about `source_url` changed.\n\nLike `source_url` these are plain URLs, and extra query parameters you append are passed\nthrough untouched. A link that opens a native app leaves the browser, so cookie-based\nattribution set by your own site does not travel with it.","operationId":"racing_best_odds_v1_racing_best_odds_get","parameters":[{"name":"num_races","in":"query","required":false,"schema":{"type":"integer","maximum":150,"minimum":1,"description":"Up to 150. Pass 150 to compare every race on the day's card in one call instead of paging.","default":10,"title":"Num Races"},"description":"Up to 150. Pass 150 to compare every race on the day's card in one call instead of paging."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":150,"minimum":1,"description":"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.","title":"Limit"},"description":"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."},{"name":"categories","in":"query","required":false,"schema":{"type":"string","description":"horse,greyhound,harness or omit for all","title":"Categories"},"description":"horse,greyhound,harness or omit for all"},{"name":"venue","in":"query","required":false,"schema":{"type":"string","description":"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.","title":"Venue"},"description":"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."},{"name":"bookmakers","in":"query","required":false,"schema":{"type":"string","description":"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.","title":"Bookmakers"},"description":"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."},{"name":"country","in":"query","required":false,"schema":{"type":"string","description":"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.","title":"Country"},"description":"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."},{"name":"include_unresolved","in":"query","required":false,"schema":{"type":"boolean","description":"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.","default":false,"title":"Include Unresolved"},"description":"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."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RacingBestOddsOut"},"title":"Response Racing Best Odds V1 Racing Best Odds Get"},"example":[{"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,"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"}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/best-odds/{sport_key}":{"get":{"tags":["Best Odds"],"summary":"Best price per selection","description":"Cost: 3 credits. Returns the best available price per selection for upcoming events, including arb_exists and arb_profit_pct when the best prices create an arbitrage margin.\n\nAn unknown `sport_key` returns 404 and costs nothing.","operationId":"best_odds_v1_best_odds__sport_key__get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BestOddsEventOut"},"title":"Response Best Odds V1 Best Odds  Sport Key  Get"},"example":[{"id":"6b1608c5-a9b8-4235-9a12-6db9656fef74","sport_key":"nrl","home_team":"Cronulla Sharks","away_team":"Nth Queensland Cowboys","commence_time":"2026-09-09T02:00:00Z","selections":[{"name":"Cronulla Sharks","best_price":1.45,"best_bookmaker":"betright","all_prices":[{"bookmaker":"sportsbet","price":1.44},{"bookmaker":"ladbrokes_au","price":1.44},{"bookmaker":"betright","price":1.45},{"bookmaker":"tab","price":1.43},{"bookmaker":"pointsbetau","price":1.42}]},{"name":"Nth Queensland Cowboys","best_price":2.85,"best_bookmaker":"pointsbetau","all_prices":[{"bookmaker":"pointsbetau","price":2.85},{"bookmaker":"tab","price":2.85},{"bookmaker":"sportsbet","price":2.81},{"bookmaker":"ladbrokes_au","price":2.8},{"bookmaker":"betright","price":2.75}]}],"arb_exists":false,"arb_profit_pct":0.0,"data_age_seconds":41,"freshest_age_seconds":12,"stale":false,"stale_bookmakers":[]}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/usage":{"get":{"tags":["Account"],"summary":"Check API usage","description":"Free — 0 credits deducted. Everything you need to decide about your plan BEFORE the\nAPI refuses you: spend by endpoint, burn rate, the date you are projected to run out, and\nthe tier that would clear it (with a headless checkout you can POST).\n\nTwo windows, and every field says which one it is in its name:\n\n* ALL TIME — `usage_by_endpoint`, `recent_activity`, `all_time`, `all_time_by_endpoint`.\n  Every call this key has ever made. This is the original meaning of `usage_by_endpoint`\n  and `recent_activity` and it has not changed. Because it spans every billing period the\n  key has lived through, it will normally be LARGER than `credits_used` — that is not a\n  discrepancy, the two are measuring different things.\n* CURRENT BILLING PERIOD — `usage_by_endpoint_period`, `recent_activity_period`, plus\n  `credits_used` / `credits_remaining` / `period_start` / `next_reset_at`. The credits in\n  `usage_by_endpoint_period` sum to `credits_used`. Use these to reconcile a bill.\n\n`all_time_by_endpoint` is an exact alias of `usage_by_endpoint`, kept because it shipped.\nNothing in the all-time pair changed on 2026-08-18; if you saw those two numbers step down\nearlier that day, this restored them.","operationId":"usage_v1_usage_get","parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(json|csv)$","description":"`csv` returns the per-endpoint breakdown as a spreadsheet instead of the JSON document. Same rows, same numbers, no row cap. Free, like the JSON.","default":"json","title":"Format"},"description":"`csv` returns the per-endpoint breakdown as a spreadsheet instead of the JSON document. Same rows, same numbers, no row cap. Free, like the JSON."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageOut"},"example":{"all_time":{"calls":310,"credits":402,"endpoints":1},"all_time_by_endpoint":[{"calls":310,"credits":402,"endpoint":"/v1/sports"}],"burn_rate_per_day":6.0,"credits_limit":1000,"credits_remaining":958,"credits_used":42,"next_reset_at":"2026-08-01T00:00:00","period_start":"2026-07-01T00:00:00","plan":"free","plans":{"free":{"credits_per_month":500,"price_aud":0}},"projected_month_end_credits":186,"recent_activity":[{"credits":1,"endpoint":"/v1/sports","ts":"2026-06-07T02:20:00"}],"recent_activity_period":[{"credits":1,"endpoint":"/v1/sports","ts":"2026-06-07T02:20:00"}],"reset_at":"2026-07-01T00:00:00","upgrade":{"checkout":{"body":{"email":"you@example.com","plan":"hobby"},"endpoint":"https://api.puntersedge.online/v1/signup","method":"POST"},"covers_projected_need":true,"credits_per_month":7500,"price_aud":9,"recommended_plan":"hobby","sized_for_credits_per_month":186,"url":"https://puntersedge.online/api/pricing?plan=hobby&utm_source=api_usage"},"usage_by_endpoint":[{"calls":310,"credits":402,"endpoint":"/v1/sports"}],"usage_by_endpoint_period":[{"calls":42,"credits":42,"endpoint":"/v1/sports"}]}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/billing/portal":{"get":{"tags":["Account"],"summary":"Self-service billing portal link","description":"Cost: 0 credits. Returns a short-lived Stripe billing portal URL for this key's\nsubscription: update the card, download invoices, see what the next charge will be.\n\nTHE API KEY IS THE IDENTITY. The website has its own portal at /account, but it needs a\nwebsite login and reads a stripe_customer_id out of the website's own subscriptions table.\nA customer who subscribed through this API has neither — they never registered on the\nwebsite, and their customer id lives on their api_keys row. Before this endpoint the only\nway for them to change a failing card was to email us, which is how a subscription is lost\nto an expired card rather than a decision.\n\nThe returned URL is a BEARER link: anyone holding it can manage the subscription until it\nexpires. It is deliberately never logged, and callers should not paste it anywhere.","operationId":"billing_portal_v1_billing_portal_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"url":"https://billing.stripe.com/p/session/live_YWNjdF8xUHZ2c3Bsb0N0Nnp0dGMx","expires_at":1757212800,"note":"Single-use and short-lived. Treat it like a password: anyone with this link can manage the subscription."}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/health":{"get":{"tags":["Account"],"summary":"Connector health status","description":"Requires auth. Shows last_ok, status, records_written per connector.","operationId":"connector_health_v1_health_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ConnectorHealthOut"},"title":"Response Connector Health V1 Health Get"},"example":[{"connector":"sportsbet","last_ok":"2026-09-07T00:49:09","last_poll":"2026-09-07T00:49:09","status":"ok","records_written":163},{"connector":"tab","last_ok":"2026-09-07T00:49:09","last_poll":"2026-09-07T00:49:09","status":"ok","records_written":349},{"connector":"ladbrokes_au","last_ok":"2026-09-07T00:49:09","last_poll":"2026-09-07T00:49:09","status":"ok","records_written":375}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/uptime":{"get":{"tags":["Account"],"summary":"Public uptime stats","description":"Public uptime stats — no auth required, 0 credits.\n\n**`uptime_30d_pct` is nullable — type `number | null`, not `number`.** It is `null`, with `checks: 0`, whenever the monitor has written no samples inside the window: the buffer file is absent, or every sample in it predates the 30-day cutoff. Until 2026-08-18 those branches returned `100.0`, which published *no measurement* as *perfect availability*; the honest value is null and it stays null.\n\nGuard before any arithmetic or `>=` comparison:\n\n```python\npct = r.get(\"uptime_30d_pct\")\nok = pct is not None and pct >= 99.9   # not: r[\"uptime_30d_pct\"] >= 99.9\n```\n\nThe no-measurement body in full:\n\n```json\n{\n  \"uptime_30d_pct\": null,\n  \"checks\": 0,\n  \"status\": \"no data yet\",\n  \"probe_target\": \"https://puntersedge.online/ping\",\n  \"measures\": \"puntersedge.online (website), not api.puntersedge.online\",\n  \"note\": \"...\"\n}\n```\n\n`checks == 0` tests the same condition without touching the nullable field.","operationId":"uptime_stats_v1_uptime_get","responses":{"200":{"description":"Uptime stats. `uptime_30d_pct` is null — NOT 100 and not absent — when the window holds no samples.","content":{"application/json":{"schema":{"properties":{"uptime_30d_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Percentage of successful probes in the window, or NULL when the window contains no samples at all. Null means unmeasured, not 100. Guard before arithmetic or a >= comparison.","examples":[99.99,null]},"checks":{"type":"integer","description":"Samples in the window. 0 is the non-null sentinel for the same condition that makes uptime_30d_pct null, and is always safe to compare."},"status":{"type":"string","description":"Present only in the no-measurement case: 'no data yet'."},"last_check":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"last_status":{"type":"string","enum":["ok","fail"]},"monitor_stale":{"type":"boolean","description":"True when the newest sample is over 15 minutes old."},"last_check_age_min":{"anyOf":[{"type":"number"},{"type":"null"}]},"window_first_check":{"type":"string","format":"date-time"},"window_span_days":{"type":"number","description":"Span actually observed, which equals 30 only if the probe never missed a sample."},"probe_target":{"type":"string"},"measures":{"type":"string"},"note":{"type":"string"}},"type":"object","required":["uptime_30d_pct","checks","probe_target","measures","note"]},"example":{"uptime_30d_pct":99.99,"checks":8640,"last_status":"ok","monitor_stale":false,"window_span_days":30.0,"probe_target":"https://puntersedge.online/ping"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/keys":{"post":{"tags":["Account"],"summary":"Create a new API key (admin only)","description":"Create a new API key. Admin only.","operationId":"create_key_v1_keys_post","parameters":[{"name":"label","in":"query","required":false,"schema":{"type":"string","default":"My Key","title":"Label"}},{"name":"plan","in":"query","required":false,"schema":{"type":"string","default":"free","title":"Plan"}},{"name":"email","in":"query","required":false,"schema":{"type":"string","title":"Email"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"key":"pe_4f9c2a7e1b8d3c6a5e0f7b2d9c4a1e8f","plan":"free","label":"My Key","credits_limit":500}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/keys/rotate":{"post":{"tags":["Account"],"summary":"Rotate your API key","description":"Issue a new API key. The old key keeps working for a short grace window so a\nmulti-process caller can roll over without an outage; pass immediate=true to kill it now.\nCost: 0 credits.","operationId":"rotate_key_v1_keys_rotate_post","parameters":[{"name":"immediate","in":"query","required":false,"schema":{"type":"boolean","description":"Revoke the old key NOW instead of after the grace window. Use this when rotating because the key leaked — a grace window keeps a compromised credential alive and is the wrong default for that case.","default":false,"title":"Immediate"},"description":"Revoke the old key NOW instead of after the grace window. Use this when rotating because the key leaked — a grace window keeps a compromised credential alive and is the wrong default for that case."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"new_key":"pe_4f9c2a7e1b8d3c6a5e0f7b2d9c4a1e8f","old_key_hint":"pe_9b21c0…","key_version":2,"rotated_at":"2026-09-07T00:52:10Z","previous_key_valid_until":"2026-09-08T00:52:10Z","message":"Key rotated. The previous key keeps working for 24h (until previous_key_valid_until) so you can roll over without downtime. Re-run with immediate=true if the old key may have leaked."}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/keys/resend":{"post":{"tags":["Account"],"summary":"Email yourself a fresh API key","description":"Rotate this account's key and mail the new one to its own verified address.\n\nKeys are sha256-hashed on insert and cannot be read back, /v1/keys/rotate needs the key\nyou have lost, and the only regenerate UI is gated behind plan == \"members\" (the tips\nsubscription), which no API customer holds. So there was no self-serve recovery at all:\nto 2026-08-25, 22 distinct IPs reached /account against 45 that minted a key.\n\n⛔ ALWAYS 202, WHATEVER HAPPENS — unknown address, unverified account, rate-limited,\nmail failure. The response is byte-identical in every case. An endpoint that says\n\"no such account\" is an account-existence oracle for anyone with a list of emails, and\nthis one names a mailbox that receives credentials.\n\nThe key goes ONLY to the address already stored and verified on the row. Nothing the\ncaller sends is used as a destination, so submitting someone else's address mails that\nperson their own key and tells the requester nothing.\n\nCost: 0 credits. Unauthenticated by necessity — the credential is the thing that is lost.\n\n⚠️ KNOWN, ACCEPTED: response time is longer when the address does exist, because the send\nis awaited (see below). That leaks account existence to an attacker who can measure it.\nAccepted deliberately — the alternative is committing a rotation before knowing the mail\nwent, which strands a real customer with a dead key. A timing signal on account existence\nis worth less than a customer's working credential, and the per-IP cap bounds sampling.","operationId":"resend_key_v1_keys_resend_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResendIn"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"status":"accepted","message":"If that address has a verified PuntersEdge API account, a new key is on its way to it. Your previous key keeps working for 24 hours, so anything running has time to roll over."}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/keys/info":{"get":{"tags":["Account"],"summary":"API key metadata","description":"Returns metadata about the authenticated key including plan, credits, org, and last-used info.","operationId":"key_info_v1_keys_info_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"key_hint":"pe_4f9c2a…","plan":"plus","org_id":null,"org_name":null,"credits_used":1240,"credits_limit":20000,"credits_remaining":18760,"key_version":2,"last_used_at":"2026-09-07T00:51:58Z","last_used_ip":"203.0.113.42","ip_whitelist":[],"created_at":"2026-08-19T06:14:02Z","rotated_at":"2026-09-07T00:52:10Z"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/keys/ip-whitelist":{"post":{"tags":["Account"],"summary":"Set IP whitelist for your key","description":"Cost: 0 credits. Restrict this key to specific IPs or CIDR ranges. Other plans get 403.\nPass an empty list to remove restrictions.","operationId":"set_ip_whitelist_v1_keys_ip_whitelist_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"type":"string"},"description":"IPv4/IPv6 addresses or CIDR ranges. A bare address is stored as a /32 (or /128). Send an empty list to remove all restrictions.","examples":[["203.0.113.7/32","198.51.100.0/24"]],"title":"Cidrs"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"ip_whitelist":["203.0.113.0/24","198.51.100.7"],"message":"Whitelist updated — 2 rule(s) active."}}}},"403":{"description":"IP whitelisting is available on Platform, Unlimited and Enterprise plans only."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/usage/analytics":{"get":{"tags":["Account"],"summary":"Usage breakdown by endpoint","description":"Cost: 0 credits. Credit usage grouped by endpoint.\n\n`period=billing` exists because the rolling window never lines up with what you are\nactually billed on: the credit counter is zeroed on the 1st of each month\n(/etc/cron.d/auodds-reset), so on the 18th a 30-day rolling total spans two allowances\nand cannot be reconciled against credits_used. The default stays `rolling` so no existing\nclient changes behaviour.\n\n⚠️ `period_days` is an int under `period=rolling` (it echoes `days`, unchanged) and a\nfloat under `period=billing`, where it is the measured length of the current period\nrather than a requested window — e.g. 31.0 for a period that started on the 1st. The\nwidening only reaches callers who opt in by passing the new parameter; `period_start` and\n`period_end` are always returned so the window never has to be inferred from it.","operationId":"usage_analytics_v1_usage_analytics_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"description":"Rolling window length. Ignored when period=billing.","default":30,"title":"Days"},"description":"Rolling window length. Ignored when period=billing."},{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^(rolling|billing)$","description":"rolling = the last `days` days (default, unchanged). billing = the CURRENT billing period, so the totals here match credits_used and credits_remaining exactly.","default":"rolling","title":"Period"},"description":"rolling = the last `days` days (default, unchanged). billing = the CURRENT billing period, so the totals here match credits_used and credits_remaining exactly."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"period":"rolling","period_days":30,"period_start":"2026-08-08T00:52:10","period_end":"2026-09-07T00:52:10","credits_limit":20000,"credits_used_this_period":1240,"credits_remaining":18760,"total_calls":1102,"total_credits":1240,"by_endpoint":[{"endpoint":"/v1/racing/next-to-go","calls":611,"credits":611},{"endpoint":"/v1/racing/best-odds","calls":302,"credits":302},{"endpoint":"/v1/racing/price-history","calls":108,"credits":216},{"endpoint":"/v1/racing/results","calls":81,"credits":81}]}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/signup":{"post":{"tags":["Signup"],"summary":"Create API key or start paid checkout","description":"Sign up for an API key.\n- free: returns key immediately\n- paid: returns Stripe checkout URL, key created on webhook","operationId":"signup_v1_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"status":"pending_verification","plan":"free","api_key":null,"message":"Almost there — check your inbox and click the verification link to activate your free API key."}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/arb/sports":{"get":{"tags":["Arb & Value"],"summary":"Sports arb scanner","description":"Cost: 3 credits. Scans upcoming sports markets for true arbitrage and best-odds overlay opportunities, returning arb_pct, optimal stakes and per-selection price comparisons.","operationId":"sports_arb_v1_arb_sports_get","parameters":[{"name":"sport_key","in":"query","required":false,"schema":{"type":"string","description":"Filter by sport e.g. afl, nrl","title":"Sport Key"},"description":"Filter by sport e.g. afl, nrl"},{"name":"maxAgeMinutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":5,"description":"Exclude bookmaker markets older than this many minutes. Same bound and default as /v1/sports/{sport_key}/odds, so an arb can never be built on a price that endpoint refuses to show.","default":360,"title":"Maxageminutes"},"description":"Exclude bookmaker markets older than this many minutes. Same bound and default as /v1/sports/{sport_key}/odds, so an arb can never be built on a price that endpoint refuses to show."},{"name":"min_profit_pct","in":"query","required":false,"schema":{"type":"number","description":"Min guaranteed profit % (0 = show all value)","default":0.0,"title":"Min Profit Pct"},"description":"Min guaranteed profit % (0 = show all value)"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SportsArbOut"},"title":"Response Sports Arb V1 Arb Sports Get"},"example":[{"event_id":"6b1608c5-a9b8-4235-9a12-6db9656fef74","sport_key":"nrl","home_team":"Cronulla Sharks","away_team":"Nth Queensland Cowboys","commence_time":"2026-09-09T02:00:00Z","is_arb":false,"arb_pct":0.0,"optimal_stakes":[],"max_overlay_pct":1.4,"selections":[{"name":"Cronulla Sharks","best_price":1.45,"best_bookmaker":"betright","avg_price":1.436,"overlay_pct":1.0,"all_prices":[{"bookmaker":"sportsbet","price":1.44},{"bookmaker":"ladbrokes_au","price":1.44},{"bookmaker":"betright","price":1.45},{"bookmaker":"tab","price":1.43},{"bookmaker":"pointsbetau","price":1.42}]},{"name":"Nth Queensland Cowboys","best_price":2.85,"best_bookmaker":"pointsbetau","avg_price":2.812,"overlay_pct":1.4,"all_prices":[{"bookmaker":"pointsbetau","price":2.85},{"bookmaker":"tab","price":2.85},{"bookmaker":"sportsbet","price":2.81},{"bookmaker":"ladbrokes_au","price":2.8},{"bookmaker":"betright","price":2.75}]}]}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/arb/best-prices":{"get":{"tags":["Arb & Value"],"summary":"Best prices comparison","description":"Cost: 2 credits. Returns the best available price per selection plus full bookmaker comparison, worst price, average price, spread and overlay percentage.","operationId":"best_prices_v1_arb_best_prices_get","parameters":[{"name":"maxAgeMinutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":5,"description":"Exclude bookmaker markets older than this many minutes. Same bound and default as /v1/sports/{sport_key}/odds, so an arb can never be built on a price that endpoint refuses to show.","default":360,"title":"Maxageminutes"},"description":"Exclude bookmaker markets older than this many minutes. Same bound and default as /v1/sports/{sport_key}/odds, so an arb can never be built on a price that endpoint refuses to show."},{"name":"sport_key","in":"query","required":false,"schema":{"type":"string","default":"nrl","title":"Sport Key"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BestPricesOut"},"title":"Response Best Prices V1 Arb Best Prices Get"},"example":[{"id":"6b1608c5-a9b8-4235-9a12-6db9656fef74","sport_key":"nrl","home_team":"Cronulla Sharks","away_team":"Nth Queensland Cowboys","commence_time":"2026-09-09T02:00:00Z","selections":[{"name":"Nth Queensland Cowboys","best_price":2.85,"best_bookmaker":"pointsbetau","worst_price":2.75,"avg_price":2.812,"price_spread":0.1,"overlay_pct":1.4,"all_prices":[{"bookmaker":"pointsbetau","price":2.85},{"bookmaker":"tab","price":2.85},{"bookmaker":"sportsbet","price":2.81},{"bookmaker":"ladbrokes_au","price":2.8},{"bookmaker":"betright","price":2.75}]},{"name":"Cronulla Sharks","best_price":1.45,"best_bookmaker":"betright","worst_price":1.42,"avg_price":1.436,"price_spread":0.03,"overlay_pct":1.0,"all_prices":[{"bookmaker":"sportsbet","price":1.44},{"bookmaker":"ladbrokes_au","price":1.44},{"bookmaker":"betright","price":1.45},{"bookmaker":"tab","price":1.43},{"bookmaker":"pointsbetau","price":1.42}]}]}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/arb/lines":{"get":{"tags":["Arb & Value"],"summary":"Spreads/totals line arb","description":"Cost: 3 credits. Line-matched arbitrage on spreads and totals: backs the two opposing sides\nat complementary lines across books. Returns the best complementary pair per event/market with a\nguaranteed-profit (is_arb) flag; is_middle marks overlapping lines where both sides can win.","operationId":"lines_arb_v1_arb_lines_get","parameters":[{"name":"sport_key","in":"query","required":false,"schema":{"type":"string","description":"Filter by sport e.g. afl, nrl","title":"Sport Key"},"description":"Filter by sport e.g. afl, nrl"},{"name":"maxAgeMinutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":5,"description":"Exclude bookmaker markets older than this many minutes. Same bound and default as /v1/sports/{sport_key}/odds, so an arb can never be built on a price that endpoint refuses to show.","default":360,"title":"Maxageminutes"},"description":"Exclude bookmaker markets older than this many minutes. Same bound and default as /v1/sports/{sport_key}/odds, so an arb can never be built on a price that endpoint refuses to show."},{"name":"min_profit_pct","in":"query","required":false,"schema":{"type":"number","description":"Min guaranteed profit % (0 = also show the best line comparison when not an arb)","default":0.0,"title":"Min Profit Pct"},"description":"Min guaranteed profit % (0 = also show the best line comparison when not an arb)"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LinesArbOut"},"title":"Response Lines Arb V1 Arb Lines Get"},"example":[{"event_id":"6b1608c5-a9b8-4235-9a12-6db9656fef74","sport_key":"nrl","home_team":"Cronulla Sharks","away_team":"Nth Queensland Cowboys","commence_time":"2026-09-09T02:00:00Z","market_type":"totals","opportunities":[{"side_a":{"name":"Over","point":47.5,"price":2.05,"bookmaker":"pointsbetau"},"side_b":{"name":"Under","point":48.5,"price":2.02,"bookmaker":"tab"},"is_arb":true,"is_middle":true,"arb_pct":1.7,"optimal_stakes":[{"name":"Over","point":47.5,"stake":49.63,"bookmaker":"pointsbetau","profit_if_wins":1.74},{"name":"Under","point":48.5,"stake":50.37,"bookmaker":"tab","profit_if_wins":1.75}]}]}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/ingest/{bookmaker_key}":{"post":{"tags":["ingest"],"summary":"Ingest Prices","description":"Push race+price data from an external source (e.g. Mac-side browser scraper).\nOverwrites existing data for this bookmaker_key. Trusted/admin keys only —\nthis writes to the price feed and must not be open to ordinary keys.","operationId":"ingest_prices_v1_ingest__bookmaker_key__post","parameters":[{"name":"bookmaker_key","in":"path","required":true,"schema":{"type":"string","title":"Bookmaker Key"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"ok":true,"races_ingested":12,"runners_built":118,"observed_at_applied":12}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/ingest/sports/{bookmaker_key}":{"post":{"tags":["ingest"],"summary":"Ingest Sport Prices","description":"Push sports H2H (and spreads/totals) prices from an external source — e.g. the\nMac-side Sportsbet feed, since Sportsbet IP-blocks the VPS. Trusted/admin keys only.\nRuns through the same canonical matching + data-quality pipeline as direct connectors.","operationId":"ingest_sport_prices_v1_ingest_sports__bookmaker_key__post","parameters":[{"name":"bookmaker_key","in":"path","required":true,"schema":{"type":"string","title":"Bookmaker Key"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SportIngestPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"ok":true,"events_ingested":8}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks":{"post":{"tags":["Webhooks"],"summary":"Create Webhook","operationId":"create_webhook_v1_webhooks_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"},"example":{"id":"wh_7c1d2e9a4b3f","url":"https://example.com/hooks/puntersedge","events":["arb.opportunity","race.odds_open"],"min_edge_pct":1.5,"sports":["racing_horse","racing_greyhound","nrl"],"is_active":true,"created_at":"2026-09-07T00:52:10","last_delivery_at":null,"failure_count":0,"secret":"whsec_EXAMPLE_ONLY_shown_once_at_creation","delivery_entitled":true,"delivery_paused_reason":null}}}},"403":{"description":"Webhook subscriptions are available on Standard, Plus, Business, Platform, Unlimited and Enterprise plans only. Legacy Starter, Growth and Pro keys also qualify."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"tags":["Webhooks"],"summary":"List Webhooks","operationId":"list_webhooks_v1_webhooks_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WebhookResponse"},"title":"Response List Webhooks V1 Webhooks Get"},"example":[{"id":"wh_7c1d2e9a4b3f","url":"https://example.com/hooks/puntersedge","events":["arb.opportunity","race.odds_open"],"min_edge_pct":1.5,"sports":["racing_horse","racing_greyhound","nrl"],"is_active":true,"created_at":"2026-09-07T00:52:10","last_delivery_at":"2026-09-07T00:57:15","failure_count":0,"secret":null,"delivery_entitled":true,"delivery_paused_reason":null}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/{webhook_id}":{"delete":{"tags":["Webhooks"],"summary":"Delete Webhook","operationId":"delete_webhook_v1_webhooks__webhook_id__delete","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"deleted":true,"id":"wh_7c1d2e9a4b3f"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/{webhook_id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"List Webhook Deliveries","operationId":"list_webhook_deliveries_v1_webhooks__webhook_id__deliveries_get","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDeliveryResponse"},"title":"Response List Webhook Deliveries V1 Webhooks  Webhook Id  Deliveries Get"},"example":[{"id":"dl_5e2c8a1f9b3d","webhook_id":"wh_7c1d2e9a4b3f","event_type":"arb.opportunity","payload":{"sport":"nrl","event_name":"Melbourne Storm v Penrith Panthers","edge_pct":2.97,"legs":[{"bookmaker":"pointsbetau","selection":"Melbourne Storm","odds":2.1},{"bookmaker":"sportsbet","selection":"Penrith Panthers","odds":2.02}]},"status":"delivered","attempt_count":1,"status_code":200,"error":null,"delivered_at":"2026-09-07T00:57:15.331905"},{"id":"dl_1a7f3c0e2d9b","webhook_id":"wh_7c1d2e9a4b3f","event_type":"odds.change","payload":{"event":"odds.change","test":true,"sent_at":"2026-09-07T00:53:02.118240Z","message":"PuntersEdge webhook test"},"status":"delivered","attempt_count":1,"status_code":200,"error":null,"delivered_at":"2026-09-07T00:53:02.410551"}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/{webhook_id}/test":{"post":{"tags":["Webhooks"],"summary":"Test Webhook","operationId":"test_webhook_v1_webhooks__webhook_id__test_post","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"delivered":true,"status_code":200,"error":null,"delivery_id":"dl_1a7f3c0e2d9b","webhook_active":true}}}},"403":{"description":"Webhook subscriptions are available on Standard, Plus, Business, Platform, Unlimited and Enterprise plans only. Legacy Starter, Growth and Pro keys also qualify."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/sports/{sport_key}/odds/history":{"get":{"tags":["Sports"],"summary":"Historical odds snapshots","description":"Cost: 5 credits. Paid tiers only — 403 on Free and Hobby.\n\nAn unknown `sport_key`, or a date range that runs backwards, returns an error and costs\nnothing.","operationId":"get_odds_history_v1_sports__sport_key__odds_history_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/time; defaults to 7 days ago","title":"From"},"description":"ISO date/time; defaults to 7 days ago"},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/time; defaults to now","title":"To"},"description":"ISO date/time; defaults to now"},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated 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.","title":"Bookmakers"},"description":"Comma-separated 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."},{"name":"market","in":"query","required":false,"schema":{"type":"string","description":"Market type, default h2h","default":"h2h","title":"Market"},"description":"Market type, default h2h"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":[{"event_id":"4b84fde7-7054-40db-a37f-ae05c9705549","home_team":"Souths","away_team":"Newcastle","sport_key":"nrl","snapshots":[{"recorded_at":"2026-09-07T00:43:24Z","bookmaker":"tab","home_price":1.4,"draw_price":null,"away_price":3.0},{"recorded_at":"2026-09-07T00:43:24Z","bookmaker":"ladbrokes_au","home_price":1.4,"draw_price":null,"away_price":3.0},{"recorded_at":"2026-09-06T22:13:09Z","bookmaker":"tab","home_price":1.42,"draw_price":null,"away_price":2.9}]}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/sports/{sport_key}/odds/movements":{"get":{"tags":["Sports"],"summary":"Price movement feed","description":"Cost: 5 credits. Returns detected home/draw/away price movements from historical snapshots.\n\nPaid tiers only — 403 on Free and Hobby. Same gate as the history endpoint above, which\nthe schema used to state while this one was silent on it. The tiers are not named here on\npurpose: the 403 body below renders the live gate through plans_disp(), and a second\nhand-typed copy is exactly what went stale when `starter` was retired.\n\nAn unknown `sport_key` returns 404 and costs nothing.","operationId":"get_odds_movements_v1_sports__sport_key__odds_movements_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/time; defaults to 24 hours ago","title":"Since"},"description":"ISO date/time; defaults to 24 hours ago"},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated 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.","title":"Bookmakers"},"description":"Comma-separated 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."},{"name":"market","in":"query","required":false,"schema":{"type":"string","default":"h2h","title":"Market"}},{"name":"min_delta","in":"query","required":false,"schema":{"type":"number","minimum":0.0,"default":0.01,"title":"Min Delta"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":[{"event_id":"4b84fde7-7054-40db-a37f-ae05c9705549","sport_key":"nrl","bookmaker":"tab","market_type":"h2h","home_team":"Souths","away_team":"Newcastle","commence_time":"2026-09-09T02:05:00Z","from_recorded_at":"2026-09-06T22:13:09Z","to_recorded_at":"2026-09-07T00:43:24Z","changes":{"home_price":{"from":1.42,"to":1.4,"delta":-0.02},"away_price":{"from":2.9,"to":3.0,"delta":0.1}}}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/health/connectors":{"get":{"tags":["Health"],"summary":"Per-connector freshness","description":"Cost: 1 credit. Per-scraper last-success age + a stale flag, so a silently\nstalled feed (the root cause of phantom arbs) is visible.\n\n`sports` breaks that down PER SPORT, because connector-level health cannot see a book\nthat has quietly stopped covering one competition. Measured 2026-08-21: betright was\nreporting healthy and writing 15-21 events a cycle — its NRL and AFL rows current to\nthe minute — while its NFL rows were 79 hours old. It had simply dropped NFL, and\nnothing said so. Those stale NFL prices were being served as arbitrage.","operationId":"connectors_v1_health_connectors_get","parameters":[{"name":"stale_after_s","in":"query","required":false,"schema":{"type":"number","description":"age (s) past which a connector is 'stale'","default":180.0,"title":"Stale After S"},"description":"age (s) past which a connector is 'stale'"},{"name":"coverage_stale_after_s","in":"query","required":false,"schema":{"type":"number","description":"age (s) past which a book is treated as no longer COVERING a sport. Defaults to 6h, the same window /v1/sports/{sport_key}/odds will serve — past it the book is effectively absent for that sport.","default":21600.0,"title":"Coverage Stale After S"},"description":"age (s) past which a book is treated as no longer COVERING a sport. Defaults to 6h, the same window /v1/sports/{sport_key}/odds will serve — past it the book is effectively absent for that sport."},{"name":"include_sports","in":"query","required":false,"schema":{"type":"boolean","description":"include the per-sport coverage breakdown","default":true,"title":"Include Sports"},"description":"include the per-sport coverage breakdown"},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Connectors V1 Health Connectors Get"},"example":{"now":"2026-09-07T00:49:16Z","stale_after_s":180.0,"any_stale":false,"any_degraded":false,"connectors":[{"connector":"sportsbet","last_ok_age_s":6.0,"last_poll_age_s":6.0,"polling":true,"stale":false,"records_written":163,"last_msg":"OK — 163 records","degraded":false,"sports":[{"sport_key":"nrl","markets":48,"newest_age_s":6.0,"covering":true},{"sport_key":"afl","markets":27,"newest_age_s":6.0,"covering":true}],"sports_not_covering":[]},{"connector":"tab","last_ok_age_s":11.2,"last_poll_age_s":11.2,"polling":true,"stale":false,"records_written":349,"last_msg":"OK — 349 records","degraded":false,"sports":[{"sport_key":"nrl","markets":48,"newest_age_s":11.2,"covering":true},{"sport_key":"afl","markets":27,"newest_age_s":11.2,"covering":true}],"sports_not_covering":[]},{"connector":"ladbrokes_au","last_ok_age_s":10.6,"last_poll_age_s":10.6,"polling":true,"stale":false,"records_written":375,"last_msg":"OK — 375 records","degraded":false,"sports":[{"sport_key":"nrl","markets":48,"newest_age_s":10.6,"covering":true},{"sport_key":"afl","markets":27,"newest_age_s":10.6,"covering":true}],"sports_not_covering":[]}],"coverage_stale_after_s":21600.0,"not_covering":[]}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/demo/racing/next-to-go":{"get":{"tags":["Sandbox (no key)"],"summary":"Sample next-to-go races (no key required)","description":"Public teaser: next 3 races, up to 5 runners each, best 3 bookmaker prices. No API key. Cached 30s, 30 req/min per IP.","operationId":"demo_racing_v1_demo_racing_next_to_go_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"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","signup_url":"https://puntersedge.online/api?utm_source=demo_api&utm_medium=sandbox","shape":"Sandbox teaser: 3 races, 5 runners, best 3 prices, wrapped in this envelope. GET /v1/racing/next-to-go returns a bare array of full race objects. race_id matches, so anything you build here extends to /v1/racing/price-history, /results and /closing-lines.","races":[{"race_id":"bec4195f-dc6a-4a78-9d17-ae79e7954c71","venue":"Healesville","race_number":1,"category":"greyhound","start_time":"2026-09-07T00:51:00Z","country":"AU","runners":[{"name":"Mrs. Puddleduck","number":1,"bookmakers":[{"key":"betdeluxe","win_price":4.4,"source_url":"https://www.betdeluxe.com.au/racing/Greyhound/AUS/Healesville/789853/1/11268767"},{"key":"neds","win_price":4.0,"source_url":"https://www.neds.com.au/racing/healesville/8165c88a-3151-4a66-a7b7-57bbb09119c7"},{"key":"sportsbet","win_price":4.0,"source_url":"https://www.sportsbet.com.au/greyhound-racing/australia-nz/healesville/race-1-10896567"}]},{"name":"Sweet Chiquita","number":3,"bookmakers":[{"key":"tab","win_price":2.6,"source_url":"https://www.tab.com.au/racing/2026-09-07/HEALESVILLE/HSV/G/1"},{"key":"betr_au","win_price":2.45,"source_url":"https://www.betr.com.au/racing/Australian-Greyhound/Healesville/Race-1/91974333/win"},{"key":"unibet","win_price":2.4,"source_url":"https://www.unibet.com.au/racing#/event/202609070000.G.AUS.healesville.1"}]}]}],"cached":false}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/demo/best-odds":{"get":{"tags":["Sandbox (no key)"],"summary":"Sample best-odds + arb detection (no key required)","description":"Public teaser: best price per selection + arb flag for up to 3 upcoming events. No API key. Cached 45s, 30 req/min per IP.","operationId":"demo_best_odds_v1_demo_best_odds_get","parameters":[{"name":"sport","in":"query","required":false,"schema":{"type":"string","description":"Optional sport_key; auto-picks a live sport if omitted","title":"Sport"},"description":"Optional sport_key; auto-picks a live sport if omitted"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"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","signup_url":"https://puntersedge.online/api?utm_source=demo_api&utm_medium=sandbox","sport":"nrl","events":[{"home_team":"Cronulla Sharks","away_team":"Nth Queensland Cowboys","commence_time":"2026-09-09T02:00:00Z","selections":[{"name":"Cronulla Sharks","best_price":1.44,"best_bookmaker":"sportsbet"},{"name":"Nth Queensland Cowboys","best_price":2.81,"best_bookmaker":"sportsbet"}],"arb_exists":false,"arb_profit_pct":0.0},{"home_team":"South Sydney Rabbitohs","away_team":"Newcastle Knights","commence_time":"2026-09-09T02:05:00Z","selections":[{"name":"South Sydney Rabbitohs","best_price":1.4,"best_bookmaker":"palmerbet"},{"name":"Newcastle Knights","best_price":2.97,"best_bookmaker":"palmerbet"}],"arb_exists":false,"arb_profit_pct":0.0}],"cached":false}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/demo/book-sport":{"get":{"tags":["Sandbox (no key)"],"summary":"Live book+sport snapshot (no key; powers SEO page differentiation)","description":"Public, book-specific live odds for one sport. Truncated to a few events/races. Cached 90s.","operationId":"demo_book_sport_v1_demo_book_sport_get","parameters":[{"name":"book","in":"query","required":true,"schema":{"type":"string","description":"SANDBOX bookmaker slug e.g. sportsbet. Slugs, not API bookmaker keys -- the real keys are betr_au, ladbrokes_au, pointsbetau, playup. Use GET /v1/racing/next-to-go for the real ones.","title":"Book"},"description":"SANDBOX bookmaker slug e.g. sportsbet. Slugs, not API bookmaker keys -- the real keys are betr_au, ladbrokes_au, pointsbetau, playup. Use GET /v1/racing/next-to-go for the real ones."},{"name":"sport","in":"query","required":true,"schema":{"type":"string","description":"SANDBOX sport slug e.g. afl or horse-racing. These are this endpoint's own URL vocabulary, NOT API sport_keys -- the keyed API has no 'horse-racing' sport_key at all (racing lives under /v1/racing/*).","title":"Sport"},"description":"SANDBOX sport slug e.g. afl or horse-racing. These are this endpoint's own URL vocabulary, NOT API sport_keys -- the keyed API has no 'horse-racing' sport_key at all (racing lives under /v1/racing/*)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"demo":true,"book":"sportsbet","sport":"nrl","type":"sports","items":[{"home_team":"Cronulla Sharks","away_team":"Nth Queensland Cowboys","commence_time":"2026-09-09T02:00:00Z","prices":[{"name":"Cronulla Sharks","price":1.44},{"name":"Nth Queensland Cowboys","price":2.81}]},{"home_team":"South Sydney Rabbitohs","away_team":"Newcastle Knights","commence_time":"2026-09-09T02:05:00Z","prices":[{"name":"South Sydney Rabbitohs","price":1.4},{"name":"Newcastle Knights","price":2.97}]}],"has_data":true,"cached":false}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/data-quality/summary":{"get":{"tags":["Data Quality"],"summary":"Data quality summary","description":"Cost: 1 credit. Summary of connector freshness, canonical mappings and recent audit status.","operationId":"data_quality_summary_v1_data_quality_summary_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"status":"ok","canonical_events":1843,"source_mappings":9211,"recent_market_quality":{"ok":612,"warn":41,"stale":9},"connectors":[{"connector":"sportsbet","last_ok":"2026-09-07T00:49:09Z","age_seconds":6,"records_written":163,"message":"OK — 163 records"},{"connector":"tab","last_ok":"2026-09-07T00:49:09Z","age_seconds":11,"records_written":349,"message":"OK — 349 records"},{"connector":"ladbrokes_au","last_ok":"2026-09-07T00:49:09Z","age_seconds":10,"records_written":375,"message":"OK — 375 records"}],"latest_audit":{"id":"2a9d3416-3cc9-41b2-a38c-9e65cca2c531","sport_key":"nba","status":"ok","api_rows":69,"source_rows":67,"exact_matches":67,"diff_rows":0,"max_delta":0.0,"finished_at":"2026-09-07T00:45:04Z"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/data-quality/audit/run":{"post":{"tags":["Data Quality"],"summary":"Run bookmaker audit","description":"Admin/enterprise only. Runs source-vs-API audit now.","operationId":"run_audit_endpoint_v1_data_quality_audit_run_post","parameters":[{"name":"sport_key","in":"query","required":false,"schema":{"type":"string","default":"nrl","title":"Sport Key"}},{"name":"market","in":"query","required":false,"schema":{"type":"string","default":"h2h","title":"Market"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":{"run_id":"2a9d3416-3cc9-41b2-a38c-9e65cca2c531","sport_key":"nba","market_type":"h2h","api_rows":69,"books_checked":6,"outliers":0,"max_delta":0.0}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/data-quality/audit/latest":{"get":{"tags":["Data Quality"],"summary":"Latest bookmaker audits","operationId":"latest_audits_v1_data_quality_audit_latest_get","parameters":[{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sport Key"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":[{"id":"2a9d3416-3cc9-41b2-a38c-9e65cca2c531","sport_key":"nba","market_type":"h2h","status":"ok","api_rows":69,"source_rows":67,"common_rows":67,"exact_matches":67,"diff_rows":0,"missing_api":0,"missing_source":0,"max_delta":0.0,"started_at":"2026-09-07T00:45:04Z","finished_at":"2026-09-07T00:45:04Z"},{"id":"ceb59980-4699-47d4-b2e8-971f288dd441","sport_key":"cricket_test","market_type":"h2h","status":"ok","api_rows":1,"source_rows":0,"common_rows":0,"exact_matches":0,"diff_rows":0,"missing_api":0,"missing_source":0,"max_delta":0.0,"started_at":"2026-09-07T00:45:04Z","finished_at":"2026-09-07T00:45:04Z"}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/data-quality/audit/{run_id}/results":{"get":{"tags":["Data Quality"],"summary":"Audit result detail","operationId":"audit_results_v1_data_quality_audit__run_id__results_get","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{},"example":[{"bookmaker_key":"tab","event_key":["penrith","wests tigers","2026-09-06"],"status":"outlier","delta":0.457,"message":"wests tigers 3.8 vs consensus 7.0 (45.7%)","api":[{"selection":"wests tigers","price":3.8,"consensus":7.0,"dev_pct":45.7}],"source":{"penrith":1.22,"wests tigers":3.8}},{"bookmaker_key":"pointsbetau","event_key":["penrith","wests tigers","2026-09-06"],"status":"outlier","delta":0.2,"message":"wests tigers 5.6 vs consensus 7.0 (20.0%)","api":[{"selection":"wests tigers","price":5.6,"consensus":7.0,"dev_pct":20.0}],"source":{"penrith":1.14,"wests tigers":5.6}}]}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/closing-lines":{"get":{"tags":["Racing Archive"],"summary":"Permanent closing-line and result archive","description":"Cost: 5 credits (JSON) / 20 credits (CSV). The permanent closing-line archive: one row\nper race, runner and bookmaker, carrying the last price seen before the jump, the first\nprice seen after the market opened, and the finishing position where one is known.\n\n**Plans.** Every paid tier from Standard (A$29) upward reads the whole archive; `window_days`\ncomes back null for them. Free and Hobby get a 403. Legacy `starter`, `pro` and `growth`\nkeys keep full access but cannot be bought — Starter was retired from checkout on\n2026-08-22, `pro` on 2026-08-12 and `growth` on 2026-08-15. No tier is currently clamped;\n`window_days` is non-null only if a rolling-window tier is reintroduced.\n\n**This is not the same data as `/v1/racing/price-history`.** That endpoint reads the live\n45-day snapshot store and returns every price move for one race. This one reads a separate,\npermanent table and returns one collapsed row per series across any date range. Price\nhistory disappears at 45 days; the archive does not.\n\n**Read the flags before you model on this.** Measured 2026-08-18 over 348,889 archived\nseries:\n\n* `is_closing_line` — only 85.5% of series have their final observation within 300s of the\n  jump. The rest stopped being quoted early and their close is a last-seen price. Filtered\n  in by default.\n* `open_is_baseline` — only 42.5% of series start at the 60-minute window entry. The rest\n  joined mid-window, so their \"open\" is not a market open.\n* `finish_position` **NULL does not mean the runner lost.** `race_results.placings` carries\n  only the placegetters, so most runners in a fully resulted race have no published\n  position. Check `result_status`: NULL there means the race has no result at all.\n* `venue_split_suspect` / `name_fragment_suspect` — the archive's record of its own known\n  contamination. Excluded by default.\n\n**Result coverage is thin and forward-growing.** The results feed began on 2026-08-15 and\nis AU/NZ only, so on 2026-08-18 only 5.3% of archived races carry a result. `resulted_rows`\non the response is the measured count for your actual selection, not a marketing figure.\n\n**`runner_ref` is the join key** (last column, since 2026-09-05). The stable registry id\nfor the runner — `ra:<horsecode>` for thoroughbreds, `grv:<dogId>` for greyhounds — the\nsame value `/v1/racing/results` carries on every `runners[]` entry and\n`/v1/racing/horses/form` accepts, so an archive row joins to its result and to the\nhorse's form without matching names. Filled from Racing Australia acceptances for AU\nthoroughbreds and from the result itself for every code once it lands, so it can be null\non a row whose race has not resulted yet, on harness (no registry feed), and on rows\narchived before the identifiers existed (late August 2026). Appended after `venue_site`\nso positional CSV readers keep working.\n\nBetfair Exchange rows are withheld from customer responses pending a data licence, so\n`close_lay_price` — which only an exchange quotes — is NULL for customers.\n\n**Dates.** `from`/`to` bound the range. `date=YYYY-MM-DD` is a convenience alias for a\nsingle day and is exactly `from=YYYY-MM-DD&to=YYYY-MM-DD`; it exists because\n`/v1/racing/events` takes `date` and callers reasonably expect the same spelling here.\nPassing `date` together with `from` or `to` is a free 422 rather than a silent precedence\nrule.\n\nAn unknown bookmaker key, an unknown parameter, a reversed date range or a bad date is an\nerror and costs nothing.","operationId":"closing_lines_v1_racing_closing_lines_get","parameters":[{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/time on the race start. Defaults to the archive floor.","title":"From"},"description":"ISO date/time on the race start. Defaults to the archive floor."},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/time. Defaults to now.","title":"To"},"description":"ISO date/time. Defaults to now."},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Single-day alias: date=YYYY-MM-DD is exactly from=YYYY-MM-DD&to=YYYY-MM-DD. Mutually exclusive with from/to. Also accepted by /v1/racing/results. NOT by /v1/racing/events, which is forward-looking and takes hours_ahead — this line claimed it did until 2026-08-25, and that false claim is how a Racing subscriber learned to send date= to /results, where it was undeclared and silently dropped for 143 requests.","title":"Date"},"description":"Single-day alias: date=YYYY-MM-DD is exactly from=YYYY-MM-DD&to=YYYY-MM-DD. Mutually exclusive with from/to. Also accepted by /v1/racing/results. NOT by /v1/racing/events, which is forward-looking and takes hours_ahead — this line claimed it did until 2026-08-25, and that false claim is how a Racing subscriber learned to send date= to /results, where it was undeclared and silently dropped for 143 requests."},{"name":"venue","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Venue name, case-insensitive, exact match.","title":"Venue"},"description":"Venue name, case-insensitive, exact match."},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing.","title":"Bookmakers"},"description":"Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"horse, harness or greyhound","title":"Category"},"description":"horse, harness or greyhound"},{"name":"country","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Two-letter country code, e.g. AU, NZ","title":"Country"},"description":"Two-letter country code, e.g. AU, NZ"},{"name":"race_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Single race, joinable to /v1/racing/price-history while that race is still inside the 45-day snapshot window.","title":"Race Id"},"description":"Single race, joinable to /v1/racing/price-history while that race is still inside the 45-day snapshot window."},{"name":"closing_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only rows whose last observation was within 300s of the jump. True by default because 14.5% of series are NOT closing lines and silently mixing them in is how a CLV study goes wrong.","default":true,"title":"Closing Only"},"description":"Only rows whose last observation was within 300s of the jump. True by default because 14.5% of series are NOT closing lines and silently mixing them in is how a CLV study goes wrong."},{"name":"resulted_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only rows from races that have a result.","default":false,"title":"Resulted Only"},"description":"Only rows from races that have a result."},{"name":"include_flagged","in":"query","required":false,"schema":{"type":"boolean","description":"Include rows flagged venue_split_suspect or name_fragment_suspect. False by default; set true if you want the contaminated rows and intend to handle them.","default":false,"title":"Include Flagged"},"description":"Include rows flagged venue_split_suspect or name_fragment_suspect. False by default; set true if you want the contaminated rows and intend to handle them."},{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(json|csv)$","description":"csv streams a flat table with a stable column order — the format a modeller actually wants.","default":"json","title":"Format"},"description":"csv streams a flat table with a stable column order — the format a modeller actually wants."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50000,"minimum":1,"default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClosingLinesOut"},"example":{"archive_from":"2026-08-04T09:20:44Z","limit":500,"offset":0,"resulted_rows":640,"rows":[{"bookmaker_key":"sportsbet","category":"horse","close_secs_to_jump":41,"close_win_price":7.0,"country":"AU","finish_position":2,"is_closing_line":true,"meeting_date_aet":"2026-08-15","name_fragment_suspect":false,"open_is_baseline":true,"open_secs_to_jump":3595,"open_win_price":6.5,"points_observed":10,"race_id":"race_123","race_number":2,"result_status":"final","runner_key":"aeolian","runner_name":"Aeolian","runner_number":4,"runner_ref":"ra:2098765","start_time":"2026-08-15T03:00:00Z","venue":"Caulfield","venue_id":"caulfield","venue_site":"caulfield","venue_split_suspect":false}],"rows_returned":1,"total_rows":12480}},"text/csv":{"schema":{"type":"string","example":"race_id,start_time,meeting_date_aet,venue,..."}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"x-curl-query":"from=2026-08-10&to=2026-08-17&category=horse&format=csv"}},"/v1/racing/closing-lines/coverage":{"get":{"tags":["Racing Archive"],"summary":"What the closing-line archive actually holds","description":"Cost: 1 credit. The archive's own measurements of itself — depth, date floor, how much\nof it is a genuine closing line, how much has a result, and how many rows carry a\ncontamination flag.\n\nResults are collected for Australian and New Zealand racing only, from `results_from`\nonward. Races in other countries appear in the archive because an Australian book\npriced them, and they carry closing prices but never a finish position. So\n`resulted_races_pct` is the share of the WHOLE archive with a result, while\n`settleable_races` / `settleable_resulted_pct` count only AU and NZ races that started\nafter results collection began — the figure to read if you intend to settle against\nthe archive. `by_category` carries `resulted_races` per code for the same reason.\n\nThis exists so nobody has to take a marketing number on trust before paying for a bulk\npull. Every figure is computed from the table at request time. Same plan gate as the\narchive itself, and the numbers describe the whole archive.","operationId":"closing_lines_coverage_v1_racing_closing_lines_coverage_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArchiveCoverageOut"},"example":{"archive_from":"2026-08-04T09:22:00Z","archive_to":"2026-09-07T00:04:00Z","rows":937526,"races":24420,"bookmakers":15,"is_closing_line_pct":85.57,"open_is_baseline_pct":50.42,"resulted_races_pct":20.2,"results_from":"2026-08-15T05:53:00Z","settleable_races":5390,"settleable_resulted_pct":94.01,"results_scope":"Results are collected for Australian and New Zealand racing only, from results_from onward. Races in other countries carry closing prices but no finish position. settleable_races counts AU and NZ races that started after results_from.","venue_split_suspect_rows":10965,"name_fragment_suspect_rows":135,"by_category":[{"category":"greyhound","rows":420993,"races":9198,"resulted_races":2915},{"category":"horse","rows":322233,"races":11500,"resulted_races":1158},{"category":"harness","rows":194300,"races":3722,"resulted_races":994}]}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/price-paths":{"get":{"tags":["Racing Archive"],"summary":"Bulk export of the permanent price-movement archive","description":"Cost: 5 credits (JSON) / 20 credits (CSV). Every OBSERVED PRICE POINT in the permanent\nmovement archive: one row per race, runner, bookmaker and captured price, from market open\nto the last pre-jump quote.\n\n**This is the bulk, downloadable form of `/v1/racing/price-history`.** That endpoint\nanswers for one race; this one exports a date range as a flat table (CSV loads straight\ninto pandas — `pd.read_csv(url)` with your key in the header; `format=parquet` returns the\nsame table typed, `pd.read_parquet(BytesIO(r.content))`). The archive is permanent:\nnothing here ages out, and coverage runs from `archive_from` (capture began 2026-08-04)\nforward, forever.\n\n**The series is change-only.** Each row is a price MOVE, not a fixed-interval sample — a\ngap between two points means the price held. To build T-snapshots (T-60/30/15/5/2), take\nthe last row at or before each mark per (race_id, runner_key, bookmaker_key).\n\n**Window guard.** Points run ~9x the series rows, so a request without `race_id` is capped\nat a 7-day `from`/`to` window per call — page a season week by week. A reversed or\nover-wide range is a free 422.\n\n**`runner_ref`** (last column, since 2026-09-05) is the stable registry id for the runner —\n`ra:<horsecode>` for thoroughbreds, `grv:<dogId>` for greyhounds — identical to the\n`runner_ref` on `/v1/racing/results` `runners[]` entries and on the live board, so every\npoint in this export joins to its result and to `/v1/racing/horses/form` on one key.\nNull where no registry id is known: harness, rows archived before late August 2026, and\na race whose result has not landed and whose acceptance list we did not hold.\n\nBetfair Exchange rows are withheld pending a data licence, so `lay_price` — which only an\nexchange quotes — is NULL for customers. BSP and traded volume are absent for the same\nreason; when a licence lands they will arrive as new columns, not a changed contract.","operationId":"price_paths_v1_racing_price_paths_get","parameters":[{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/time on the race start.","title":"From"},"description":"ISO date/time on the race start."},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO date/time.","title":"To"},"description":"ISO date/time."},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Single-day alias: date=YYYY-MM-DD is exactly from=YYYY-MM-DD&to=YYYY-MM-DD. Mutually exclusive with from/to.","title":"Date"},"description":"Single-day alias: date=YYYY-MM-DD is exactly from=YYYY-MM-DD&to=YYYY-MM-DD. Mutually exclusive with from/to."},{"name":"venue","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Venue name, case-insensitive, exact match.","title":"Venue"},"description":"Venue name, case-insensitive, exact match."},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys.","title":"Bookmakers"},"description":"Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"horse, harness or greyhound","title":"Category"},"description":"horse, harness or greyhound"},{"name":"country","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Two-letter country code, e.g. AU, NZ","title":"Country"},"description":"Two-letter country code, e.g. AU, NZ"},{"name":"race_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Single race. With race_id the window guard does not apply.","title":"Race Id"},"description":"Single race. With race_id the window guard does not apply."},{"name":"include_flagged","in":"query","required":false,"schema":{"type":"boolean","description":"Include rows flagged venue_split_suspect or name_fragment_suspect. False by default.","default":false,"title":"Include Flagged"},"description":"Include rows flagged venue_split_suspect or name_fragment_suspect. False by default."},{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(json|csv|parquet)$","description":"csv streams a flat table with a stable column order — the bulk download this endpoint exists for. parquet returns the same table, same column order, as one Apache Parquet file with typed columns (UTC timestamps, a date, float64 prices) — pd.read_parquet(BytesIO(r.content)). Same credit cost and row cap as csv.","default":"json","title":"Format"},"description":"csv streams a flat table with a stable column order — the bulk download this endpoint exists for. parquet returns the same table, same column order, as one Apache Parquet file with typed columns (UTC timestamps, a date, float64 prices) — pd.read_parquet(BytesIO(r.content)). Same credit cost and row cap as csv."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200000,"minimum":1,"default":1000,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricePathsOut"},"example":{"rows_returned":3,"total_points":41872,"limit":500,"offset":0,"archive_from":"2026-08-04T09:22:00Z","rows":[{"race_id":"104702cc-8a42-4b14-88b0-1fb6de91e913","start_time":"2026-09-06T09:00:00Z","meeting_date_aet":"2026-09-06","venue":"Kalgoorlie","race_number":8,"category":"horse","country":"AU","runner_key":"vomo island","runner_name":"Vomo Island","runner_number":8,"bookmaker_key":"sportsbet","captured_at":"2026-09-06T08:00:12Z","secs_to_jump":3588,"win_price":5.0,"lay_price":null,"venue_id":"kalgoorlie","venue_site":"kalgoorlie","runner_ref":"ra:NzkyMzc2NTE1MA"},{"race_id":"104702cc-8a42-4b14-88b0-1fb6de91e913","start_time":"2026-09-06T09:00:00Z","meeting_date_aet":"2026-09-06","venue":"Kalgoorlie","race_number":8,"category":"horse","country":"AU","runner_key":"vomo island","runner_name":"Vomo Island","runner_number":8,"bookmaker_key":"sportsbet","captured_at":"2026-09-06T08:41:03Z","secs_to_jump":1137,"win_price":4.8,"lay_price":null,"venue_id":"kalgoorlie","venue_site":"kalgoorlie","runner_ref":"ra:NzkyMzc2NTE1MA"},{"race_id":"104702cc-8a42-4b14-88b0-1fb6de91e913","start_time":"2026-09-06T09:00:00Z","meeting_date_aet":"2026-09-06","venue":"Kalgoorlie","race_number":8,"category":"horse","country":"AU","runner_key":"vomo island","runner_name":"Vomo Island","runner_number":8,"bookmaker_key":"sportsbet","captured_at":"2026-09-06T08:59:21Z","secs_to_jump":39,"win_price":4.6,"lay_price":null,"venue_id":"kalgoorlie","venue_site":"kalgoorlie","runner_ref":"ra:NzkyMzc2NTE1MA"}]}},"text/csv":{"schema":{"type":"string","example":"race_id,start_time,meeting_date_aet,venue,..."}},"application/vnd.apache.parquet":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"x-curl-query":"date=2026-08-15&category=horse&format=csv"}},"/v1/racing/greyhounds/form":{"get":{"tags":["Racing"],"summary":"Greyhound form history for one dog","description":"Cost: 3 credits. Every recorded run for one greyhound, newest first.\n\nSOURCE. The official Topaz feed for Australian greyhound racing, synced into this system\ntwice a day at 22:30 and 00:30 UTC. Coverage runs from 2020-01-01 to the last meeting the\nlatest sync carried; `source.coverage_to` gives that date and `source.synced_at` the sync\ntimestamp, so a client can see exactly how current the answer is. Greyhounds only —\nthoroughbred and harness form come from a different source and are not served here.\n\nRESOLVING `dog`. Pass a Topaz dogId for an exact answer, or a name. Names are compared on\nletters and digits only, so \"Ando's Mac\", \"ANDOS MAC\" and \"andos mac\" are one query.\nPartial names do not match; the whole name must. When one name belongs to several dogs\n(9 names in 69,040 dogs do), the response is a 200 carrying `ambiguous: true` and\n`candidates[]` with each dogId, its career starts and its last start date, and `runs` is\nempty. Re-query with the dogId you want. A name matching nothing returns 404.\n\nJOINING TO LIVE DATA. `dog_id` here is the same identifier as the numeric part of\n`runner_ref` on /v1/racing/results runners, which greyhounds carry as \"grv:<dogId>\" —\nstrip the prefix and you have the join key from a settled result to this form history.\n`trainer.id` is the Topaz trainerId and is stable across meetings. `topaz_race_id` groups\nevery runner in one race within this feed; it is Topaz's identifier and is a different\nnamespace from `race_id` on /v1/racing/next-to-go, /events and /results, which is our own\nUUID and is stable for a race from upcoming through to results.\n\nREADING A RUN. `position` is the finishing place and is NULL when the dog did not complete\nthe course; `abnormal` then says why (Fell, TailedOff, PulledUp, Disqualified,\nStayedInBox). `scratched: true` runs are kept in this list so the record is complete, and\nthey carry no time, place or margin. `win_time_s` is the winning time of that race, so\n`time_s - win_time_s` is the dog's deficit; it is present even when the dog itself has no\nrecorded time. `margin` is Topaz's published margin in lengths and `margin_s` the same gap\nin seconds. `sp` is the official starting price, not a bookmaker's price — for per-book\nprices on an upcoming race use /v1/racing/next-to-go or /v1/racing/best-odds.\n\nGRADES. `grade` is the race's type code; `grade_in` and `grade_out` are the DOG's grade\nentering and leaving the run, so `grade_in != grade_out` marks the runs that changed the\ndog's grade (155,976 of 2.86M runs do). `second_split_time` is recorded only at tracks\nthat time two sectional points — 21% of runs carry one; NULL elsewhere means the track\ndid not time it, never that the dog missed it.\n\nCached 10 minutes. The source only changes twice a day, so a repeat query inside that\nwindow returns the same data.","operationId":"greyhound_form_v1_racing_greyhounds_form_get","parameters":[{"name":"dog","in":"query","required":true,"schema":{"type":"string","description":"Dog name or Topaz dogId. Names match case- and punctuation-insensitively on the whole name.","title":"Dog"},"description":"Dog name or Topaz dogId. Names match case- and punctuation-insensitively on the whole name."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Most recent runs to return.","default":20,"title":"Limit"},"description":"Most recent runs to return."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GreyhoundFormOut"},"example":{"ambiguous":false,"candidates":[],"dog_id":760119227,"dog_name":"VELOCITY YIN","runs":[{"box":3,"date":"2026-08-30","distance_m":395,"dog_id":760119227,"dog_name":"VELOCITY YIN","first_split_position":5,"first_split_time":5.98,"grade":"5","grade_in":"5","grade_out":"5","margin":"5.00L","margin_s":0.34,"pir":"5544","position":4,"race_number":4,"rug":3,"scratched":false,"sp":8.4,"time_s":22.65,"topaz_race_id":-2075830351,"track":"Albion Park","trainer":{"id":-41481,"name":"HEATH McADAM"},"weight_kg":29.5,"win_time_s":22.31}],"source":{"coverage_from":"2020-01-01","coverage_to":"2026-08-31","feed":"topaz","region":"AU","sport":"greyhound","synced_at":"2026-09-01T00:32:11+00:00"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/greyhounds/stats":{"get":{"tags":["Racing"],"summary":"Greyhound record by track, distance, box or grade","description":"Cost: 3 credits. One greyhound's record, split by track, distance, box, grade, or any\ncombination of them.\n\nSOURCE. The official Topaz feed for Australian greyhound racing, synced twice a day at\n22:30 and 00:30 UTC, covering 2020-01-01 to the last meeting the latest sync carried\n(`source.coverage_to`). Greyhounds only. Every start the feed holds counts toward these\nfigures — this is a career record over the covered period, with no recency window.\n\nGROUPING. `by=track,distance` returns one row per track-and-distance pair the dog has\nstarted at; `by=box` returns one row per box. The dimensions you did not request come back\nNULL on each row. Rows are ordered by starts, most first.\n\nTHE NUMBERS. `starts` counts runs the dog actually took part in — scratchings are excluded\nfrom every figure here, which is why `starts` is lower than the run count on\n/v1/racing/greyhounds/form for the same dog. `wins` is place 1, `places` is a finish in\nthe first three, and both percentages are over `starts`. `avg_time_s`, `best_time_s` and\n`avg_first_split` are computed over the runs that have a recorded time; a run where the\ndog fell or was pulled up has none and is counted in `starts` but not in the averages. On\na small group these averages describe a handful of runs, so read `starts` alongside them.\n\nJOINING TO LIVE DATA. `dog_id` is the same identifier as the numeric part of `runner_ref`\non /v1/racing/results runners (\"grv:<dogId>\" for greyhounds). `race_id` on\n/v1/racing/next-to-go, /events and /results is our own UUID and is stable for a race from\nupcoming through to results, which is a separate namespace from Topaz's raceId.\n\nAn ambiguous name returns 200 with `ambiguous: true` and `candidates[]`, exactly as on\n/v1/racing/greyhounds/form. A name matching nothing returns 404.\n\nCached 10 minutes.","operationId":"greyhound_stats_v1_racing_greyhounds_stats_get","parameters":[{"name":"dog","in":"query","required":true,"schema":{"type":"string","description":"Dog name or Topaz dogId, resolved exactly as on /v1/racing/greyhounds/form.","title":"Dog"},"description":"Dog name or Topaz dogId, resolved exactly as on /v1/racing/greyhounds/form."},{"name":"by","in":"query","required":false,"schema":{"type":"string","description":"Dimensions to group by: track, distance, box, grade. Comma-separate to cross them, e.g. track,distance.","default":"track","title":"By"},"description":"Dimensions to group by: track, distance, box, grade. Comma-separate to cross them, e.g. track,distance."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GreyhoundStatsOut"},"example":{"ambiguous":false,"candidates":[],"dog_id":760119227,"dog_name":"VELOCITY YIN","group_by":["track"],"groups":[{"avg_first_split":7.594,"avg_time_s":20.02,"best_time_s":19.16,"place_pct":28.57,"places":8,"starts":28,"track":"Albion Park","win_pct":10.71,"wins":3}],"source":{"coverage_from":"2020-01-01","coverage_to":"2026-08-31","feed":"topaz","region":"AU","sport":"greyhound","synced_at":"2026-09-01T00:32:11+00:00"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/horses/form":{"get":{"tags":["Racing"],"summary":"Thoroughbred form, career record and profile for one horse","description":"Cost: 3 credits. One AU thoroughbred's recent form, career record and profile,\nparsed from Racing Australia's public horse page — the registry's own data.\n\nSOURCE AND FRESHNESS. The horse's page is read on demand and the parsed result stored\n(`source.fetched_at` dates it), so the first query for a horse pays a page-read to RA\n(typically under two seconds) and later reads are instant. A horse's form only\nchanges when it races or trials, so the stored copy is re-read from RA only when our\nown results archive shows the horse has started since it was read, or when the copy\nis more than seven days old (a barrier trial is invisible to results, so it surfaces\nwithin a week at worst). Inside six hours it is served without even that check. A\nhorse collected once, by you or by anyone, is therefore served from our store without\nanother RA read for as long as it stays unraced. When RA cannot be reached and a\nstored copy exists, that copy is served\nwith `source.stale: true` rather than an error. When RA is refusing reads (its bot\nwall) and no stored copy exists, the answer is 503 with a `Retry-After` header and\nfresh reads pause for that long; stored horses keep serving throughout. Only a 200\nis billed — a 404, 502 or 503 costs nothing.\n\nRESOLVING `horse`. Pass the ra: code from `runner_ref` for an exact answer — it is\non every /v1/racing/results runner (AU thoroughbred) and on acceptance-enriched live\nrunners — or pass a name. Names resolve against horses seen on our own feeds:\nacceptances roughly three days ahead, plus every archived result runner (from\n2026-09-01). A name that matches several horses (registered names are reused across\neras) returns 200 with `ambiguous: true` and `candidates[]`; re-query with the\nrunner_ref you meant. A name never seen on our feeds returns 404 even when RA knows\nthe horse — there is deliberately no name-search against RA itself.\n\nREADING A RUN. Runs come newest first: the page's recent-form table, typically the\nlast ten starts plus barrier trials (`trial: true` — RA publishes those with $0\nprize, 0kg weights and no market). `race_class` is RA's official class string\n('MDN-SW', 'BM64', 'CTRY MDN'), the first place race class appears anywhere on this\nAPI. `prices` is the published price path oldest-quote-to-SP and `sp` its last\nentry. `margin_l` is lengths behind the winner — on the horse's own win, its winning\nmargin. `pir` is position in running at the 800m and 400m marks where published.\n`placegetters` names the two nearest finishers with their own ra: codes, so one\nhorse's page walks to its rivals'. Track names are RA's compressed codes ('W FM',\n'RAND') served as published. `trainer` on a run is the trainer OF THAT DAY, joined\nfrom our own results archive — populated for runs from 2026-09-01 onward and NULL\nbefore, because RA's form table names only the current trainer (`profile.trainer`);\nthe archive floor moves back nowhere, so the populated share grows with every\nmeeting.\n\nTHE CAREER BLOCK. RA's own summaries, not derived from the runs list: overall\nstarts/wins/seconds/thirds, prizemoney, and per-dimension splits (first_up,\nsecond_up, firm/good/soft/heavy/synthetic; track/distance splits appear when RA\nrenders them). These cover the whole career even where the runs list shows only\nrecent starts.\n\nJOINING TO LIVE DATA. `runner_ref` here, on /v1/racing/results runners and on\nenriched live runners is the same stable identifier, so pre-race model rows join to\nofficial results and to this form history on one key. `jockey_code` and `meet_code`\nare RA's stable identifiers for the rider and the meeting.\n\nCached 10 minutes per query on top of the six-hour page store.","operationId":"horse_form_v1_racing_horses_form_get","parameters":[{"name":"horse","in":"query","required":true,"schema":{"type":"string","description":"Horse name, or the stable ra: code from runner_ref on results/live runners. Names match case- and punctuation-insensitively on the whole name.","title":"Horse"},"description":"Horse name, or the stable ra: code from runner_ref on results/live runners. Names match case- and punctuation-insensitively on the whole name."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HorseFormOut"},"example":{"ambiguous":false,"candidates":[],"career":{"prizemoney":4875,"seconds":0,"splits":{"first_up":{"seconds":0,"starts":2,"thirds":1,"wins":0},"good":{"seconds":0,"starts":2,"thirds":1,"wins":0}},"starts":3,"thirds":1,"wins":0},"horse_name":"Zweigelt","profile":{"age_sex":"4yo Bay Gelding","colours":"Lime, Purple And Gold Braces And Armbands","dam":"MEDGRINDA","dob":"2022-11-01","last_gear_change":"20-Aug-2026, Blinkers, First Time","sire":"VINO ROSSO (USA)","trainer":"Matthew Dunn","trainer_location":"Murwillumbah"},"runner_ref":"ra:MTMwNDY2ODI0NA","runs":[{"barrier":9,"date":"2026-08-18","distance_m":1430,"field_size":9,"going":"Soft","going_rating":7,"jockey":"Andrew Mallyon","jockey_code":"MTQ5MjQ5OTIw","margin_l":0.76,"meet_code":"MTI2NzM2MDQwMA","pir":[{"at_m":800,"position":7},{"at_m":400,"position":7}],"placegetters":[{"name":"Sunset Trail","position":1,"runner_ref":"ra:MTMwMDQ4MDk2MA","weight_kg":58.0}],"position":4,"prices":[7.5,4.0,4.8,5.0],"prize_total":27000,"prize_won":1300,"race_class":"MDN","race_number":4,"result_raw":"4th of 9","sectional_distance_m":600,"sectional_time_s":37.16,"sp":5.0,"time_s":86.82,"track":"GRAF","trial":false,"weight_kg":58.5}],"source":{"feed":"racing_australia","fetched_at":"2026-09-01T11:20:00+00:00","region":"AU","sport":"thoroughbred","stale":false}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/login/request":{"post":{"tags":["Account"],"summary":"Email yourself a one-time login link for the key console","description":"Always 202 with an identical body. Cost: 0 credits. Unauthenticated by necessity.","operationId":"login_request_v1_login_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequestIn"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/login/exchange":{"post":{"tags":["Account"],"summary":"Redeem a login link for a console session","operationId":"login_exchange_v1_login_exchange_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginExchangeIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/logout":{"post":{"tags":["Account"],"summary":"End a console session","operationId":"logout_v1_logout_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}}],"responses":{"204":{"description":"Successful Response","content":{"application/json":{}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/keys/reveal":{"post":{"tags":["Account"],"summary":"Issue a fresh key and return it (signed-in console only)","operationId":"keys_reveal_v1_keys_reveal_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/horses/backfill":{"post":{"tags":["Racing"],"summary":"Queue a batch of horses for paced form collection","description":"Cost: free to submit. 3 credits per horse when it is delivered.\n\nHand us a list of horses and we collect their Racing Australia form at a pace RA\ntolerates, then hold it for you. This is the supported way to backfill a model:\nlooping /v1/racing/horses/form yourself will trip RA's bot protection long before a\nfew thousand horses, and the 503s that follow are shared with every other customer.\n\nNames resolve the same way they do on the form endpoint — against acceptances (about\nthree days ahead) and every archived result runner. A name we have never seen comes\nstraight back in `not_found` rather than being queued; a name matching several horses\ncomes back in `ambiguous` with the candidates, so you can re-submit the ref you meant.\nNeither costs anything.\n\nHorses we already hold a fresh copy of skip the queue and are ready at once. A horse\nalready queued for you is not queued twice.\n\nPoll GET /v1/racing/horses/backfill/{job_id} for progress and collect from\n/results as rows complete — you do not have to wait for the whole batch.","operationId":"submit_backfill_v1_racing_horses_backfill_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackfillSubmitIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackfillSubmitOut"},"example":{"job_id":"bf_20260907_3a9c1e","label":"Saturday metro fields","queued":38,"already_stored":6,"duplicates":0,"not_found":["Fabulos Fiano"],"ambiguous":[{"input":"Media Spin","candidates":[{"runner_ref":"ra:MTMwNDY2ODI0NA","runner_name":"Media Spin","last_seen":"2026-09-07","source":"acceptances"},{"runner_ref":"ra:MTI5ODc1NDQwMA","runner_name":"Media Spin (NZ)","last_seen":"2026-08-22","source":"results"}]}],"estimated_ready":"2026-09-07T01:35:00Z"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/horses/backfill/{job_id}":{"get":{"tags":["Racing"],"summary":"Progress of a backfill job","description":"Cost: free. Poll as often as your rate limit allows.\n\n`credits_blocked` is the one field worth watching: the worker bills 3 credits per\nhorse as it delivers it, and when your balance drops below that it stops taking rows\nfor your key and leaves them queued. Nothing is lost — collection resumes when your\ncredits reset on the 1st, or immediately if you upgrade.","operationId":"backfill_status_v1_racing_horses_backfill__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackfillStatusOut"},"example":{"job_id":"bf_20260907_3a9c1e","submitted_at":"2026-09-07T00:52:10Z","total":38,"done":21,"queued":17,"failed":0,"credits_charged":63,"credits_blocked":false,"queue_position":0,"estimated_ready":"2026-09-07T01:35:00Z","ready_to_collect":21}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"delete":{"tags":["Racing"],"summary":"Cancel the horses a job has not collected yet","description":"Cost: free. Drops this job's queued rows. Delivered horses stay collectable, and\nnothing already billed is re-billed.","operationId":"cancel_backfill_v1_racing_horses_backfill__job_id__delete","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Cancel Backfill V1 Racing Horses Backfill  Job Id  Delete"},"example":{"job_id":"bf_20260907_3a9c1e","cancelled":17}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/horses/backfill/{job_id}/results":{"get":{"tags":["Racing"],"summary":"Collect the form of horses this job has delivered","description":"Cost: free. The 3 credits per horse were charged when it was delivered, and\nre-reading data you have already paid for costs nothing.\n\nEach row is the same payload /v1/racing/horses/form returns for that horse — profile,\ncareer, runs — so anything written against the form endpoint reads this unchanged.\nRows appear as they complete: collect early and often rather than waiting for the\nwhole batch.","operationId":"backfill_results_v1_racing_horses_backfill__job_id__results_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":25,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Backfill Results V1 Racing Horses Backfill  Job Id  Results Get"},"example":{"job_id":"bf_20260907_3a9c1e","rows_returned":1,"total_done":21,"limit":50,"offset":0,"rows":[{"input":"Vomo Island","runner_ref":"ra:NzkyMzc2NTE1MA","horse_name":"Vomo Island","delivered_at":"2026-09-07T00:58:41Z","fetched_at":"2026-09-07T00:58:39Z","available":true,"form":{"horse_name":"Vomo Island","runner_ref":"ra:NzkyMzc2NTE1MA","ambiguous":false,"candidates":[],"profile":{"age_sex":"5yo Bay Gelding","trainer":"John Lugg","trainer_location":"Kalgoorlie"},"career":{"starts":24,"wins":5,"seconds":4,"thirds":3,"prizemoney":118450},"runs":[{"date":"2026-09-06","track":"KALG","race_number":8,"trial":false,"position":1,"field_size":14,"distance_m":1200,"going":"Good","going_rating":4,"jockey":"Austin Galati","weight_kg":57.0,"barrier":3,"sp":4.6,"prices":[5.0,4.8,4.6],"margin_l":0.0,"pir":[],"placegetters":[]}],"source":{"feed":"racing_australia","region":"AU","sport":"thoroughbred","fetched_at":"2026-09-07T00:58:39+00:00","stale":false}}}]}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/jockeys/stats":{"get":{"tags":["Racing"],"summary":"Jockey premiership leaderboards and lookup","description":"Cost: 2 credits. Jockey premiership leaderboards — 1st-5th finishes, prize\n    money, strike rate and starts — from Racing Australia's own published tables.\n\n    WHAT A LEADERBOARD IS. RA publishes each premiership as a TOP-20 table, one per\n    state and scope, and that is exactly what is held: the national table (`state=AUS`)\n    and the eight state tables, each split all/metro/provincial/country/picnic, for the\n    running season and the previous one (RA seasons run 1 Aug - 31 Jul). There is no\n    page two at the source, so a jockey outside every top-20 has no row here — an empty\n    `rows` for a name means \"outside the published top 20\", never \"inactive\". For\n    per-horse jockey identity on individual runs, /v1/racing/horses/form carries it\n    with a stable code on every run.\n\n    LOOKUP SHAPES. With no `name`, the response is one table: `state` (default AUS) x\n    `scope` (default all) for the chosen `season`. With `name`, the search widens to\n    every state and scope for that season by default — a rider prominent only in one\n    state's country scope is found without knowing which — and `state`/`scope`, if\n    given, narrow it back. Name matching is case-insensitive on the whole name or any\n    part of it.\n\n    JOINS. `ra_code` is the code inside RA's own engagement links, stored with padding\n    stripped — the same identity namespace as `jockey_code` on horse-form runs and\n    `trainer_code` on acceptance rows, so premiership standing joins to per-run data on\n    code, not name spelling.\n\n    HOW FRESH ANY GIVEN TABLE IS. There are 180 of these tables and Racing Australia\n    will not serve them all in one sitting, so they are refreshed in paced passes\n    overnight Australian time: the national tables (`state=AUS`) daily, and the state\n    tables on a rolling refresh that takes a few nights to come round. Nothing is\n    guessed or interpolated in between — a table is exactly as RA last published it.\n    Read the age off the response rather than assuming it: `period_to` on the running\n    season is the day RA rendered that table, so it dates every row, and\n    `source.scraped_at` gives the newest scrape among the rows returned. A state table\n    added to the store for the first time simply appears; until then its `rows` are\n    empty, the same shape as a name that is outside every top 20.\n\n    `season=previous` serves the completed season, useful as the longer baseline of the\n    two. It changes only on a late protest, so it is refreshed about weekly.\n\n    Cached 1 hour.","operationId":"jockey_stats_v1_racing_jockeys_stats_get","parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Find one jockey by (partial) name across every state and scope.","title":"Name"},"description":"Find one jockey by (partial) name across every state and scope."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"AUS (national) or a state. Default AUS, unless `name` widens the search.","title":"State"},"description":"AUS (national) or a state. Default AUS, unless `name` widens the search."},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"all, metro, provincial, country, picnic. Default all, unless `name` widens the search.","title":"Scope"},"description":"all, metro, provincial, country, picnic. Default all, unless `name` widens the search."},{"name":"season","in":"query","required":false,"schema":{"type":"string","description":"current or previous RA season (1 Aug - 31 Jul).","default":"current","title":"Season"},"description":"current or previous RA season (1 Aug - 31 Jul)."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PremiershipOut"},"example":{"role":"jockey","season_start_year":2026,"state":"AUS","scope":"all","name":null,"rows":[{"state":"AUS","scope":"all","rank":1,"name":"William Pike","ra_code":"OTI5MTgwMDAw","firsts":24.0,"seconds":13.0,"thirds":7.0,"fourths":8.0,"fifths":8.0,"prize_money":1115623.0,"strike_rate":26.7,"starts":90,"period_from":"2026-08-01","period_to":"2026-09-07"},{"state":"AUS","scope":"all","rank":2,"name":"Zac Lloyd","ra_code":"MzY0MzEwNjc3NTA","firsts":22.0,"seconds":13.0,"thirds":7.0,"fourths":8.0,"fifths":6.0,"prize_money":1772645.0,"strike_rate":30.6,"starts":72,"period_from":"2026-08-01","period_to":"2026-09-07"}],"source":{"feed":"racing-australia-premierships","region":"AU","sport":"thoroughbred","scraped_at":"2026-09-06T19:40:44+00:00","top_n":20}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/trainers/stats":{"get":{"tags":["Racing"],"summary":"Trainer premiership leaderboards and lookup","description":"Cost: 2 credits. Trainer premiership leaderboards — 1st-5th finishes, prize\n    money, strike rate and starts — from Racing Australia's own published tables.\n\n    WHAT A LEADERBOARD IS. RA publishes each premiership as a TOP-20 table, one per\n    state and scope, and that is exactly what is held: the national table (`state=AUS`)\n    and the eight state tables, each split all/metro/provincial/country/picnic, for the\n    running season and the previous one (RA seasons run 1 Aug - 31 Jul). There is no\n    page two at the source, so a trainer outside every top-20 has no row here — an empty\n    `rows` for a name means \"outside the published top 20\", never \"inactive\". For\n    per-horse trainer identity on individual runs, /v1/racing/horses/form carries it\n    with a stable code on every run.\n\n    LOOKUP SHAPES. With no `name`, the response is one table: `state` (default AUS) x\n    `scope` (default all) for the chosen `season`. With `name`, the search widens to\n    every state and scope for that season by default — a rider prominent only in one\n    state's country scope is found without knowing which — and `state`/`scope`, if\n    given, narrow it back. Name matching is case-insensitive on the whole name or any\n    part of it.\n\n    JOINS. `ra_code` is the code inside RA's own engagement links, stored with padding\n    stripped — the same identity namespace as `jockey_code` on horse-form runs and\n    `trainer_code` on acceptance rows, so premiership standing joins to per-run data on\n    code, not name spelling.\n\n    HOW FRESH ANY GIVEN TABLE IS. There are 180 of these tables and Racing Australia\n    will not serve them all in one sitting, so they are refreshed in paced passes\n    overnight Australian time: the national tables (`state=AUS`) daily, and the state\n    tables on a rolling refresh that takes a few nights to come round. Nothing is\n    guessed or interpolated in between — a table is exactly as RA last published it.\n    Read the age off the response rather than assuming it: `period_to` on the running\n    season is the day RA rendered that table, so it dates every row, and\n    `source.scraped_at` gives the newest scrape among the rows returned. A state table\n    added to the store for the first time simply appears; until then its `rows` are\n    empty, the same shape as a name that is outside every top 20.\n\n    `season=previous` serves the completed season, useful as the longer baseline of the\n    two. It changes only on a late protest, so it is refreshed about weekly.\n\n    Cached 1 hour.","operationId":"trainer_stats_v1_racing_trainers_stats_get","parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Find one trainer by (partial) name across every state and scope.","title":"Name"},"description":"Find one trainer by (partial) name across every state and scope."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"AUS (national) or a state. Default AUS, unless `name` widens the search.","title":"State"},"description":"AUS (national) or a state. Default AUS, unless `name` widens the search."},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"all, metro, provincial, country, picnic. Default all, unless `name` widens the search.","title":"Scope"},"description":"all, metro, provincial, country, picnic. Default all, unless `name` widens the search."},{"name":"season","in":"query","required":false,"schema":{"type":"string","description":"current or previous RA season (1 Aug - 31 Jul).","default":"current","title":"Season"},"description":"current or previous RA season (1 Aug - 31 Jul)."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PremiershipOut"},"example":{"role":"trainer","rows":[{"fifths":14.0,"firsts":17.0,"fourths":22.0,"name":"Chris Waller","period_from":"2026-08-01","period_to":"2026-09-02","prize_money":2437350.0,"ra_code":"Njg0MjM2MA","rank":1,"scope":"all","seconds":23.5,"starts":147,"state":"NSW","strike_rate":11.6,"thirds":20.0}],"scope":"all","season_start_year":2026,"source":{"feed":"racing-australia-premierships","region":"AU","scraped_at":"2026-09-02T07:10:00+00:00","sport":"thoroughbred","top_n":20},"state":"NSW"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/changes":{"get":{"tags":["Racing"],"summary":"Races and prices that changed since a timestamp","description":"Cost: 2 credits. Everything that moved on the live racing board since `since` — one\npoll instead of re-downloading the whole card to diff it yourself. Never cached; serving\na cached delta would hand you changes you already have and hide the ones you do not.\n\n**Poll contract.** Call with a recent `since`, then send the `server_time` from each\nresponse as the `since` of the next call. `server_time` is deliberately set back 30\nseconds from the server clock: ingest stamps a price change in Python and commits the\nbatch afterwards, so a cursor taken at the exact clock would step over a write that was\nin flight and lose that price change permanently. The setback covers two racing poll\ncycles, which makes this feed AT LEAST ONCE — a change may arrive twice, never zero\ntimes. Deduplicate on (race_id, name, bookmaker_key, updated_at); that tuple is stable\nacross a redelivery. Measured over two live polls 75 seconds apart, 2026-09-01: 487 of\n530 runner deltas on the second poll were new and 43 were the deliberate replay.\n\n**`since` older than 30 minutes returns 422 and is not charged.** The live board holds\nupcoming races only and purges a race once it jumps, so a gap wider than the board's own\nlifetime cannot be served from it. Do a full fetch from /v1/racing/next-to-go and resume\npolling here from the `server_time` it hands back.\n\n**What drives `changed_runners`.** `race_runners.updated_at`, which is a genuine\nlast-price-CHANGE stamp, not a poll stamp. Ingest rewrites every runner row on every poll\nbut carries the previous timestamp forward when the win and lay prices are unchanged, so\na steady price does not appear here. Measured on the live board 2026-09-01: 97.4% of\nrunner rows were re-polled inside 2 minutes while 22.4% had an `updated_at` inside the\nsame window. A move in `place_price` alone does not advance the column and is delivered\non that runner's next win-price move — poll /v1/racing/next-to-go instead if place prices\nare your primary signal.\n\n**A race with an empty `changed_runners` is expected.** A race is also returned when its\nown row was written since `since`, and that row's timestamp advances on Betfair market\nvolume as well as on real state changes, so the race-level arm is a superset. On the\nfirst live poll of this endpoint, 46 of 61 races arrived on that arm alone. Read each\nrace object as the CURRENT state of the race and compare against what you hold:\n`scratchings` is always the complete current list for the race, never a delta, so a\nrunner leaving it is visible as an absence.\n\n`status` is not a state signal and is documented as such on the response model: it is\nwritten once at row creation and never revised while a race is upcoming, so it reads\n\"open\" for every race here.\n\n`market_status`, `inplay` and the `market_closed_at` / `inplay_at` lifecycle stamps are\ngated on WHICH FEED reported the state, the same rule /v1/racing/events applies. PointsBet\ncloses its book at the real jump and its state is served to everybody; Betfair Exchange\nstate is withheld from customer responses pending a data licence, so a race whose state\nonly the exchange saw reads null for customers. `market_state_source` names the feed\nbehind the values you receive. The race-level facts a customer can act on are\n`scratchings`, a revised `start_time`, and the lifecycle stamps.\n\nBetfair prices are withheld from customer keys here exactly as they are on every other\nracing endpoint, so a Betfair-only price move produces no `changed_runners` entry.\n\n`race_id` is the same stable identifier used by /v1/racing/next-to-go,\n/v1/racing/price-history and /v1/racing/results.","operationId":"racing_changes_v1_racing_changes_get","parameters":[{"name":"since","in":"query","required":true,"schema":{"type":"string","description":"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.","title":"Since"},"description":"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."},{"name":"categories","in":"query","required":false,"schema":{"type":"string","description":"horse, greyhound, harness — comma-separated. Omit for all three. An unknown value is refused rather than silently ignored.","title":"Categories"},"description":"horse, greyhound, harness — comma-separated. Omit for all three. An unknown value is refused rather than silently ignored."},{"name":"country","in":"query","required":false,"schema":{"type":"string","description":"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.","title":"Country"},"description":"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."},{"name":"include_unresolved","in":"query","required":false,"schema":{"type":"boolean","description":"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.","default":false,"title":"Include Unresolved"},"description":"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."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RacingChangesOut"},"example":{"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"}]}]}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/track-conditions":{"get":{"tags":["Racing"],"summary":"Track condition, weather and rail: current state and today's change log","description":"Cost: 1 credit. Per meeting: the CURRENT official track condition, weather and rail\nposition (Racing Australia, AU thoroughbred meetings, refreshed every two hours) and the\nday's CHANGE LOG — every transition we observed, with the previous value beside the new\none, when it was seen and which feed saw it.\n\n**Two kinds of change row, told apart by `source`.** `racing_australia` rows come from\nRA's own meeting page, so they are the official rating and carry the rail; bookmaker\nrows (`betright`, `ladbrokes_au`, ...) come from a book's race page moving between two\nof our polls and carry `race_id`. A book row can land minutes before RA re-publishes,\nand a book's spelling (\"Soft (6)\") differs from RA's (\"Soft 6\"); the log compares\nnormalised values per feed, so the same rating spelled two ways never appears as a\nchange, and a book that merely lags another produces no row.\n\n**Baselines.** The first RA row for a meeting has every `prev_*` null: it is the state\nwhen we first read the page (about two days out), not a change. Bookmaker rows are only\never written for a genuine transition, so they always carry a `prev_track_condition`.\n\n**Coverage.** Official state is AU thoroughbred only — RA publishes nothing for\ngreyhounds or harness, so those meetings appear here only when a book reported a\nchange, with `current` labelled by that book. New Zealand meetings likewise.\n\n**One entry per meeting, whatever each feed calls it** (2026-09-10). RA welds the\nsponsor into its own venue key ('Picklebet Park Warwick') and the books send the bare\ntown ('Warwick'), so the same card used to arrive here twice — once official with the\nrail and once as a bookmaker shell with the change history. Spellings are now folded\nonto one meeting by venue_id, by the sponsor word-subset rule, and (for a course inside\na parent racecourse, e.g. the books' 'Randwick' against RA's 'Kensington') by\nvenue_site, each fold refusing to act unless exactly one candidate meeting qualifies\nand every change row on the bookmaker key is positively categorised thoroughbred.\n`merged_from` lists every raw venue_norm folded in and `official_venue_norm` names RA's\nspelling, so a join on either old string still finds the meeting, and `venue` selects it\nby any of them. A merged entry is published under RA's spelling, because a bookmaker's\ncan be ambiguous about which course of a complex ran ('sandown' for a Sandown HILLSIDE\ncard) and `venue_id` must never name the wrong track; where RA's spelling is not yet a\nvenue-registry entry its `venue_id` is that spelling's slug\n('picklebet-park-warwick'), which is unmapped rather than wrong.\n\n**Mixed `current`.** `current` is built field by field: RA's official block wherever RA\npublished a value, a book filling only a field RA left null, and RA's own later row\nsuperseding either. `current.sources` names the feed behind each value field and\n`current.field_as_of` when it was observed; the block-level `source` reads `mixed` when\nmore than one feed is behind the block, and the block-level `as_of` is the OLDEST of\nthe per-field instants, so neither overstates what the block knows. `changes` keeps\nevery row from both feeds in one sequence.\n\n**`going` / `going_rating`** sit beside `track_condition` (and `prev_going` /\n`prev_going_rating` beside `prev_track_condition`) as the same vocabulary\n/v1/racing/horses/form publishes, so 'Soft 6', 'Soft (6)' and 'Good4' need one parser\nand not three. The raw string is unchanged.\n\nThe live endpoints carry the same signal per race as `track_condition_changed_at` — the\ninstant of the latest logged transition for that race's meeting — so a poller reading\n/v1/racing/changes need not call this to know something moved; call this for the\nsequence. `date` before 7 days ago or malformed is a free 422.","operationId":"track_conditions_v1_racing_track_conditions_get","parameters":[{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Meeting day, Australia/Sydney, YYYY-MM-DD. Defaults to today. Up to 7 days back.","title":"Date"},"description":"Meeting day, Australia/Sydney, YYYY-MM-DD. Defaults to today. Up to 7 days back."},{"name":"venue","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"One meeting, case-insensitive ('Belmont', 'sandown hillside'). Omit for every meeting on the day.","title":"Venue"},"description":"One meeting, case-insensitive ('Belmont', 'sandown hillside'). Omit for every meeting on the day."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackConditionsOut"},"example":{"date":"2026-09-10","meetings":[{"category":"horse","changes":[{"category":"horse","going":"Good","going_rating":4,"observed_at":"2026-09-09T04:16:29Z","rail":"True","source":"racing_australia","track_condition":"Good 4","weather":"Windy"},{"category":"horse","going":"Good","going_rating":4,"observed_at":"2026-09-10T02:36:58Z","prev_going":"Good","prev_going_rating":3,"prev_track_condition":"Good3","race_id":"race_123","source":"betgold","track_condition":"Good4"}],"current":{"as_of":"2026-09-10T02:17:24Z","field_as_of":{"penetrometer":"2026-09-10T02:17:24Z","rail":"2026-09-10T02:17:24Z","track_condition":"2026-09-10T02:17:24Z","track_type":"2026-09-10T02:17:24Z","weather":"2026-09-10T02:17:24Z"},"going":"Good","going_rating":4,"penetrometer":"5.63","rail":"True","source":"racing_australia","sources":{"penetrometer":"racing_australia","rail":"racing_australia","track_condition":"racing_australia","track_type":"racing_australia","weather":"racing_australia"},"track_condition":"Good 4","track_type":"Turf","weather":"Windy"},"merged_from":["picklebet park warwick","warwick"],"official_venue_norm":"picklebet park warwick","venue":"picklebet park warwick","venue_id":"picklebet-park-warwick","venue_norm":"picklebet park warwick","venue_site":"picklebet-park-warwick"}],"note":"…"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/racing/acceptances":{"get":{"tags":["Racing"],"summary":"Full-day AU thoroughbred acceptance card: every meeting, race and runner","description":"Cost: 2 credits. The day's complete AU thoroughbred card as Racing Australia\npublishes it — meeting, then race, then runners in saddlecloth order — with no\ndependency on any bookmaker having framed a market. Per runner: saddlecloth number,\nbarrier, jockey, trainer, weight, the stable `runner_ref`, Last-10 figures, Hcp Rating,\nthe trainer/raceentry codes, the `emergency` flag, and `scratched` plus a `scratching`\nblock (RA's own scratch time, late flag, \"(Eliminated)\" marker and note) for a runner\nalready off. Per race: official name, local start time, distance, the conditions line\nverbatim, class, prize total and per-place breakdown, bonus note, track name/type,\nfield limit, and the counts. Per meeting: rail, track type, condition — as RA's raw\n`track_condition` and, parsed from it, `going` plus `going_rating`, the same pair\n/v1/racing/horses/form publishes — weather, penetrometer, its own `as_of`,\n`emergency_data`, `scratching_data` and `withheld_acceptance_data`.\n\n**No prices, on purpose.** This is the entry list, not a market. It is available about\ntwo days out, which is the point of it — /v1/racing/next-to-go and\n/v1/racing/best-odds carry prices when books open, and this endpoint stays readable\nlong before they do.\n\n**Scratched runners are flagged, never removed.** They keep their place in the card so\nit can be reconciled line-for-line against RA's official sheet.\n\n**`emergency` is true, false or null.** Null means the meeting was scraped before the\nemergency parser went live (2026-09-08 01:01:39 UTC) — its emergency acceptances are\nabsent from the source table entirely, so \"false\" would be a fabrication. The meeting\nsays which case it is in `emergency_data`: \"parsed\" or \"unknown\".\n\n**`scratched` is true, false or null too, for the same reason.** RA's scratching sheet\ndoes not exist for every date in the window — measured 2026-09-08, 2026-09-05 and\n2026-09-06 have no scratching row at all while 09-07 has 96 and 09-08 has 77 — and the\nscraper fetches each meeting's sheet in its own try/except, so one meeting can lose its\nsheet while its acceptances still write. A row from the sheet is the only positive\nevidence it was read, so each meeting publishes `scratching_data`:\n  * `\"parsed\"` — the sheet was read for this meeting; `scratched` is a real boolean\n    and the counts resting on it are real numbers. `scratching_sheet_as_of` says when.\n  * `\"unknown_date\"` — no meeting on this date has a scratching row. No sheet exists\n    for the day; `scratched` is null everywhere on it.\n  * `\"unknown_meeting\"` — other meetings on this date have a sheet and this one does\n    not: either nobody was scratched here or this meeting's fetch failed, and nothing\n    on this card tells them apart, so `scratched` is null.\nDo not read a null as \"not scratched\", and do not read `scratching_data` other than\n\"parsed\" as \"clean meeting\".\n\n**The counts name their population.** An emergency acceptance runs only if it is\nPROMOTED and RA does not publish whether it was, so a count that mixes emergencies\nwith declared runners overstates the field. Every level (race, meeting, day) carries:\n`runner_count` (rows held, scratched and emergencies included — a floor wherever\n`runner_set_status` is not \"checks_passed\"), `emergency_count`, `declared_count`\n(runner_count minus emergency_count), `scratched_count`, `unscratched_count`\n(runner_count minus scratched_count, a reconciliation figure that STILL includes\nemergencies), `declared_scratched_count`, and `declared_remaining_count` — the horses\nexpected to jump barring a promotion, which is the number to render as the field.\nWorked example, Moe R1 on 2026-09-08: 17 acceptances, 5 emergencies (#1, #14-#17),\n12 declared, 6 scratchings all of declared runners — 11 rows are unscratched but only\n6 declared horses remain. Anything resting on an unknown fact is null, not a guess,\nand the day totals are null unless every meeting with rows knows the fact. With NO\nmeeting on the card publishing acceptances there is nothing to total: runner_count is\na real 0 and every other day count is null, matching what those meetings each report,\nand `counts_cover_every_published_meeting` is false.\n\n**On a race that is not \"checks_passed\", every count is a FLOOR.** All seven are\ncomputed over the acceptance rows this card HOLDS, and such a race is one the card has\nitself declared short. Two of them looked exact and were not.\n`scratched_count` and `declared_scratched_count` count sheet rows MATCHED to an\nacceptance row, so a scratching with no row here is in `missing_runners` and not in the\ncount: Townsville R6 on 2026-09-08 reads `scratching_data: \"parsed\"`,\n`scratched_count: 0`, while RA's sheet lists #4 Five Star King and #9 Kickoff — at\nleast 2. And `declared_remaining_count` is the number to render as the field ONLY on a\n\"checks_passed\" race: Townsville R5 that day is \"gap_detected\" with saddlecloth #6\nunaccounted for and NOT on the sheet — a horse that may well be jumping — so its 11 is\n\"at least 11\", never an exact field. Each such race's `runner_set_note` states the\nbound explicitly.\n\n**Completeness is cross-checked, and the note says what was OBSERVED.** ra_race_fields\nis keyed (meeting_date_aet, runner_key) without a race number, so a horse accepted in\ntwo races on one Sydney day has ONE slot — and since 2026-09-08 the acceptance pass\ngives that slot to NEITHER race rather than publish one race's barrier and weight\nagainst the other race's runner. It does not \"survive in only one\", which is what this\ndocumentation said until 2026-09-10 and which cost a caller every one of the 14\nhorses / 29 slots withheld on that date alone. **Do not de-duplicate by `runner_key`\nto recover them — there is nothing to de-duplicate.** Read `missing_runners` instead:\nevery withheld slot is named there, in its own race, with its REAL saddlecloth number,\n`source` `\"withheld_acceptance\"`, and a `cause_detail` naming the slot published in its\nplace where one was. Each race is checked three ways — for saddlecloth holes, against\nthe independently-keyed scratching sheet where one exists, and against the withheld\nslots the scraper recorded as it discarded them — then reports `runner_set_status`\n(\"checks_passed\", \"gap_detected\" or \"unverified\"), a `runner_set_note` and any\n`missing_runners`. Each missing runner carries the cause actually observed for it —\n`duplicate_runner_key` (the collision is visible, not assumed: the same runner_key is\nheld elsewhere on this card, or the scraper recorded discarding the slot),\n`declared_preferred` (a declared acceptance elsewhere on the day won the key and this\nrace's emergency slot was dropped — nothing collided, so do not hunt a second race),\n`eliminated_emergency` (RA's sheet marks it an emergency and no acceptance row exists)\nor `not_determinable` — plus a `cause_detail` naming the observation. The endpoint\ndoes not guess which of several indistinguishable causes applies.\n\n**The withheld check is the only one that can see a loss above the top of the\nnumbering**, which is exactly where Racing Australia numbers its emergencies: drop\n#13e from a 1..14 field and 1..12 is perfectly contiguous. Measured 2026-09-10, that\nsilence certified Warwick R2 and Bunbury R2 as \"checks_passed\" with `emergency_count`\n0 while RA listed 14 and 13 entries against the 12 served each — false statements\nunder a schema that calls `declared_remaining_count` exact on a checks_passed race.\nEach meeting publishes `withheld_acceptance_data`: `\"parsed\"` (this card placed this\nmeeting's withheld rows, so its discards are accounted for below), `\"unplaced\"` (the\npass recorded discards here and this card could NOT place a single one — the meeting's\nacceptance fields are unpublished, or the race the row names is not on this card;\n`withheld_acceptance_slots_unplaced` counts them and\n`completeness.withheld_acceptance_slots_unplaced` totals the card) or `\"unknown\"` (no\nrow at all — either nothing was withheld, or the pass that read this meeting predates\nthe record). A row is the only positive evidence, so do NOT read `\"unknown\"` as\n\"nothing was lost\"; `completeness.withheld_acceptances` says whether the whole date\nholds any record at all.\n\nEvery `missing_runners` entry carries `withheld_acceptance` (true/false) — the\nprovenance of the loss, and the field `completeness.withheld_acceptance_slots` counts.\nRead THAT, not `source`: one slot can be named by RA's scratching sheet AND recorded in\nra_withheld_acceptances, and it is listed once, keeping the sheet's `source` (the sheet\nsaw the scratching) while carrying the withheld cause and `withheld_acceptance: true`.\nA withheld slot whose horse this card already holds in that SAME race under another\nsaddlecloth is not listed at all: RA renders one horse twice in one race under two\nsaddlecloths, only one row can be published, and no runner is missing from the race.\n\n**A past-date card is a snapshot and will never complete.** `date_status` is \"past\",\n\"today\" or \"future\". The scraper only ever fetches today-and-forward Form keys, so on\na PAST date: a meeting at `acceptances_published: false` will NEVER fill in (live on\n2026-09-05: sportsbet bundaberg and sportsbet mareeba) — calling again cannot help,\nand the meeting's `publication_note` says so; and the scratchings are frozen at the\nlast scrape of that day, roughly midday race-day, with no later movement recoverable\nfrom anywhere, /v1/racing/changes included. On \"today\" or \"future\" the next scrape CAN\nadd both, every two hours — but that is not a promise that the card converges, and this\ndocumentation used to make one. Racing Australia removes a meeting from home.aspx once\nit is UNDERWAY, and home.aspx is the scraper's only discovery surface, so a meeting's\nrows stop advancing around 12:15-14:15 AEST while its own races run on to 17:00-19:30.\nLog-verified 2026-09-09: the 02:15Z pass discovered all six of that day's meetings, the\n04:15Z pass discovered one, the 06:15Z pass discovered none. The in-band signal is per\nmeeting — its own `as_of` and `scratching_sheet_as_of`. A stamp that stops moving while\nlater passes land on other meetings is a meeting that has gone underway, not a quiet\none, and from that point `/v1/racing/changes` carries the race-day movement.\n\n**Why 2 credits.** A whole day is one request assembled from four reference tables: a\nmidweek card is a few hundred runners over a handful of meetings and a Saturday well\nover a thousand across seventeen or more (measured: 372 runners / 4 meetings on\nTuesday 2026-09-08; 1,259 / 17 on Saturday 2026-09-05). That is materially more than\n/v1/racing/track-conditions (1 credit, tens of rows) and is priced level with\n/v1/racing/next-to-go; it does no cross-book price assembly, so it is not a 3. The\nunderlying scrape only moves every two hours — calling this once or twice a day is the\nintended shape, not polling it.\n\n**Coverage, honestly.** Australian THOROUGHBRED meetings only. Racing Australia\npublishes no acceptance card for greyhounds or harness, and none for New Zealand, so\nthose meetings are absent here — not empty, absent. There is no history: the reference\ntables are pruned on a 3-day rule, so a date more than 3 days back is a free 422 rather\nthan a truthful-looking empty card.\n\n**How current it is.** `as_of` is the newest RA read backing the meetings actually\nreturned (a venue filter narrows it, so one stale meeting cannot borrow another's\nfreshness), and each meeting carries its own plus `scratching_sheet_as_of`.\n\nA malformed or out-of-window `date`, and an unknown query parameter, are both a free\n422 — nothing is charged.","operationId":"acceptances_v1_racing_acceptances_get","parameters":[{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Meeting day, Australia/Sydney, YYYY-MM-DD. Defaults to today. Up to 3 days back and 3 days ahead.","title":"Date"},"description":"Meeting day, Australia/Sydney, YYYY-MM-DD. Defaults to today. Up to 3 days back and 3 days ahead."},{"name":"venue","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"One meeting, case-insensitive ('Moe', 'sandown hillside'). Omit for every meeting on the day.","title":"Venue"},"description":"One meeting, case-insensitive ('Moe', 'sandown hillside'). Omit for every meeting on the day."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptancesOut"},"example":{"as_of":"2026-09-08T02:16:38.306760Z","completeness":{"counts_cover_every_published_meeting":true,"known_missing_runners":6,"meetings_acceptances_unpublished":0,"meetings_emergency_data_unknown":0,"meetings_scratching_data_unknown":0,"meetings_scratching_sheet_absent_for_whole_date":0,"meetings_withheld_acceptance_data_unknown":4,"races_checks_passed":24,"races_gap_detected":5,"races_unverified":0,"runners_without_emergency_data":0,"runners_without_scratching_data":0,"withheld_acceptance_slots":0,"withheld_acceptance_slots_unplaced":0,"withheld_acceptances":"unknown_date"},"date":"2026-09-08","date_status":"today","declared_count":334,"declared_remaining_count":272,"declared_scratched_count":62,"emergency_count":38,"meeting_count":4,"meetings":[{"acceptances_published":true,"as_of":"2026-09-08T02:16:38.306760Z","declared_count":89,"declared_remaining_count":59,"declared_scratched_count":30,"emergency_count":18,"emergency_data":"parsed","race_count":7,"races":[{"conditions":"Maiden, Set Weights, Three-Years-Old and Upwards, Apprentices can claim.","declared_count":12,"declared_remaining_count":6,"declared_scratched_count":6,"distance_m":1108,"emergency_count":5,"field_limit":"12 + 6 EM","missing_runners":[],"prize_breakdown":{"1st":17600,"2nd":5760,"3rd":2880},"prize_total":32000,"race_class":"Maiden","race_name":"Moe Denture Clinic Maiden Plate","race_number":1,"runner_count":17,"runner_set_status":"checks_passed","runners":[{"barrier":14,"emergency":false,"jockey":"Teo Nugent","last10":"2x3x4","number":2,"raceentry_code":"MzE2ODU1MTEwOTY","runner_key":"backupbarbie","runner_name":"Back Up Barbie","runner_ref":"ra:MTA3MjIxMjU2MzI","scratched":true,"scratching":{"emergency":false,"late":false,"scratched_at":"2026-09-07T21:29:00Z"},"trainer":"Ciaron Maher","trainer_code":"NDU4MDA0NzI","weight_kg":57.5}],"scratched_count":6,"start_time_local":"2:00PM","track_name":"Main","track_type":"Turf","unscratched_count":11}],"runner_count":107,"runner_set_status":"checks_passed","scratched_count":34,"scratching_data":"parsed","scratching_sheet_as_of":"2026-09-08T02:16:38.306760Z","track":{"dual_track":"N","going":"Heavy","going_rating":9,"penetrometer":"5.96","rail":"Out 3m Entire Circuit","track_condition":"Heavy 9","track_info":"Inspection 5:30AM 8/9;","track_type":"Turf","weather":"Fine"},"unscratched_count":73,"venue":"Moe","venue_id":"moe","venue_norm":"moe","venue_site":"moe","withheld_acceptance_data":"unknown","withheld_acceptance_slots_unplaced":0}],"note":"…","race_count":29,"runner_count":372,"scratched_count":72,"unscratched_count":300}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/referral":{"get":{"tags":["Referrals"],"summary":"Your referral link and earnings","description":"Your referral code, the link to share, and what it has earned.\n\nThe code is minted on your first call to this endpoint and never changes afterwards, so the\nlink is safe to print. Sharing it costs nothing and referring a free signup earns nothing:\nthe commission is on what a referred customer actually pays.\n\n**What you earn.** 10% of every invoice a customer you referred pays us, for 12 months from\ntheir first payment, taken either as cash or as API credits — `terms` in the response\ncarries the live numbers rather than these words, which is what you should read from.\n\n**Credits settle immediately** and are added to your allowance for the current month.\n**Cash is held** until it clears the minimum payout in `terms.min_payout_aud`, then paid by\ntransfer. `owing_aud` is what is currently unpaid.","operationId":"my_referral_v1_referral_get","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["Referrals"],"summary":"Choose how your referral reward is paid","description":"Switch between `cash` and `credits`, and set a payout address.\n\nThe switch applies to commission accrued from here on. Anything already in your ledger keeps\nthe form it was earned in — we do not retroactively convert a paid-out dollar into credits or\nthe other way round.","operationId":"set_reward_v1_referral_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string","title":"X-Api-Key"}},{"name":"X-RapidAPI-Proxy-Secret","in":"header","required":false,"schema":{"type":"string","title":"X-Rapidapi-Proxy-Secret"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewardIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Send `X-API-Key`. See https://puntersedge.online/developers/errors","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Monthly credit allowance exhausted for this plan. Upgrade at https://puntersedge.online/api/pricing","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded for this plan. Honour the `Retry-After` header (seconds) before retrying. See https://puntersedge.online/developers/rate-limits","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"schemas":{"AcceptancesOut":{"properties":{"date":{"type":"string","title":"Date","description":"Meeting day, Australia/Sydney calendar date."},"date_status":{"type":"string","title":"Date Status","description":"'past', 'today' or 'future', relative to the Australia/Sydney date. On 'past' this card is a FROZEN SNAPSHOT: the scraper only fetches today-and-forward, so a meeting still showing acceptances_published false will never fill in and the scratching set can never converge. On 'today'/'future' the next scrape CAN add acceptances and scratchings, every two hours — but that is not a promise that a meeting converges, which is what this field used to make. Racing Australia removes a meeting from home.aspx once it is UNDERWAY and home.aspx is the scraper's only discovery surface, so a meeting's rows stop advancing around 12:15-14:15 AEST while its own races are still running to 17:00-19:30. Log-verified 2026-09-09: the 02:15Z pass discovered all six of that day's meetings, the 04:15Z pass discovered one, the 06:15Z pass discovered none. The in-band signal is per meeting, not per day — watch its own as_of and scratching_sheet_as_of: a stamp that stops moving while later passes land on other meetings is a meeting that has gone underway, not a quiet one. From that point race-day movement is on /v1/racing/changes, not here."},"meeting_count":{"type":"integer","title":"Meeting Count"},"race_count":{"type":"integer","title":"Race Count"},"runner_count":{"type":"integer","title":"Runner Count","description":"Acceptance rows on this card — scratched runners and emergencies INCLUDED. A floor, not a guarantee, wherever a race reports runner_set_status other than 'checks_passed'."},"emergency_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Emergency Count","description":"Rows flagged as an emergency acceptance. NULL unless every meeting with rows on this card has emergency_data 'parsed' — see completeness. A FLOOR, not an exact figure, wherever a race reports runner_set_status other than 'checks_passed': it counts only the acceptance rows this card holds."},"declared_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Declared Count","description":"runner_count minus emergency_count: the DECLARED field, i.e. the acceptances that are not emergencies. NULL when emergency_count is. A FLOOR wherever a race reports runner_set_status other than 'checks_passed'."},"scratched_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Scratched Count","description":"Sheet rows MATCHED to an acceptance row this card holds — NOT every scratching Racing Australia lists. A sheet row with no acceptance row here is not counted; it appears in that race's missing_runners with scratched true. Measured 2026-09-08: Townsville R6 has scratching_data 'parsed' and scratched_count 0 while the sheet holds #4 Five Star King and #9 Kickoff, so the true figure is at least 2. Read it as a FLOOR wherever a race reports runner_set_status other than 'checks_passed'; that race's runner_set_note states the bound. NULL unless every meeting with rows on this card has scratching_data 'parsed' — a meeting with no sheet cannot be said to have nobody scratched."},"unscratched_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Unscratched Count","description":"runner_count minus scratched_count. A RECONCILIATION number against the sheet, NOT a field size: it still includes emergencies, which run only if promoted. A FLOOR wherever a race reports runner_set_status other than 'checks_passed'. NULL when scratched_count is."},"declared_scratched_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Declared Scratched Count","description":"Rows that are scratched AND not emergencies. Like scratched_count it counts only sheet rows MATCHED to an acceptance row held here, so it is a FLOOR wherever a race reports runner_set_status other than 'checks_passed'. NULL unless BOTH emergency_data and scratching_data are 'parsed' for every meeting with rows."},"declared_remaining_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Declared Remaining Count","description":"declared_count minus declared_scratched_count: the horses expected to jump barring an emergency promotion, which Racing Australia does not publish. It is the number to render as the field ONLY where every race behind it reports runner_set_status 'checks_passed'. Anywhere else it is a FLOOR — render it as 'at least N', never as an exact field: a runner Racing Australia lists that this card does not hold, and that is not on the scratching sheet, may well be jumping. Measured 2026-09-08: Townsville R5 is 'gap_detected' with saddlecloth #6 unaccounted for and published 11. The race's runner_set_note states the bound. NULL unless BOTH facts are known everywhere."},"as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"As Of","description":"Newest RA scrape backing the meetings actually returned, UTC — computed after the venue filter."},"completeness":{"type":"object","title":"Completeness","description":"What could and could not be verified about this card."},"meetings":{"items":{"type":"object"},"type":"array","title":"Meetings"},"note":{"type":"string","title":"Note"}},"type":"object","required":["date","date_status","meeting_count","race_count","runner_count","completeness","meetings","note"],"title":"AcceptancesOut","example":{"as_of":"2026-09-08T02:16:38.306760Z","completeness":{"counts_cover_every_published_meeting":true,"known_missing_runners":6,"meetings_acceptances_unpublished":0,"meetings_emergency_data_unknown":0,"meetings_scratching_data_unknown":0,"meetings_scratching_sheet_absent_for_whole_date":0,"meetings_withheld_acceptance_data_unknown":4,"races_checks_passed":24,"races_gap_detected":5,"races_unverified":0,"runners_without_emergency_data":0,"runners_without_scratching_data":0,"withheld_acceptance_slots":0,"withheld_acceptance_slots_unplaced":0,"withheld_acceptances":"unknown_date"},"date":"2026-09-08","date_status":"today","declared_count":334,"declared_remaining_count":272,"declared_scratched_count":62,"emergency_count":38,"meeting_count":4,"meetings":[{"acceptances_published":true,"as_of":"2026-09-08T02:16:38.306760Z","declared_count":89,"declared_remaining_count":59,"declared_scratched_count":30,"emergency_count":18,"emergency_data":"parsed","race_count":7,"races":[{"conditions":"Maiden, Set Weights, Three-Years-Old and Upwards, Apprentices can claim.","declared_count":12,"declared_remaining_count":6,"declared_scratched_count":6,"distance_m":1108,"emergency_count":5,"field_limit":"12 + 6 EM","missing_runners":[],"prize_breakdown":{"1st":17600,"2nd":5760,"3rd":2880},"prize_total":32000,"race_class":"Maiden","race_name":"Moe Denture Clinic Maiden Plate","race_number":1,"runner_count":17,"runner_set_status":"checks_passed","runners":[{"barrier":14,"emergency":false,"jockey":"Teo Nugent","last10":"2x3x4","number":2,"raceentry_code":"MzE2ODU1MTEwOTY","runner_key":"backupbarbie","runner_name":"Back Up Barbie","runner_ref":"ra:MTA3MjIxMjU2MzI","scratched":true,"scratching":{"emergency":false,"late":false,"scratched_at":"2026-09-07T21:29:00Z"},"trainer":"Ciaron Maher","trainer_code":"NDU4MDA0NzI","weight_kg":57.5}],"scratched_count":6,"start_time_local":"2:00PM","track_name":"Main","track_type":"Turf","unscratched_count":11}],"runner_count":107,"runner_set_status":"checks_passed","scratched_count":34,"scratching_data":"parsed","scratching_sheet_as_of":"2026-09-08T02:16:38.306760Z","track":{"dual_track":"N","going":"Heavy","going_rating":9,"penetrometer":"5.96","rail":"Out 3m Entire Circuit","track_condition":"Heavy 9","track_info":"Inspection 5:30AM 8/9;","track_type":"Turf","weather":"Fine"},"unscratched_count":73,"venue":"Moe","venue_id":"moe","venue_norm":"moe","venue_site":"moe","withheld_acceptance_data":"unknown","withheld_acceptance_slots_unplaced":0}],"note":"…","race_count":29,"runner_count":372,"scratched_count":72,"unscratched_count":300}},"ArchiveCoverageOut":{"properties":{"archive_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Archive From"},"archive_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Archive To"},"rows":{"type":"integer","title":"Rows"},"races":{"type":"integer","title":"Races"},"bookmakers":{"type":"integer","title":"Bookmakers"},"is_closing_line_pct":{"type":"number","title":"Is Closing Line Pct"},"open_is_baseline_pct":{"type":"number","title":"Open Is Baseline Pct"},"resulted_races_pct":{"type":"number","title":"Resulted Races Pct"},"results_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Results From"},"settleable_races":{"type":"integer","title":"Settleable Races"},"settleable_resulted_pct":{"type":"number","title":"Settleable Resulted Pct"},"results_scope":{"type":"string","title":"Results Scope"},"venue_split_suspect_rows":{"type":"integer","title":"Venue Split Suspect Rows"},"name_fragment_suspect_rows":{"type":"integer","title":"Name Fragment Suspect Rows"},"by_category":{"items":{"type":"object"},"type":"array","title":"By Category"}},"type":"object","required":["archive_from","archive_to","rows","races","bookmakers","is_closing_line_pct","open_is_baseline_pct","resulted_races_pct","results_from","settleable_races","settleable_resulted_pct","results_scope","venue_split_suspect_rows","name_fragment_suspect_rows","by_category"],"title":"ArchiveCoverageOut"},"BackfillStatusOut":{"properties":{"job_id":{"type":"string","title":"Job Id"},"submitted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Submitted At"},"total":{"type":"integer","title":"Total"},"done":{"type":"integer","title":"Done"},"queued":{"type":"integer","title":"Queued"},"failed":{"type":"integer","title":"Failed"},"credits_charged":{"type":"integer","title":"Credits Charged","description":"3 per horse delivered so far."},"credits_blocked":{"type":"boolean","title":"Credits Blocked","description":"True when your remaining credits are below the cost of one horse, which pauses collection until they reset or you upgrade."},"queue_position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Queue Position","description":"Horses ahead of this job's oldest waiting row, across all jobs."},"estimated_ready":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Estimated Ready"},"ready_to_collect":{"type":"integer","title":"Ready To Collect","description":"Completed rows you have not yet read."}},"type":"object","required":["job_id","total","done","queued","failed","credits_charged","credits_blocked","ready_to_collect"],"title":"BackfillStatusOut"},"BackfillSubmitIn":{"properties":{"horses":{"items":{"type":"string"},"type":"array","title":"Horses","description":"Horse names or ra: refs. Max 500 per call. A ref is exact; a name is resolved against acceptances and archived results."},"label":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Label","description":"Your own tag for this batch, echoed back on status."}},"type":"object","required":["horses"],"title":"BackfillSubmitIn"},"BackfillSubmitOut":{"properties":{"job_id":{"type":"string","title":"Job Id"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"queued":{"type":"integer","title":"Queued","description":"Horses accepted and waiting on the worker."},"already_stored":{"type":"integer","title":"Already Stored","description":"Horses we already hold a fresh copy of. Collectable immediately, still billed at 3 credits each."},"duplicates":{"type":"integer","title":"Duplicates","description":"Already queued for you; not queued twice."},"not_found":{"items":{"type":"string"},"type":"array","title":"Not Found","description":"Names we have never seen on our own feeds.","default":[]},"ambiguous":{"items":{"type":"object"},"type":"array","title":"Ambiguous","description":"Names matching several horses. Re-submit with the runner_ref you meant.","default":[]},"estimated_ready":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Estimated Ready","description":"UTC estimate for the last horse, from the worker's measured pace."}},"type":"object","required":["job_id","queued","already_stored","duplicates"],"title":"BackfillSubmitOut"},"BestOddsEventOut":{"properties":{"id":{"type":"string","title":"Id"},"sport_key":{"type":"string","title":"Sport Key"},"home_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Home Team"},"away_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Away Team"},"commence_time":{"type":"string","title":"Commence Time"},"selections":{"items":{"type":"object"},"type":"array","title":"Selections"},"arb_exists":{"type":"boolean","title":"Arb Exists"},"arb_profit_pct":{"type":"number","title":"Arb Profit Pct"},"data_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Data Age Seconds"},"freshest_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Freshest Age Seconds"},"stale":{"type":"boolean","title":"Stale","default":false},"stale_bookmakers":{"items":{"type":"string"},"type":"array","title":"Stale Bookmakers","default":[]}},"type":"object","required":["id","sport_key","home_team","away_team","commence_time","selections","arb_exists","arb_profit_pct"],"title":"BestOddsEventOut","example":{"arb_exists":false,"arb_profit_pct":0.0,"away_team":"Storm","commence_time":"2026-06-07T09:30:00Z","home_team":"Broncos","id":"event_123","selections":[{"best_bookmaker":"sportsbet","best_price":2.1,"name":"Broncos"}],"sport_key":"nrl"}},"BestPricesOut":{"properties":{"id":{"type":"string","title":"Id"},"sport_key":{"type":"string","title":"Sport Key"},"home_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Home Team"},"away_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Away Team"},"commence_time":{"type":"string","title":"Commence Time"},"selections":{"items":{"type":"object"},"type":"array","title":"Selections"}},"type":"object","required":["id","sport_key","home_team","away_team","commence_time","selections"],"title":"BestPricesOut","example":{"away_team":"Storm","commence_time":"2026-06-07T09:30:00Z","home_team":"Broncos","id":"event_123","selections":[{"best_price":2.1,"name":"Broncos","price_spread":0.12}],"sport_key":"nrl"}},"ChangedRaceOut":{"properties":{"race_id":{"type":"string","title":"Race Id","description":"Stable race identifier — the same id /v1/racing/next-to-go, /v1/racing/price-history and /v1/racing/results use."},"venue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue"},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","description":"horse, greyhound or harness."},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country","description":"ISO country code, or null while the meeting is unresolved."},"start_time":{"type":"string","title":"Start Time","description":"Advertised start, UTC. It is revised as the meeting firms, and a revision bumps this race into the feed."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Set when the race row is created and revised exactly once, if at all: to 'abandoned' when Racing Australia posts an official abandoned, postponed or transferred notice for the meeting — that revision bumps every runner of the race into this feed (2026-09-06). Otherwise it reads 'open' for every race here. It is not a live market-state signal — do not poll this endpoint for it."},"market_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Status","description":"Betting-market state (OPEN, SUSPENDED, CLOSED). Reported by PointsBet, which sends CLOSED when it shuts its book at the jump, and by the Betfair Exchange, which is withheld from customer responses pending a data licence — so a value last written by the exchange reads null for customers. Same shape as /v1/racing/events."},"inplay":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Inplay","description":"True once the market is in-play. Exchange-only — bookmakers do not run racing in-play — so it carries the same licence hold as `market_status` and reads null for customers."},"market_closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Closed At","description":"UTC time the market was FIRST seen shut. Poll-observed and set once — see /v1/racing/events for the full semantics. Served when a BOOKMAKER reported the transition; null for customers when the exchange was the only feed that saw it. Null never means the market stayed open."},"inplay_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inplay At","description":"UTC time the race was FIRST seen in-play — the actual off, as opposed to `start_time`, which is advertised and moves. Exchange-only, so null for customer plans."},"market_state_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market State Source","description":"Which feed's market state produced `market_closed_at` / `inplay_at` — 'pointsbetau' or 'betfair_ex_au'. Set once, with the first stamp, and null whenever those stamps are withheld."},"scratchings":{"items":{"type":"object"},"type":"array","title":"Scratchings","description":"The COMPLETE current scratchings for this race, merged across every book that reports one — not a delta, so a runner leaving the list is visible as an absence. Greyhound cards also carry box-vacancy placeholders here (a literal '....' or 'Vacant Box'), which name an empty box rather than a withdrawn runner; match on `number` and treat a name from that set as a vacant box."},"track_condition_changed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition Changed At","description":"UTC instant of the most recent logged track-condition, weather or rail transition for this race's meeting (2026-09-05). Null when nothing has changed since we began watching it. Read it with `track_condition`: the value says WHAT, this says WHEN."},"track_condition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition","description":"The race's CURRENT track condition, not a change log — a poller diffing against what it holds sees a condition change the same way it sees a scratching. Null where no source reports it."},"weather":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Weather","description":"Current weather for the meeting, same read-as-current rule as track_condition."},"changed_runners":{"items":{"$ref":"#/components/schemas/ChangedRunnerOut"},"type":"array","title":"Changed Runners","description":"Runner prices that moved inside the window. An empty list means this race arrived on the race-level arm alone — expected, and explained in the endpoint description."}},"type":"object","required":["race_id","start_time"],"title":"ChangedRaceOut"},"ChangedRunnerOut":{"properties":{"name":{"type":"string","title":"Name","description":"Runner name as the bookmaker publishes it."},"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number","description":"Saddlecloth / rug number, where the book supplies one."},"bookmaker_key":{"type":"string","title":"Bookmaker Key","description":"Bookmaker this price belongs to."},"win_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Win Price","description":"Current fixed win price at this bookmaker."},"place_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Place Price","description":"Current fixed place price. A place-only move does not advance `updated_at` — see the endpoint description."},"top2_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top2 Price","description":"Current Top 2 (first-two) price at this bookmaker, where quoted."},"top3_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top3 Price","description":"Current Top 3 price at this bookmaker, where quoted."},"top4_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top4 Price","description":"Current Top 4 price at this bookmaker, where quoted."},"updated_at":{"type":"string","title":"Updated At","description":"UTC instant this runner's win or lay price last CHANGED at this bookmaker. Not the last poll: ingest carries this timestamp forward while the price is steady."}},"type":"object","required":["name","bookmaker_key","updated_at"],"title":"ChangedRunnerOut"},"ClosingLinesOut":{"properties":{"rows_returned":{"type":"integer","title":"Rows Returned"},"total_rows":{"type":"integer","title":"Total Rows","description":"Rows matching the filter before limit/offset"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"archive_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Archive From","description":"Earliest start_time in the whole archive. The archive only grows; this is its true floor as of right now, not a promise."},"window_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Window Days","description":"Set when the caller's plan clamps the lookback. NULL means the full archive was searched."},"resulted_rows":{"type":"integer","title":"Resulted Rows","description":"Rows in the matching set that have a result_status. Published because result coverage is thin and growing (5.3% of archived races on 2026-08-18) — do not assume a NULL finish_position means the runner lost. It means the runner is not in the published placings."},"rows":{"items":{"type":"object"},"type":"array","title":"Rows"}},"type":"object","required":["rows_returned","total_rows","limit","offset","resulted_rows","rows"],"title":"ClosingLinesOut","example":{"archive_from":"2026-08-04T09:20:44Z","limit":500,"offset":0,"resulted_rows":640,"rows":[{"bookmaker_key":"sportsbet","category":"horse","close_secs_to_jump":41,"close_win_price":7.0,"country":"AU","finish_position":2,"is_closing_line":true,"meeting_date_aet":"2026-08-15","name_fragment_suspect":false,"open_is_baseline":true,"open_secs_to_jump":3595,"open_win_price":6.5,"points_observed":10,"race_id":"race_123","race_number":2,"result_status":"final","runner_key":"aeolian","runner_name":"Aeolian","runner_number":4,"runner_ref":"ra:2098765","start_time":"2026-08-15T03:00:00Z","venue":"Caulfield","venue_id":"caulfield","venue_site":"caulfield","venue_split_suspect":false}],"rows_returned":1,"total_rows":12480}},"ConnectorHealthOut":{"properties":{"connector":{"type":"string","title":"Connector"},"last_ok":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Ok"},"last_poll":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Poll"},"status":{"type":"string","title":"Status"},"records_written":{"type":"integer","title":"Records Written"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["connector","last_ok","status","records_written"],"title":"ConnectorHealthOut","example":{"connector":"sportsbet","last_ok":"2026-06-07T02:20:00","records_written":128,"status":"ok"}},"DogCandidate":{"properties":{"dog_id":{"type":"integer","title":"Dog Id"},"dog_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dog Name"},"runs":{"type":"integer","title":"Runs","description":"Career starts on this feed, scratchings included."},"last_start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Start","description":"Meeting date of the most recent run."}},"type":"object","required":["dog_id","runs"],"title":"DogCandidate"},"GreyhoundFormOut":{"properties":{"dog_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dog Id"},"dog_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dog Name"},"ambiguous":{"type":"boolean","title":"Ambiguous","description":"True when the name matched several dogs. `runs` is then empty and `candidates` lists them."},"candidates":{"items":{"$ref":"#/components/schemas/DogCandidate"},"type":"array","title":"Candidates","default":[]},"runs":{"items":{"$ref":"#/components/schemas/GreyhoundRun"},"type":"array","title":"Runs","default":[]},"source":{"$ref":"#/components/schemas/SourceBlock"}},"type":"object","required":["ambiguous","source"],"title":"GreyhoundFormOut","example":{"ambiguous":false,"candidates":[],"dog_id":760119227,"dog_name":"VELOCITY YIN","runs":[{"box":3,"date":"2026-08-30","distance_m":395,"dog_id":760119227,"dog_name":"VELOCITY YIN","first_split_position":5,"first_split_time":5.98,"grade":"5","grade_in":"5","grade_out":"5","margin":"5.00L","margin_s":0.34,"pir":"5544","position":4,"race_number":4,"rug":3,"scratched":false,"sp":8.4,"time_s":22.65,"topaz_race_id":-2075830351,"track":"Albion Park","trainer":{"id":-41481,"name":"HEATH McADAM"},"weight_kg":29.5,"win_time_s":22.31}],"source":{"coverage_from":"2020-01-01","coverage_to":"2026-08-31","feed":"topaz","region":"AU","sport":"greyhound","synced_at":"2026-09-01T00:32:11+00:00"}}},"GreyhoundRun":{"properties":{"date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date","description":"Meeting date, AEST calendar day, YYYY-MM-DD."},"track":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track","description":"Official Topaz track name, sponsor included (e.g. 'Bet Deluxe Capalaba')."},"distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance M"},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade","description":"Topaz raceTypeCode, e.g. '5', 'M', 'X45'."},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"box":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Box"},"rug":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rug"},"weight_kg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Weight Kg"},"sp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Sp","description":"Official starting price, decimal."},"position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position","description":"Finishing place. NULL when the dog did not complete the course or was scratched."},"scratched":{"type":"boolean","title":"Scratched"},"abnormal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Abnormal","description":"Fell, TailedOff, PulledUp, Disqualified or StayedInBox. NULL for a normal run."},"time_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Time S"},"win_time_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Win Time S","description":"Winning time of this race, i.e. the place-1 dog's time."},"margin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Margin","description":"Margin to the winner in lengths, as Topaz publishes it (e.g. '5.50L')."},"margin_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Margin S","description":"Margin to the winner in seconds. On the winner's own run this is the winning margin over the runner-up."},"first_split_time":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"First Split Time"},"first_split_position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Split Position"},"second_split_time":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Second Split Time","description":"Second sectional split. Only some tracks time two points: 21% of all runs carry one (603,734 of 2,861,690, measured 2026-09-02)."},"second_split_position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Second Split Position"},"grade_in":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade In","description":"The dog's grade entering this race, as Topaz graded it."},"grade_out":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade Out","description":"The dog's grade after this race's result was applied. A value differing from grade_in is a grade change — a win typically moves the dog down a number (5 -> 4)."},"pir":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pir","description":"Position in running, one digit per section."},"trainer":{"$ref":"#/components/schemas/TrainerRef"},"topaz_race_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Topaz Race Id","description":"Topaz raceId. Every runner in the same race shares it."},"dog_id":{"type":"integer","title":"Dog Id"},"dog_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dog Name"}},"type":"object","required":["scratched","trainer","dog_id"],"title":"GreyhoundRun"},"GreyhoundStatGroup":{"properties":{"track":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track"},"distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance M"},"box":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Box"},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade"},"starts":{"type":"integer","title":"Starts"},"wins":{"type":"integer","title":"Wins"},"places":{"type":"integer","title":"Places","description":"Finishes in the first three."},"win_pct":{"type":"number","title":"Win Pct"},"place_pct":{"type":"number","title":"Place Pct"},"avg_time_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Time S"},"best_time_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Time S"},"avg_first_split":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg First Split"}},"type":"object","required":["starts","wins","places","win_pct","place_pct"],"title":"GreyhoundStatGroup"},"GreyhoundStatsOut":{"properties":{"dog_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dog Id"},"dog_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dog Name"},"ambiguous":{"type":"boolean","title":"Ambiguous"},"candidates":{"items":{"$ref":"#/components/schemas/DogCandidate"},"type":"array","title":"Candidates","default":[]},"group_by":{"items":{"type":"string"},"type":"array","title":"Group By","default":[]},"groups":{"items":{"$ref":"#/components/schemas/GreyhoundStatGroup"},"type":"array","title":"Groups","default":[]},"source":{"$ref":"#/components/schemas/SourceBlock"}},"type":"object","required":["ambiguous","source"],"title":"GreyhoundStatsOut","example":{"ambiguous":false,"candidates":[],"dog_id":760119227,"dog_name":"VELOCITY YIN","group_by":["track"],"groups":[{"avg_first_split":7.594,"avg_time_s":20.02,"best_time_s":19.16,"place_pct":28.57,"places":8,"starts":28,"track":"Albion Park","win_pct":10.71,"wins":3}],"source":{"coverage_from":"2020-01-01","coverage_to":"2026-08-31","feed":"topaz","region":"AU","sport":"greyhound","synced_at":"2026-09-01T00:32:11+00:00"}}},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HorseCandidate":{"properties":{"runner_ref":{"type":"string","title":"Runner Ref","description":"Stable ra: horsecode — re-query with this."},"runner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Runner Name"},"last_seen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Seen","description":"Most recent date this ref appeared on our feeds (acceptance or result)."},"source":{"type":"string","title":"Source","description":"Where it was seen: acceptances or results."}},"type":"object","required":["runner_ref","source"],"title":"HorseCandidate"},"HorseCareer":{"properties":{"starts":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Starts"},"wins":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wins"},"seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seconds"},"thirds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thirds"},"prizemoney":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prizemoney","description":"Career prizemoney, whole AUD."},"bonus":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bonus","description":"Total bonus (e.g. BOBS), whole AUD."},"min_dist_win_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Dist Win M"},"max_dist_win_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Dist Win M"},"splits":{"additionalProperties":{"$ref":"#/components/schemas/SplitRecord"},"type":"object","title":"Splits","description":"RA's own career splits, keyed first_up, second_up, firm, good, soft, heavy, synthetic — plus track, distance and track_distance when RA renders them. Each value is starts/wins/seconds/thirds."}},"type":"object","title":"HorseCareer"},"HorseFormOut":{"properties":{"horse_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Horse Name"},"runner_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Runner Ref","description":"Stable ra: horsecode — the same identifier on /v1/racing/results runners and acceptance-enriched live runners."},"ambiguous":{"type":"boolean","title":"Ambiguous","description":"True when the name matched several horses. `runs` is then empty and `candidates` lists them."},"candidates":{"items":{"$ref":"#/components/schemas/HorseCandidate"},"type":"array","title":"Candidates","default":[]},"profile":{"anyOf":[{"$ref":"#/components/schemas/HorseProfile"},{"type":"null"}]},"career":{"anyOf":[{"$ref":"#/components/schemas/HorseCareer"},{"type":"null"}]},"runs":{"items":{"$ref":"#/components/schemas/HorseRun"},"type":"array","title":"Runs","default":[]},"source":{"anyOf":[{"$ref":"#/components/schemas/HorseSource"},{"type":"null"}]}},"type":"object","required":["ambiguous"],"title":"HorseFormOut","example":{"ambiguous":false,"candidates":[],"career":{"prizemoney":4875,"seconds":0,"splits":{"first_up":{"seconds":0,"starts":2,"thirds":1,"wins":0},"good":{"seconds":0,"starts":2,"thirds":1,"wins":0}},"starts":3,"thirds":1,"wins":0},"horse_name":"Zweigelt","profile":{"age_sex":"4yo Bay Gelding","colours":"Lime, Purple And Gold Braces And Armbands","dam":"MEDGRINDA","dob":"2022-11-01","last_gear_change":"20-Aug-2026, Blinkers, First Time","sire":"VINO ROSSO (USA)","trainer":"Matthew Dunn","trainer_location":"Murwillumbah"},"runner_ref":"ra:MTMwNDY2ODI0NA","runs":[{"barrier":9,"date":"2026-08-18","distance_m":1430,"field_size":9,"going":"Soft","going_rating":7,"jockey":"Andrew Mallyon","jockey_code":"MTQ5MjQ5OTIw","margin_l":0.76,"meet_code":"MTI2NzM2MDQwMA","pir":[{"at_m":800,"position":7},{"at_m":400,"position":7}],"placegetters":[{"name":"Sunset Trail","position":1,"runner_ref":"ra:MTMwMDQ4MDk2MA","weight_kg":58.0}],"position":4,"prices":[7.5,4.0,4.8,5.0],"prize_total":27000,"prize_won":1300,"race_class":"MDN","race_number":4,"result_raw":"4th of 9","sectional_distance_m":600,"sectional_time_s":37.16,"sp":5.0,"time_s":86.82,"track":"GRAF","trial":false,"weight_kg":58.5}],"source":{"feed":"racing_australia","fetched_at":"2026-09-01T11:20:00+00:00","region":"AU","sport":"thoroughbred","stale":false}}},"HorseProfile":{"properties":{"age_sex":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Age Sex","description":"As published, e.g. '4yo Bay Gelding'."},"dob":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dob"},"sire":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sire"},"dam":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dam"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"trainer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trainer"},"trainer_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trainer Location"},"owners":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owners"},"colours":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Colours"},"last_gear_change":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Gear Change","description":"As published, date and gear, e.g. '20-Aug-2026, Blinkers, First Time, ...'."}},"type":"object","title":"HorseProfile"},"HorseRun":{"properties":{"date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date"},"track":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track","description":"RA's compressed track code as published ('W FM' is Warwick Farm, 'RAND' Randwick)."},"meet_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meet Code","description":"RA meeting code; every runner in the same meeting shares it."},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"trial":{"type":"boolean","title":"Trial","description":"True for a barrier trial: $0 prize, 0kg weights and no market are how RA publishes them."},"position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position"},"field_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Field Size"},"result_raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result Raw","description":"The position cell as published, kept for rows that don't parse to a number."},"distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance M"},"going":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Going","description":"Condition word: Firm, Good, Soft, Heavy, Synthetic."},"going_rating":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Going Rating","description":"The number after the condition (Soft7 -> 7). Null where RA prints none."},"race_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Class","description":"RA's official class string for that race: 'MDN-SW', 'CTRY MDN', 'BM64', '3Y MDN-TRL'."},"prize_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prize Total"},"prize_won":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prize Won","description":"This horse's cut, when RA shows it."},"jockey":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jockey"},"jockey_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jockey Code","description":"RA's stable jockey code, consistent across every page that names the same rider."},"trainer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trainer","description":"Trainer of the day for THIS run, filled from our own results archive — RA's form table never names the trainer per run. Populated for runs from 2026-09-01 onward (the archive's full-runner floor) and NULL before that; `profile.trainer` is the horse's current trainer, which is usually but not always the same person."},"weight_kg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Weight Kg"},"barrier":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Barrier"},"time_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Time S","description":"Official race time in seconds."},"sectional_distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sectional Distance M","description":"The published closing sectional's distance, usually 600."},"sectional_time_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Sectional Time S"},"margin_l":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Margin L","description":"Margin in lengths. On this horse's own win it is the winning margin over the runner-up."},"pir":{"items":{"$ref":"#/components/schemas/RunPIR"},"type":"array","title":"Pir","description":"Position in running at the published marks (800m/400m)."},"prices":{"items":{"type":"number"},"type":"array","title":"Prices","description":"The published price path, opening quote through SP, oldest first."},"sp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Sp","description":"Starting price — the last entry of `prices`."},"placegetters":{"items":{"$ref":"#/components/schemas/RunPlacegetter"},"type":"array","title":"Placegetters","description":"The two nearest finishers RA lists (2nd and 3rd on this horse's win; the winner and runner-up otherwise), each with its own ra: code."}},"type":"object","required":["trial"],"title":"HorseRun"},"HorseSource":{"properties":{"feed":{"type":"string","title":"Feed","default":"racing_australia"},"region":{"type":"string","title":"Region","default":"AU"},"sport":{"type":"string","title":"Sport","default":"thoroughbred"},"fetched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fetched At","description":"UTC time this horse's page was last read from RA."},"stale":{"type":"boolean","title":"Stale","description":"True when RA could not be reached just now and this is the stored copy served past its six-hour refresh window.","default":false}},"type":"object","title":"HorseSource"},"IngestPayload":{"properties":{"bookmaker_key":{"type":"string","title":"Bookmaker Key"},"races":{"items":{"$ref":"#/components/schemas/RaceIn"},"type":"array","title":"Races"},"observed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Observed At"}},"type":"object","required":["bookmaker_key","races"],"title":"IngestPayload"},"KeyResendIn":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"KeyResendIn"},"LinesArbOut":{"properties":{"event_id":{"type":"string","title":"Event Id"},"sport_key":{"type":"string","title":"Sport Key"},"home_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Home Team"},"away_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Away Team"},"commence_time":{"type":"string","title":"Commence Time"},"market_type":{"type":"string","title":"Market Type"},"opportunities":{"items":{"type":"object"},"type":"array","title":"Opportunities"}},"type":"object","required":["event_id","sport_key","home_team","away_team","commence_time","market_type","opportunities"],"title":"LinesArbOut","example":{"away_team":"Magpies","commence_time":"2026-06-07T09:30:00Z","event_id":"event_123","home_team":"Lions","market_type":"totals","opportunities":[{"arb_pct":1.3,"is_arb":true,"is_middle":true,"optimal_stakes":[],"side_a":{"bookmaker":"sportsbet","name":"Over","point":38.5,"price":1.95},"side_b":{"bookmaker":"tab","name":"Under","point":39.5,"price":1.98}}],"sport_key":"afl"}},"LoginExchangeIn":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"LoginExchangeIn"},"LoginRequestIn":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"LoginRequestIn"},"MarketIn":{"properties":{"market_type":{"type":"string","title":"Market Type","default":"h2h"},"selections":{"items":{"$ref":"#/components/schemas/SelectionIn"},"type":"array","title":"Selections"}},"type":"object","required":["selections"],"title":"MarketIn"},"MoverOut":{"properties":{"venue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue"},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"start_time":{"type":"string","title":"Start Time"},"mins_to_jump":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mins To Jump"},"runner":{"type":"string","title":"Runner"},"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number"},"direction":{"type":"string","title":"Direction"},"open_price":{"type":"number","title":"Open Price"},"current_price":{"type":"number","title":"Current Price"},"move_pct":{"type":"number","title":"Move Pct"},"books_quoting":{"type":"integer","title":"Books Quoting","description":"Books with a price for this runner, including ones we have never observed move."},"books_with_history":{"type":"integer","title":"Books With History","description":"Books with two or more captured prices — the only ones that can evidence a move. The consensus and the `min_books` gate use THIS count, not `books_quoting`."},"books_firming":{"type":"integer","title":"Books Firming"},"books_drifting":{"type":"integer","title":"Books Drifting"},"books_unchanged":{"type":"integer","title":"Books Unchanged","description":"Books whose captured price has not moved, including books seen only once. books_firming + books_drifting + books_unchanged == books_quoting."},"bookmakers":{"items":{"type":"object"},"type":"array","title":"Bookmakers"}},"type":"object","required":["venue","race_number","category","country","start_time","runner","direction","open_price","current_price","move_pct","books_quoting","books_with_history","books_firming","books_drifting","books_unchanged","bookmakers"],"title":"MoverOut","example":{"bookmakers":[{"counted_in_consensus":true,"current_price":9.0,"key":"betright","move_pct":-18.18,"open_price":11.0,"open_secs_to_jump":3597,"points":4}],"books_drifting":0,"books_firming":9,"books_quoting":10,"books_unchanged":1,"books_with_history":9,"category":"horse","country":"AU","current_price":9.17,"direction":"firming","mins_to_jump":3,"move_pct":-18.3,"number":10,"open_price":11.22,"race_number":8,"runner":"Magnaburma","start_time":"2026-08-15T08:52:00Z","venue":"Broome"}},"NextRaceOut":{"properties":{"race_id":{"type":"string","title":"Race Id"},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"venue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue"},"venue_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Id","description":"Stable slug for this venue spelling ('sandown-hillside'). Spelling variants of one venue share one id."},"venue_canonical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Canonical","description":"Canonical display name for venue_id ('Sandown Hillside')."},"venue_site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Site","description":"Physical-complex key: tracks sharing one venue complex share one site ('sandown' covers Sandown, Sandown Hillside, Sandown Lakeside and the Sandown Park greyhound track)."},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"start_time":{"type":"string","title":"Start Time"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"race_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Name"},"distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance M"},"track_condition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition"},"weather":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Weather"},"places_paid":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Places Paid"},"track_condition_changed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition Changed At","description":"UTC instant of the most recent logged track-condition, weather or rail transition for this race's meeting — Racing Australia's re-published rating or a bookmaker's race page moving between two of our polls. Null when nothing has changed since we began watching the meeting. /v1/racing/track-conditions serves the sequence."},"race_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Class","description":"The first term of RA's official conditions line for this race ('BenchMark 64', 'Maiden', 'Group 1'). AU thoroughbred only; null elsewhere."},"conditions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conditions","description":"RA's official conditions line, verbatim — class, race type, minimum weight, age/sex restrictions, claiming allowances."},"prize_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prize Total","description":"Total advertised prize for the race, whole AUD."},"rail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rail","description":"Rail position for the meeting, as RA publishes it ('+3m Entire'). AU thoroughbred only."},"track_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Name","description":"Racing Australia's own name for the course this race runs on, verbatim: 'Hillside' at Sandown, 'Randwick' vs the Kensington inner course, 'Main' elsewhere. Combine with venue_site to tell dual-course meetings apart. AU thoroughbred only; null elsewhere."},"market_closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Closed At","description":"UTC time at which the market was FIRST seen shut — the book stopped taking bets on this race. Poll-observed: racing polls every 8s inside 120s of a jump, 15s inside 10 minutes and 20s otherwise, so this is the first poll that saw the state, not the exact instant; it lands within about one poll interval after it. Set once: a market that flickers SUSPENDED -> OPEN -> SUSPENDED keeps the FIRST suspension. TWO SOURCES report market state. PointsBet closes its book at the real jump — measured 2026-09-03 over 9 live races followed across their jump, 0.2 to 1.4 minutes AFTER the advertised start on AU/NZ cards — and the Betfair Exchange reports it while it quotes the market, which is until about two minutes before the jump. `market_state_source` names the one that produced this value. Exchange-sourced stamps are withheld from customer plans (operator ruling 2026-09-01): they republish licence-held market state, so a race seen shut only by the exchange reads null for customers and carries the value for internal callers. Bookmaker-sourced stamps are served to everybody. Null also means no feed observed a transition — it never means the market stayed open."},"inplay_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inplay At","description":"UTC time at which the race was FIRST seen in-play — the actual off, as opposed to `start_time`, which is the ADVERTISED off and moves. Same poll-observed, set-once semantics as `market_closed_at`, and withheld on the same source rule. Reported by the Betfair Exchange only: bookmakers do not run racing in-play, so a bookmaker feed says when it CLOSED its book, not when the race went. For a race the exchange has stopped quoting, `market_closed_at` from a bookmaker is the closest measured instant to the off."},"market_state_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market State Source","description":"Which feed's market state produced `market_closed_at` / `inplay_at` on this race — 'pointsbetau' for the bookmaker, 'betfair_ex_au' for the exchange. Set once, alongside the first stamp. Null when no transition has been observed, and null for customer plans whenever the stamps themselves are withheld, so a non-null value here is always the provenance of values you can see."},"scratchings":{"items":{"type":"object"},"type":"array","title":"Scratchings","default":[]},"runners":{"items":{"type":"object"},"type":"array","title":"Runners"},"data_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Data Age Seconds","description":"Age in seconds of the OLDEST bookmaker quote in this race — the staleness you are exposed to if you act on any price here. Since 2026-08-15 this is the worst leg, not the freshest; `freshest_age_seconds` carries the best-case figure."},"freshest_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Freshest Age Seconds","description":"Age in seconds of the freshest bookmaker quote in this race."},"refresh_tier":{"type":"string","title":"Refresh Tier","description":"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.","default":"live"},"stale":{"type":"boolean","title":"Stale","description":"True when the oldest contributing bookmaker is older than this race's `refresh_tier` threshold — 120s on `live`, 1800s on `card`. It does NOT mean the whole race is dead — read `stale_bookmakers` and each bookmaker's own `stale`/`age_seconds`.","default":false},"stale_bookmakers":{"items":{"type":"string"},"type":"array","title":"Stale Bookmakers","description":"Bookmaker keys in this race whose quote is older than the `refresh_tier` threshold (120s live, 1800s card)."},"cached":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cached"},"cache_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Age Seconds"}},"type":"object","required":["race_id","source_id","venue","race_number","category","start_time","country","runners"],"title":"NextRaceOut","example":{"category":"horse","country":"AU","data_age_seconds":7,"race_id":"race_123","race_number":4,"refresh_tier":"live","runners":[{"bookmakers":[{"age_seconds":7,"key":"sportsbet","refresh_tier":"live","source_url":"https://www.sportsbet.com.au/horse-racing/australia-nz/eagle-farm/race-4-10861234","stale":false,"win_price":3.2},{"age_seconds":9,"betslip_app_only":false,"betslip_place_url":"https://www.unibet.com.au/racing?competitorKey=202609030600.T.AUS.mackay.4.emilia_jane&priceCode=FXD&marketName=Place&priceBetType=FixedPlace#/event/202609030600.T.AUS.mackay.4/Place/FixedPlace","betslip_win_url":"https://www.unibet.com.au/racing?competitorKey=202609030600.T.AUS.mackay.4.emilia_jane&priceCode=FXD&marketName=Win&priceBetType=FixedWin#/event/202609030600.T.AUS.mackay.4/Win/FixedWin","key":"unibet","place_price":1.55,"refresh_tier":"live","source_url":"https://www.unibet.com.au/racing#/event/202609030600.T.AUS.mackay.4","stale":false,"win_price":3.3},{"age_seconds":11,"betslip_app_only":true,"betslip_place_url":"https://www.ladbrokes.com.au/racing/casino/7d3bfac5-f5b2-4ad1-82dc-e00ae2220a36/1a51322a-65bf-4f96-88b2-c294faf9199c/fp","betslip_win_url":"https://www.ladbrokes.com.au/racing/casino/7d3bfac5-f5b2-4ad1-82dc-e00ae2220a36/1a51322a-65bf-4f96-88b2-c294faf9199c/fw","key":"ladbrokes_au","place_price":1.5,"refresh_tier":"live","source_url":"https://www.ladbrokes.com.au/racing/casino/7d3bfac5-f5b2-4ad1-82dc-e00ae2220a36","stale":false,"top2_price":1.9,"top3_price":1.5,"top4_price":1.3,"win_price":3.1}],"name":"Example Runner","number":1}],"source_id":"src_a1b2c3d4e5","stale":false,"start_time":"2026-06-07T04:10:00Z","venue":"Eagle Farm"}},"PremiershipOut":{"properties":{"role":{"type":"string","title":"Role"},"season_start_year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Season Start Year"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State","description":"Echo of the filter; null when a name search crossed all states."},"scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"rows":{"items":{"$ref":"#/components/schemas/PremiershipRow"},"type":"array","title":"Rows","default":[]},"source":{"$ref":"#/components/schemas/PremiershipSource"}},"type":"object","required":["role","source"],"title":"PremiershipOut","example":{"role":"trainer","rows":[{"fifths":14.0,"firsts":17.0,"fourths":22.0,"name":"Chris Waller","period_from":"2026-08-01","period_to":"2026-09-02","prize_money":2437350.0,"ra_code":"Njg0MjM2MA","rank":1,"scope":"all","seconds":23.5,"starts":147,"state":"NSW","strike_rate":11.6,"thirds":20.0}],"scope":"all","season_start_year":2026,"source":{"feed":"racing-australia-premierships","region":"AU","scraped_at":"2026-09-02T07:10:00+00:00","sport":"thoroughbred","top_n":20},"state":"NSW"}},"PremiershipRow":{"properties":{"state":{"type":"string","title":"State","description":"AUS is the national table — RA's own combined leaderboard, not a sum of the state rows."},"scope":{"type":"string","title":"Scope","description":"RA's split: all, metro, provincial, country or picnic."},"rank":{"type":"integer","title":"Rank","description":"Position in RA's table, 1-20."},"name":{"type":"string","title":"Name"},"ra_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ra Code","description":"RA's stable code from the row's engagement link — the same namespace as trainer_code on acceptance data and jockey_code on /v1/racing/horses/form runs, so rows join on identity, not name spelling."},"firsts":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Firsts","description":"Wins. REAL, not int: dead heats appear as halves in some tables (RA prints 23½)."},"seconds":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Seconds"},"thirds":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Thirds"},"fourths":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Fourths"},"fifths":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Fifths"},"prize_money":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Prize Money","description":"Season prize money, AUD."},"strike_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Strike Rate","description":"RA's own figure, percent."},"starts":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Starts"},"period_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period From","description":"Start of the window this row measures (RA seasons run 1 Aug to 31 Jul)."},"period_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period To","description":"End of the measured window. On the running season this is the day RA rendered the table, which makes it the per-row freshness stamp: rows from a table scraped two nights ago carry that day, not today."}},"type":"object","required":["state","scope","rank","name"],"title":"PremiershipRow"},"PremiershipSource":{"properties":{"feed":{"type":"string","title":"Feed","default":"racing-australia-premierships"},"region":{"type":"string","title":"Region","default":"AU"},"sport":{"type":"string","title":"Sport","default":"thoroughbred"},"scraped_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scraped At","description":"UTC time of the newest row served in this response. For the usual single-table lookup (a `state` + `scope`) every row comes from one scrape, so this IS that table's age. A `name` search spans tables scraped on different nights, so there read it as the freshest of them and use `period_to` for the age of each individual row."},"top_n":{"type":"integer","title":"Top N","description":"RA publishes each premiership as a top-20 table. Absence from `rows` means outside the top 20, never inactivity.","default":20}},"type":"object","title":"PremiershipSource"},"PriceHistoryOut":{"properties":{"race_id":{"type":"string","title":"Race Id"},"venue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue"},"venue_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Id"},"venue_canonical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Canonical"},"venue_site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Site"},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"start_time":{"type":"string","title":"Start Time"},"points_returned":{"type":"integer","title":"Points Returned"},"truncated":{"type":"boolean","title":"Truncated"},"history_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"History From","description":"Start time of the earliest race this endpoint can answer for. Since 2026-09-01 races older than the ~45-day live window are served from the permanent archive, so this floor is fixed at capture start (2026-08-04) and history only deepens."},"runners":{"items":{"type":"object"},"type":"array","title":"Runners"}},"type":"object","required":["race_id","venue","race_number","category","country","start_time","points_returned","truncated","runners"],"title":"PriceHistoryOut","example":{"category":"horse","country":"AU","history_from":"2026-08-04T09:22:00Z","points_returned":2481,"race_id":"race_123","race_number":2,"runners":[{"bookmakers":[{"close_price":7.0,"close_secs_to_jump":41,"high":7.0,"key":"sportsbet","low":6.0,"move_pct":7.69,"open_is_baseline":true,"open_price":6.5,"open_secs_to_jump":3595,"points":[{"captured_at":"2026-08-15T02:00:04Z","secs_to_jump":3595,"win_price":6.5}],"points_count":10}],"name":"Aeolian","number":4}],"start_time":"2026-08-15T03:00:00Z","truncated":false,"venue":"Caulfield"}},"PricePathsOut":{"properties":{"rows_returned":{"type":"integer","title":"Rows Returned"},"total_points":{"type":"integer","title":"Total Points","description":"Points matching the filter before limit/offset"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"archive_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Archive From"},"rows":{"items":{"type":"object"},"type":"array","title":"Rows"}},"type":"object","required":["rows_returned","total_points","limit","offset","rows"],"title":"PricePathsOut"},"RaceEventOut":{"properties":{"race_id":{"type":"string","title":"Race Id"},"venue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue"},"venue_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Id"},"venue_canonical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Canonical"},"venue_site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Site"},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"start_time":{"type":"string","title":"Start Time"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"race_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Name"},"distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance M"},"track_condition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition"},"weather":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Weather"},"race_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Class","description":"The first term of RA's official conditions line ('BenchMark 64', 'Maiden', 'Group 1'). AU thoroughbred only."},"conditions":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Conditions","description":"RA's official conditions line, verbatim."},"prize_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prize Total","description":"Total advertised prize, whole AUD."},"rail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rail","description":"Rail position for the meeting, as RA publishes it."},"track_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Name","description":"RA's own name for the course this race runs on ('Hillside', 'Randwick', 'Main'). AU thoroughbred only; null elsewhere."},"track_condition_changed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition Changed At","description":"UTC instant of the most recent logged track-condition, weather or rail transition for this race's meeting; null when nothing has changed since we began watching it. The sequence is on /v1/racing/track-conditions."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Set when the race row is created and revised exactly once, if at all: to 'abandoned' when Racing Australia posts an official abandoned, postponed or transferred notice for the meeting (2026-09-06). Otherwise it reads 'open' for every race in this window. It is not a live market-state signal - /v1/racing/results carries the settled status (interim, final, abandoned)."},"scratchings":{"items":{"type":"object"},"type":"array","title":"Scratchings","description":"Scratched runners for this race: {name, number, barrier, scratched_at, late, emergency}. `scratched_at` (UTC) is the book's own stamp where it sends one (Ladbrokes, Neds, BetRight) and Racing Australia's official scratching sheet otherwise (AU thoroughbreds, read two-hourly); absent when neither knows. `late` marks a late scratching, `emergency` an emergency that did not gain a start. Merged across every book that reports one. Greyhound cards also carry box-vacancy placeholders here (a literal '....' or 'Vacant Box'), which are the reporting book naming an empty box rather than a withdrawn runner; match on `number` and treat a name from that set as a vacant box."},"market_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Status","description":"Betting-market state (OPEN, SUSPENDED, CLOSED). Reported by PointsBet, which sends CLOSED when it shuts its book at the jump, and by the Betfair Exchange, which is withheld from customer responses pending a data licence — so a value last written by the exchange reads null for customers and carries through for internal callers. Most races are quoted by both: the exchange holds the state until about two minutes before the jump, PointsBet reports the close."},"inplay":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Inplay","description":"True once the market is in-play. Exchange-only — bookmakers do not run racing in-play — so it carries the same licence hold as `market_status` and reads null for customers."},"market_closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Closed At","description":"UTC time at which the market was FIRST seen shut — the book stopped taking bets on this race. Poll-observed: racing polls every 8s inside 120s of a jump, 15s inside 10 minutes and 20s otherwise, so this is the first poll that saw the state, not the exact instant; it lands within about one poll interval after it. Set once: a market that flickers SUSPENDED -> OPEN -> SUSPENDED keeps the FIRST suspension. TWO SOURCES report market state. PointsBet closes its book at the real jump — measured 2026-09-03 over 9 live races followed across their jump, 0.2 to 1.4 minutes AFTER the advertised start on AU/NZ cards — and the Betfair Exchange reports it while it quotes the market, which is until about two minutes before the jump. `market_state_source` names the one that produced this value. Exchange-sourced stamps are withheld from customer plans (operator ruling 2026-09-01): they republish licence-held market state, so a race seen shut only by the exchange reads null for customers and carries the value for internal callers. Bookmaker-sourced stamps are served to everybody. Null also means no feed observed a transition — it never means the market stayed open."},"inplay_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inplay At","description":"UTC time at which the race was FIRST seen in-play — the actual off, as opposed to `start_time`, which is the ADVERTISED off and moves. Same poll-observed, set-once semantics as `market_closed_at`, and withheld on the same source rule. Reported by the Betfair Exchange only: bookmakers do not run racing in-play, so a bookmaker feed says when it CLOSED its book, not when the race went. For a race the exchange has stopped quoting, `market_closed_at` from a bookmaker is the closest measured instant to the off."},"market_state_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market State Source","description":"Which feed's market state produced `market_closed_at` / `inplay_at` on this race — 'pointsbetau' for the bookmaker, 'betfair_ex_au' for the exchange. Set once, alongside the first stamp. Null when no transition has been observed, and null for customer plans whenever the stamps themselves are withheld, so a non-null value here is always the provenance of values you can see."}},"type":"object","required":["race_id","venue","race_number","category","start_time","country"],"title":"RaceEventOut","example":{"category":"horse","country":"AU","market_closed_at":"2026-06-07T04:11:12Z","market_state_source":"pointsbetau","market_status":"CLOSED","race_id":"race_123","race_number":4,"scratchings":[{"barrier":"9","name":"Soul Of Spain","number":9}],"start_time":"2026-06-07T04:10:00Z","status":"open","venue":"Eagle Farm"}},"RaceIn":{"properties":{"source_id":{"type":"string","title":"Source Id"},"venue":{"type":"string","title":"Venue"},"race_number":{"type":"integer","title":"Race Number"},"category":{"type":"string","title":"Category"},"start_time":{"type":"string","title":"Start Time"},"country":{"type":"string","title":"Country","default":"AU"},"runners":{"items":{"$ref":"#/components/schemas/RunnerIn"},"type":"array","title":"Runners"},"observed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Observed At"}},"type":"object","required":["source_id","venue","race_number","category","start_time","runners"],"title":"RaceIn"},"RaceResultOut":{"properties":{"race_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Id","description":"Race id, joinable to /v1/racing/price-history and (while the race is still open) /v1/racing/next-to-go. Null if the race cannot be resolved in the price-history store."},"coverage_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coverage From","description":"Start time of the earliest result currently retained."},"venue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue"},"venue_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Id"},"venue_canonical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Canonical"},"venue_site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Site"},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"start_time":{"type":"string","title":"Start Time"},"race_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Name"},"distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance M"},"track_condition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition"},"going":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Going","description":"Track condition word, normalised: Good, Soft, Heavy, Firm, Fast, Synthetic. Parsed from `track_condition` with the same helper /v1/racing/horses/form uses, so the two surfaces agree. Null when `track_condition` is null or unparseable; `track_condition` remains the raw string exactly as the source published it."},"going_rating":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Going Rating","description":"Numeric going rating 1-12 (the 4 in 'Good (4)'), or null. NULL IS THE MAJORITY CASE, not an exception for two rare words: most stored conditions are a bare word carrying no number at all. Measured 2026-09-10 across all 5,579 stored results, a bare 'Good' is 3,954 rows (71%) and yields null, so a rating is present on roughly 29% of rows — greyhound and other non-thoroughbred meetings publish an unrated word as a rule. A rating is never guessed from the word ('Synthetic', 67 rows, and 'Firm', 0 rows, never carry one). Separately, 205 rows store the four-character STRING 'None' in `track_condition`: `going` and `going_rating` are both null on those, while `track_condition` still serves that literal string, because `track_condition` is the raw source value passed through untouched."},"weather":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Weather"},"status":{"type":"string","title":"Status","description":"interim | final | abandoned. `interim` until the placings have been stable for 30 minutes, then `final`. `abandoned` (2026-09-06) is a race that did NOT run: Racing Australia's official Abandoned & Transferred Meetings notice for that venue and date, applied to every race of the meeting — placings are empty and runners null. Settle only against `final`; treat `abandoned` as a void for settlement purposes."},"status_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Note","description":"Only on `abandoned` rows: RA's own word for the non-run — \"abandoned\", \"postponed\", or \"transferred to <venue>\". A postponed or transferred meeting did not run at THIS venue on THIS date; if it ran elsewhere or later it appears as its own result."},"placings":{"items":{"type":"object"},"type":"array","title":"Placings","description":"The placegetters: {position, number, name, win_price, place_price, sp, sp_note}. `win_price` and `place_price` are the SETTLING BOOKMAKER'S FIXED prices. `sp` is the OFFICIAL STARTING PRICE as a decimal return on a $1 stake (4.6 means $4.60), null where it is unknown — never 0. It is the settling market's own price, not our consensus and not a bookmaker's fixed price: greyhound SP comes from the official Topaz feed, thoroughbred SP from the Racing Australia results table, and HARNESS SP IS ALWAYS NULL — no source we hold publishes it. `sp_note` carries Racing Australia's favouritism marker on thoroughbred runners ('F' favourite, 'EF' equal favourite) and is null everywhere else. Coverage RE-MEASURED 2026-09-08 over the whole retained store, counting only rows whose `status` is `ran` (a barrier scratching is not a starter and its price is not in this denominator): 99.8% of greyhound starters carry `sp` (14,540 of 14,568) and 88.6% of thoroughbred starters do (2,105 of 2,375). The Racing Australia block that held thoroughbred SP at 0% when this was last measured on 2026-09-03 has lifted and the collector has been landing meetings since. A null `sp` is unknown, never zero — RA prints no SP at all on some rows. On placings, `sp` is copied from the matching `runners` entry when the response is built, so the two can never disagree. On a race with no `runners` payload the `sp` and `sp_note` KEYS ARE ABSENT from each placing — not present and null. The copy step is what creates them and it is skipped when there is nothing to copy from, so a schema-validating consumer sees a different object shape, not a null: read them with `.get()`. The keys are deliberately NOT normalised in, because adding them here would change the shipped payload of every COMPLETE race too, and nothing may move under a customer mid-integration. `field_status` tells you which case you are in before you look."},"runners":{"anyOf":[{"items":{"type":"object"},"type":"array"},{"type":"null"}],"title":"Runners","description":"One entry per runner: {number, box, name, position, status, abnormal, margin, time_s, dead_heat}. Presence in this list is NOT proof the runner started. `status` is what says so, and `status` is what you settle on. ⚠️ COVERAGE. Read `field_status` on the row FIRST — it is the per-race answer, and it is what separates a field that has not been filled yet from one that never will be. Two different facts live here and they used to be told as one. (1) WHETHER THERE IS A FIELD AT ALL, and mostly there is not. Measured 2026-09-10 across the whole retained store — 5,577 results, 2,902 of them serving `runners: null`: AU greyhound carries a field on 2,360 of 3,219 races, AU thoroughbred on 315 of 1,104, and AU HARNESS, NZ THOROUGHBRED AND NZ HARNESS CARRY NONE AT ALL — 1,254 races in codes no collector has ever existed for, every one of them `field_status: unsupported`. Inside the two supported codes it is not a clean ramp either: each is filled by a scheduled pass, and where that pass cannot match a race with certainty it writes nothing rather than another race's field, so past the fill window 104 of 2,201 AU greyhound races (4.7%) and 44 of 288 AU thoroughbred races (15.3%) keep `runners: null` for good — `field_status: unavailable`, and polling them will not help. `runners` is JSON null in every one of these cases and has never once been an empty array (0 empty arrays in the whole store, measured the same day), so `runners is None` is a safe test and an empty field is not a shape you have to handle. (2) THE SHAPE OF A FIELD THAT IS PRESENT, which is where the 2026-09-08 date belongs. An AU thoroughbred field WRITTEN from 2026-09-08 carries every starter plus every runner Racing Australia printed as scratched; one written before that date carries STARTERS ONLY, because the collector dropped RA's scratched rows and a stored field is never rewritten. Measured 2026-09-08, all 244 thoroughbred fields stored at that point were the old shape and 198 of them publish a `deductions` row for a horse absent from `runners`. So on an older race `deductions` is the list of scratchings and `runners` is the starters; do not read a short `runners` array as a small field. Greyhound fields have carried the whole field since greyhound collection began on the 2026-08-21 Sydney card — but 859 AU greyhound races carry no field at all, which is question (1) and not this one. `position` is the finishing position for every runner that completed the race — not just the placegetters — and is null whenever there is none: the runner did not start, or it started and did not finish. `status` is ran | scratched | late_scratching | reserve (an emergency that never got a start). `abnormal` is the official reason cell, passed through VERBATIM from the source — on AU thoroughbred it is Racing Australia's own Finish cell whenever that cell is not a number. THERE IS NO CLOSED LIST OF CODES and you should not write a match statement that assumes one: the source invents them, and a code nobody here has seen before will reach you unchanged rather than be dropped or renamed. Everything stored to date, with counts as at 2026-09-14: Fell (101), TailedOff (32), SB (25), PulledUp (8), FF (1, Rosehill R4 on 2026-09-12), LP (1). Treat `abnormal` as the authoritative explanation for a missing `position`, and treat an unfamiliar value as a did-not-finish unless `status` says otherwise — which is exactly what this API does: only codes PROVEN to mean the runner never left the barrier flip `status` to `scratched`, and that list currently holds one entry, 'SB'. Anything else stays `ran`, deliberately, because misreading a starter as a non-starter turns a losing bet into a refund. It carries TWO families that settle in OPPOSITE directions, so read it with `status` and never alone. (1) A DID-NOT-FINISH reason on a row whose `status` is `ran` — Fell, PulledUp, Disqualified, TailedOff, StayedInBox, FF and LP are the ones seen so far: the runner started and simply has no finishing position, and a bet on it lost. (2) A DID-NOT-START reason on a row whose `status` is `scratched` — 'SB', Racing Australia's code for scratched at the barrier: the runner was taken out of the market before the jump, so `position` is null, it is in no `placings` entry, and bets on it were refunded while the rest of the race was deducted. An 'SB' row may still carry `sp` — the last price Racing Australia printed for it, kept rather than blanked because it is a real market fact about the withdrawn runner. It is NOT the basis of the deduction, and you must never derive one from the other: `sp` comes from RA's results table while a deduction is struck by the settling bookmaker, and they do not agree. Measured 2026-09-08 across all five stored 'SB' rows that carry a price, 1/`sp` equals the published win deduction on NONE of them — Varejao, Sunshine Coast R4 2026-09-06, saddlecloth 5, withdrawn about 2 minutes 20 before the jump: `sp` 3.70, so 1/3.70 is 0.27 in the dollar, against a published `deductions` row of 0.18 win and 0.15 place; Dubai Dancer, Warrnambool R5: 1/5.50 is 0.18 against a published 0.10. Settle from `deductions`, which is authoritative. None of the five carries a `position` or a `margin`. Telling a barrier scratching from one made days earlier is CODE-SPECIFIC, and the two collectors do not spell it the same way — do not write one rule across categories. On AU thoroughbred (Racing Australia) both are `status: scratched`, and `abnormal` is the discriminator ONLY in one direction: a barrier scratching carries 'SB'; every other scratching carries `abnormal` null. Both carry their saddlecloth `number`, both carry `box` null (the runner has no barrier it jumped from — RA prints 0 there and we do not pass that sentinel on), and an ordinary scratching carries `sp` null while an 'SB' may carry one. So `abnormal == 'SB'` identifies a barrier scratching; `abnormal` null tells you only that it was not one, not how early it happened — use `deductions[].scratched_at` for that. On greyhound (Topaz) `abnormal` is null on every scratching and `status` is the discriminator: the LATE one is `late_scratching`, an earlier one is `scratched`. So today `late_scratching` is a greyhound-only value — a thoroughbred barrier scratching does NOT use it — and `scratched` does not carry identical meaning in the two codes. `deductions` carries `scratched_at` for either, and is the one cross-code way to see how close to the jump a runner came out. `dead_heat` is true on each runner sharing a position. `runner_ref` is a STABLE runner identifier from the official registry (namespaced: ra:<code> thoroughbred, grv:<id> greyhound) — it persists across meetings, so it is the join key for longitudinal work; null while a code's registry source is not yet wired. `trainer` and `jockey` come from the same official results source and may be null where that source is not yet live for the code. `sp` is the OFFICIAL STARTING PRICE as a decimal return on a $1 stake (4.6 means $4.60), null where it is unknown — never 0. It is the settling market's own price, not our consensus and not a bookmaker's fixed price: greyhound SP comes from the official Topaz feed, thoroughbred SP from the Racing Australia results table, and HARNESS SP IS ALWAYS NULL — no source we hold publishes it. `sp_note` carries Racing Australia's favouritism marker on thoroughbred runners ('F' favourite, 'EF' equal favourite) and is null everywhere else. Coverage RE-MEASURED 2026-09-08 over the whole retained store, counting only rows whose `status` is `ran` (a barrier scratching is not a starter and its price is not in this denominator): 99.8% of greyhound starters carry `sp` (14,540 of 14,568) and 88.6% of thoroughbred starters do (2,105 of 2,375). The Racing Australia block that held thoroughbred SP at 0% when this was last measured on 2026-09-03 has lifted and the collector has been landing meetings since. A null `sp` is unknown, never zero — RA prints no SP at all on some rows. Null `runners` means the full field is not available for this race — never an empty field; population is rolling out per racing code as our own results collection comes online. `placings` remains the source for the settling bookmaker's own fixed prices, and now also carries `sp` copied from here."},"field_status":{"type":"string","title":"Field Status","description":"Why `runners` is or is not there, per race — the field a caller was missing when `runners: null` was the only signal and it collapsed several different futures into one. SIX values. `complete`: `runners` is present. It is set whenever a field is stored, including on an abandoned race that unexpectedly carries one, so `field_status == 'complete'` is a safe gate before parsing. `pending`: a collector exists for this code and the pass that NORMALLY fills this race has not been yet; `field_note` names the UTC instant by which it will have run. `overdue`: that pass has been and gone without writing a field, but the collector still selects this row as a candidate and keeps re-attempting it — this state is NOT terminal. For AU greyhound the Topaz enricher re-reads every field-less row on each daily run for ten days after the race (its own LOOKBACK_DAYS); for AU thoroughbred a bounded re-fetch reaches six days but is run by hand rather than on a schedule. Most rows that reach `overdue` stay null, so do not build on a field arriving — but do not write the race off either, and `field_note` gives the horizon. `unavailable`: no scheduled pass still selects this row. Its collector's own candidate window closed at the instant in `field_note` and no field was written — the pass either never matched the race or refused an ambiguous match, which it does rather than write another race's field. `unsupported`: no collector has ever existed for this category/country — AU harness, NZ thoroughbred and NZ harness, 1,254 of the 5,577 stored results on 2026-09-10, plus any other country that appears because an AU book listed the meeting — and no amount of waiting will produce one. `not_applicable`: the race did not run (abandoned, postponed or transferred) and carries no field, so there is nothing to collect. TRANSITIONS, stated explicitly because acting on the wrong one costs you data. The scheduled progression is pending -> overdue -> unavailable, one direction only. It is bounded at every step by a collector's own window rather than by hope: a race can sit matched-and-never-resulted forever (The Gardens, 2026-09-05, 12 races), and an open-ended `pending` would have you polling it forever. Any of those three becomes `complete` the moment a field is written, AND THAT INCLUDES `unavailable` — nothing un-writes a `runners` array, so a collector re-run by hand, a widened window or a late upstream publication can still fill a row we have stopped scheduling passes for. So `unavailable` means 'nothing further is scheduled', never 'this can never arrive'; if you reconcile, re-read rather than caching it as final. `unsupported` and `not_applicable` are properties of the racing code and of the race itself, not of a schedule, and do not change on their own.","default":"unsupported"},"field_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Field Source","description":"Which collector owns `runners` for this race: `racing_australia` (AU thoroughbred), `topaz` (AU greyhound), or null where no collector exists. Set on `pending`, `overdue`, `unavailable` and `complete` rows alike — it names who WOULD fill the field, not who did."},"field_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Field Note","description":"Prose for the non-`complete` states: which pass fills this code, when its normal pass for this race has been and gone, how long the collector keeps re-attempting the row, or that the code has no source at all. Null on `complete`. Written for a human reading a response — branch on `field_status`, never on this text."},"deductions":{"items":{"type":"object"},"type":"array","title":"Deductions","description":"Scratched runners and the deduction applied to bets already struck. `win` and `place` are fractions of the dollar in the range 0-1 (0.34 = 34 cents in the dollar), as published by the settling bookmaker."},"dividends":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Dividends","description":"Dividends declared for the race. `straight` holds one row per placegetter per product: {position, number, name, market ('WIN' or 'PLC'), product, amount}. `exotics` holds one row per exotic product: {type, code ('QN', 'EX', 'TF', 'FF'), selection, product, amount}, where `selection` is the winning saddlecloth numbers in finishing order, e.g. '5,1'. Every `amount` is the return on a $1 stake, the same convention as the odds fields — a 'FIXED' straight dividend equals that runner's own settled fixed price. `product` names the settling pool: 'FIXED' is the bookmaker's own price, the rest are tote products (SP, MIDDIV, VIC, RD+, BT+SP). A product that declared no dividend is omitted rather than returned as 0. Exotic dividends are only declared once the race fully settles, a few minutes after the placings, so a just-resulted race can carry `straight` while `exotics` is still empty — it fills in on a later poll. Quaddie and daily-double dividends span several races and are NOT reported here. `straight_types` is the book's raw list of product codes, retained for backwards compatibility."},"resulted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resulted At"},"market_closed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Closed At","description":"UTC time at which the market was FIRST seen shut — the book stopped taking bets on this race. Poll-observed: racing polls every 8s inside 120s of a jump, 15s inside 10 minutes and 20s otherwise, so this is the first poll that saw the state, not the exact instant; it lands within about one poll interval after it. Set once: a market that flickers SUSPENDED -> OPEN -> SUSPENDED keeps the FIRST suspension. TWO SOURCES report market state. PointsBet closes its book at the real jump — measured 2026-09-03 over 9 live races followed across their jump, 0.2 to 1.4 minutes AFTER the advertised start on AU/NZ cards — and the Betfair Exchange reports it while it quotes the market, which is until about two minutes before the jump. `market_state_source` names the one that produced this value. Exchange-sourced stamps are withheld from customer plans (operator ruling 2026-09-01): they republish licence-held market state, so a race seen shut only by the exchange reads null for customers and carries the value for internal callers. Bookmaker-sourced stamps are served to everybody. Null also means no feed observed a transition — it never means the market stayed open. Copied from the live race row when the result is written; that row is deleted 10 minutes after the jump and results land a median 4.9 minutes after the start, so a late result keeps null."},"inplay_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inplay At","description":"UTC time at which the race was FIRST seen in-play — the actual off, as opposed to `start_time`, which is the ADVERTISED off and moves. Same poll-observed, set-once semantics as `market_closed_at`, and withheld on the same source rule. Reported by the Betfair Exchange only: bookmakers do not run racing in-play, so a bookmaker feed says when it CLOSED its book, not when the race went. For a race the exchange has stopped quoting, `market_closed_at` from a bookmaker is the closest measured instant to the off. Copied from the live race row when the result is written; that row is deleted 10 minutes after the jump and results land a median 4.9 minutes after the start, so a late result keeps null."},"market_state_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market State Source","description":"Which feed's market state produced `market_closed_at` / `inplay_at` on this race — 'pointsbetau' for the bookmaker, 'betfair_ex_au' for the exchange. Set once, alongside the first stamp. Null when no transition has been observed, and null for customer plans whenever the stamps themselves are withheld, so a non-null value here is always the provenance of values you can see."}},"type":"object","required":["venue","race_number","category","country","start_time","status"],"title":"RaceResultOut","example":{"category":"horse","country":"AU","coverage_from":"2026-08-15T05:53:00Z","deductions":[{"name":"Acapulco Girl","number":5,"place":0.0,"scratched_at":"2026-08-14T07:06:29Z","win":0.01}],"distance_m":1200,"dividends":{"exotics":[{"amount":14.4,"code":"QN","product":"VIC","selection":"6,3","type":"Quinella"},{"amount":77.7,"code":"TF","product":"VIC","selection":"6,3,9","type":"Trifecta"}],"straight":[{"amount":4.6,"market":"WIN","name":"Grinzinger Champ","number":6,"position":1,"product":"FIXED"},{"amount":5.0,"market":"WIN","name":"Grinzinger Champ","number":6,"position":1,"product":"MIDDIV"},{"amount":1.8,"market":"PLC","name":"Grinzinger Champ","number":6,"position":1,"product":"FIXED"}],"straight_types":["FIXED","MIDDIV"]},"field_source":"racing_australia","field_status":"complete","going":"Good","going_rating":4,"market_closed_at":"2026-08-15T05:54:31Z","market_state_source":"pointsbetau","placings":[{"name":"Grinzinger Champ","number":6,"place_price":1.8,"position":1,"sp":4.4,"sp_note":"F","win_price":4.6}],"race_id":"race_123","race_name":"TAB ROSEBUD","race_number":8,"resulted_at":"2026-08-15T06:01:12Z","runners":[{"box":3,"dead_heat":false,"jockey":"R Maloney","name":"Grinzinger Champ","number":6,"position":1,"runner_ref":"ra:AB12CD34","sp":4.4,"sp_note":"F","status":"ran","trainer":"T Gollan"},{"dead_heat":false,"name":"Acapulco Girl","number":5,"status":"scratched"}],"start_time":"2026-08-15T05:53:00Z","status":"final","track_condition":"Good (4)","venue":"Doomben","weather":"Light Rain"}},"RacingBestOddsOut":{"properties":{"race_id":{"type":"string","title":"Race Id"},"venue":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue"},"race_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Race Number"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"start_time":{"type":"string","title":"Start Time"},"race_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race Name"},"track_condition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Track Condition"},"market_percentage":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Market Percentage"},"books_compared":{"type":"integer","title":"Books Compared"},"scratchings":{"items":{"type":"object"},"type":"array","title":"Scratchings","default":[]},"data_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Data Age Seconds","description":"Age in seconds of the OLDEST bookmaker quote behind this race."},"freshest_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Freshest Age Seconds"},"refresh_tier":{"type":"string","title":"Refresh Tier","description":"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.","default":"live"},"stale":{"type":"boolean","title":"Stale","default":false},"stale_bookmakers":{"items":{"type":"string"},"type":"array","title":"Stale Bookmakers"},"cached":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cached"},"cache_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Age Seconds"},"runners":{"items":{"type":"object"},"type":"array","title":"Runners"}},"type":"object","required":["race_id","venue","race_number","category","country","start_time","books_compared","runners"],"title":"RacingBestOddsOut","example":{"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,"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"}},"RacingChangesOut":{"properties":{"server_time":{"type":"string","title":"Server Time","description":"Send this back as `since` on your next poll. Deliberately set back 30 seconds from the server clock so no in-flight write is skipped — the feed is at-least-once, not exactly-once."},"races":{"items":{"$ref":"#/components/schemas/ChangedRaceOut"},"type":"array","title":"Races"}},"type":"object","required":["server_time"],"title":"RacingChangesOut"},"ResultsCoverageOut":{"properties":{"as_of":{"type":"string","title":"As Of","description":"When these counts were computed (UTC)."},"days":{"type":"integer","title":"Days","description":"How many Sydney days the window spans, ending today."},"from_day":{"type":"string","title":"From Day","description":"First Sydney date counted (inclusive)."},"to_day":{"type":"string","title":"To Day","description":"Last Sydney date counted — today in Sydney, so it is a PART day and its races are mostly `pending`."},"coverage_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coverage From","description":"Start time of the earliest result retained anywhere in the store, the same value /v1/racing/results puts on every row."},"races":{"type":"integer","title":"Races","description":"Every stored result in the window, all codes and all states. races == complete + pending + overdue + unavailable + unsupported + not_applicable, always."},"complete":{"type":"integer","title":"Complete","description":"Races carrying a `runners` field."},"pending":{"type":"integer","title":"Pending","description":"A collector exists and the pass that normally fills the race has not been yet."},"overdue":{"type":"integer","title":"Overdue","description":"That pass has been and gone with no field written, but the collector still re-attempts the row. NOT a terminal state — see `field_status` on /v1/racing/results for the horizons."},"unavailable":{"type":"integer","title":"Unavailable","description":"Past the collector's own retry horizon: no scheduled pass still selects the row."},"unsupported":{"type":"integer","title":"Unsupported","description":"No collector has ever existed for the code."},"not_applicable":{"type":"integer","title":"Not Applicable","description":"The race did not run (abandoned, postponed or transferred) and has no field — nothing to collect, not a gap."},"collectable":{"type":"integer","title":"Collectable","description":"races - unsupported - not_applicable: the races in this window that could carry a field at all. It is the denominator of `supported_complete_pct`, published so that a null percentage is readable as 0-of-0 rather than guessed at."},"complete_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Complete Pct","description":"complete / races as a percentage, across the whole window including codes that can never carry a field. NULL — not 0.0 — when `races` is 0. An empty window has no coverage figure, and 0.0 there is a fabricated number that reads as total failure: a `days=1` window called between Sydney midnight and the day's first result is empty on most days (the median first result of a Sydney day lands at 10:50 local), so this is an ordinary case and not a corner."},"supported_complete_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Supported Complete Pct","description":"complete / collectable: the figure to read if you only care about the codes we collect fields for. The raw `complete_pct` is dragged down by 1,254 stored races in codes that can never have one. NULL — not 0.0 — when `collectable` is 0, which happens both on an empty window and on a window holding only AU harness / NZ rows. If you alert on this figure, treat null as 'no data' and never as 'zero coverage'."},"by_category":{"items":{"type":"object"},"type":"array","title":"By Category","description":"One row per country + category over the window: {country, category, field_source, races, complete, pending, unavailable, unsupported, not_applicable}."},"by_day":{"items":{"type":"object"},"type":"array","title":"By Day","description":"One row per Sydney day + country + category, newest first, with the same counts. This is the day-by-day answer to 'is yesterday finished yet'."},"venues_incomplete":{"items":{"type":"object"},"type":"array","title":"Venues Incomplete","description":"Venue-days still missing a field in a code we DO collect: {day, venue, venue_id, category, country, settled} plus every counter above (races, complete, pending, overdue, unavailable, unsupported, not_applicable), which sum to `races` exactly. Ordered worst first: most `unavailable`, then the biggest outstanding shortfall, then oldest day. `settled` is true when nothing on that venue-day can still fill (no `pending`, no `overdue`) — that is the difference between a genuine terminal gap and a card simply still in progress, and without it a day mid-fill outranks a real one. A venue-day is listed when complete + not_applicable < races: an abandoned card has nothing to collect and is NOT a shortfall. A venue-day whose only gap is `unsupported` is deliberately absent — it would be most of the list and it is already counted above. Capped at 200 entries; `venues_incomplete_truncated` says when the cap bit."},"venues_incomplete_truncated":{"type":"boolean","title":"Venues Incomplete Truncated","default":false},"field_scope":{"type":"string","title":"Field Scope","description":"Prose: which codes are collected, from when, and what the never-collected codes are."}},"type":"object","required":["as_of","days","from_day","to_day","races","complete","pending","overdue","unavailable","unsupported","not_applicable","collectable","by_category","by_day","venues_incomplete","field_scope"],"title":"ResultsCoverageOut","example":{"as_of":"2026-09-10T03:40:12Z","by_category":[{"category":"greyhound","complete":698,"country":"AU","field_source":"topaz","not_applicable":0,"overdue":25,"pending":19,"races":742,"unavailable":0,"unsupported":0},{"category":"horse","complete":108,"country":"AU","field_source":"racing_australia","not_applicable":0,"overdue":21,"pending":2,"races":137,"unavailable":6,"unsupported":0},{"category":"harness","complete":0,"country":"AU","not_applicable":0,"overdue":0,"pending":0,"races":131,"unavailable":0,"unsupported":131}],"by_day":[{"category":"greyhound","complete":0,"country":"AU","day":"2026-09-10","field_source":"topaz","not_applicable":0,"overdue":0,"pending":19,"races":19,"unavailable":0,"unsupported":0},{"category":"horse","complete":42,"country":"AU","day":"2026-09-09","field_source":"racing_australia","not_applicable":0,"overdue":7,"pending":0,"races":49,"unavailable":0,"unsupported":0}],"collectable":879,"complete":806,"complete_pct":75.54,"coverage_from":"2026-08-15T05:53:00Z","days":7,"field_scope":"Full fields are collected for AU thoroughbred (Racing Australia, from the 2026-09-01 Sydney card) and AU greyhound (Topaz, from 2026-08-21). AU harness, NZ thoroughbred and NZ harness have no full-field source and are counted as unsupported. No pass reaches back beyond its own floor.","from_day":"2026-09-04","not_applicable":0,"overdue":52,"pending":21,"races":1067,"supported_complete_pct":91.7,"to_day":"2026-09-10","unavailable":0,"unsupported":188,"venues_incomplete":[{"category":"horse","complete":0,"country":"AU","day":"2026-09-09","not_applicable":0,"overdue":7,"pending":0,"races":7,"settled":false,"unavailable":0,"unsupported":0,"venue":"Randwick","venue_id":"randwick"}],"venues_incomplete_truncated":false}},"RewardIn":{"properties":{"reward_type":{"type":"string","title":"Reward Type","description":"`cash` or `credits`."},"payout_email":{"anyOf":[{"type":"string","maxLength":254},{"type":"null"}],"title":"Payout Email","description":"Where a cash remittance should go, if it differs from the account email. Ignored for `credits`."},"code":{"anyOf":[{"type":"string","maxLength":24},{"type":"null"}],"title":"Code","description":"Requested referral code, first call only. 3-24 characters, lowercase letters, digits and hyphens. Ignored once a code has been issued — a code that changes is a dead link on somebody's blog."}},"type":"object","required":["reward_type"],"title":"RewardIn"},"RunPIR":{"properties":{"at_m":{"type":"integer","title":"At M","description":"Metres from the finish, e.g. 800."},"position":{"type":"integer","title":"Position"}},"type":"object","required":["at_m","position"],"title":"RunPIR"},"RunPlacegetter":{"properties":{"position":{"type":"integer","title":"Position"},"name":{"type":"string","title":"Name"},"runner_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Runner Ref","description":"That horse's own stable ra: code — walkable straight back into this endpoint."},"weight_kg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Weight Kg"}},"type":"object","required":["position","name"],"title":"RunPlacegetter"},"RunnerIn":{"properties":{"name":{"type":"string","title":"Name"},"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number"},"win_price":{"type":"number","title":"Win Price"},"place_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Place Price"},"top2_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top2 Price"},"top3_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top3 Price"},"top4_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top4 Price"}},"type":"object","required":["name","win_price"],"title":"RunnerIn"},"SelectionIn":{"properties":{"name":{"type":"string","title":"Name"},"price":{"type":"number","title":"Price"},"point":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Point"}},"type":"object","required":["name","price"],"title":"SelectionIn"},"SignupRequest":{"properties":{"email":{"type":"string","title":"Email"},"plan":{"type":"string","title":"Plan","default":"free"},"label":{"type":"string","title":"Label","default":"My Key"},"use_case":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Case"},"use_case_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Case Note"},"referral":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Referral"},"referral_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Referral Code"}},"type":"object","required":["email"],"title":"SignupRequest"},"SourceBlock":{"properties":{"feed":{"type":"string","title":"Feed","default":"topaz"},"region":{"type":"string","title":"Region","default":"AU"},"sport":{"type":"string","title":"Sport","default":"greyhound"},"coverage_from":{"type":"string","title":"Coverage From"},"coverage_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Coverage To","description":"Meeting date of the latest run held, i.e. how far the last sync got."},"synced_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Synced At","description":"UTC timestamp of the last Topaz sync into this store."}},"type":"object","required":["coverage_from"],"title":"SourceBlock"},"SplitRecord":{"properties":{"starts":{"type":"integer","title":"Starts"},"wins":{"type":"integer","title":"Wins"},"seconds":{"type":"integer","title":"Seconds"},"thirds":{"type":"integer","title":"Thirds"}},"type":"object","required":["starts","wins","seconds","thirds"],"title":"SplitRecord"},"SportEventIn":{"properties":{"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"sport_key":{"type":"string","title":"Sport Key"},"home_team":{"type":"string","title":"Home Team"},"away_team":{"type":"string","title":"Away Team"},"commence_time":{"type":"string","title":"Commence Time"},"markets":{"items":{"$ref":"#/components/schemas/MarketIn"},"type":"array","title":"Markets"},"competition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Competition"}},"type":"object","required":["sport_key","home_team","away_team","commence_time","markets"],"title":"SportEventIn"},"SportIngestPayload":{"properties":{"bookmaker_key":{"type":"string","title":"Bookmaker Key"},"events":{"items":{"$ref":"#/components/schemas/SportEventIn"},"type":"array","title":"Events"}},"type":"object","required":["bookmaker_key","events"],"title":"SportIngestPayload"},"SportOddsEventOut":{"properties":{"id":{"type":"string","title":"Id"},"sport_key":{"type":"string","title":"Sport Key"},"sport_title":{"type":"string","title":"Sport Title"},"commence_time":{"type":"string","title":"Commence Time"},"home_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Home Team"},"away_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Away Team"},"competition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Competition"},"bookmakers":{"items":{"type":"object"},"type":"array","title":"Bookmakers"},"odds_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Odds Format"},"fetched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fetched At"},"data_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Data Age Seconds"},"freshest_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Freshest Age Seconds"},"stale":{"type":"boolean","title":"Stale","default":false},"stale_bookmakers":{"items":{"type":"string"},"type":"array","title":"Stale Bookmakers","default":[]},"cached":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cached"},"cache_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Age Seconds"},"canonical_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Canonical Event Id"},"data_quality":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Data Quality"}},"type":"object","required":["id","sport_key","sport_title","commence_time","home_team","away_team","bookmakers"],"title":"SportOddsEventOut","example":{"away_team":"Collingwood","bookmakers":[{"key":"sportsbet","markets":[{"key":"h2h","outcomes":[{"name":"Brisbane Lions","price":1.9}]}],"title":"Sportsbet"}],"commence_time":"2026-06-07T09:30:00Z","data_age_seconds":15,"fetched_at":"2026-06-07T09:29:45+00:00","home_team":"Brisbane Lions","id":"event_123","sport_key":"afl","sport_title":"AFL","stale":false}},"SportOut":{"properties":{"key":{"type":"string","title":"Key"},"title":{"type":"string","title":"Title"},"group":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Group"},"active":{"type":"boolean","title":"Active"},"cached":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cached"},"cache_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Age Seconds"},"canonical_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Canonical Event Id"},"data_quality":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Data Quality"}},"type":"object","required":["key","title","group","active"],"title":"SportOut","example":{"active":true,"group":"Australian Rules","key":"afl","title":"AFL"}},"SportsArbOut":{"properties":{"event_id":{"type":"string","title":"Event Id"},"sport_key":{"type":"string","title":"Sport Key"},"home_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Home Team"},"away_team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Away Team"},"commence_time":{"type":"string","title":"Commence Time"},"is_arb":{"type":"boolean","title":"Is Arb"},"arb_pct":{"type":"number","title":"Arb Pct"},"optimal_stakes":{"items":{"type":"object"},"type":"array","title":"Optimal Stakes"},"max_overlay_pct":{"type":"number","title":"Max Overlay Pct"},"selections":{"items":{"type":"object"},"type":"array","title":"Selections"}},"type":"object","required":["event_id","sport_key","home_team","away_team","commence_time","is_arb","arb_pct","optimal_stakes","max_overlay_pct","selections"],"title":"SportsArbOut","example":{"arb_pct":2.4,"away_team":"Magpies","commence_time":"2026-06-07T09:30:00Z","event_id":"event_123","home_team":"Lions","is_arb":true,"max_overlay_pct":5.1,"optimal_stakes":[],"selections":[{"best_bookmaker":"sportsbet","best_price":2.05,"name":"Lions"}],"sport_key":"afl"}},"TrackConditionsOut":{"properties":{"date":{"type":"string","title":"Date","description":"Meeting day, Australia/Sydney calendar date."},"meetings":{"items":{"type":"object"},"type":"array","title":"Meetings"},"note":{"type":"string","title":"Note"}},"type":"object","required":["date","meetings","note"],"title":"TrackConditionsOut","example":{"date":"2026-09-10","meetings":[{"category":"horse","changes":[{"category":"horse","going":"Good","going_rating":4,"observed_at":"2026-09-09T04:16:29Z","rail":"True","source":"racing_australia","track_condition":"Good 4","weather":"Windy"},{"category":"horse","going":"Good","going_rating":4,"observed_at":"2026-09-10T02:36:58Z","prev_going":"Good","prev_going_rating":3,"prev_track_condition":"Good3","race_id":"race_123","source":"betgold","track_condition":"Good4"}],"current":{"as_of":"2026-09-10T02:17:24Z","field_as_of":{"penetrometer":"2026-09-10T02:17:24Z","rail":"2026-09-10T02:17:24Z","track_condition":"2026-09-10T02:17:24Z","track_type":"2026-09-10T02:17:24Z","weather":"2026-09-10T02:17:24Z"},"going":"Good","going_rating":4,"penetrometer":"5.63","rail":"True","source":"racing_australia","sources":{"penetrometer":"racing_australia","rail":"racing_australia","track_condition":"racing_australia","track_type":"racing_australia","weather":"racing_australia"},"track_condition":"Good 4","track_type":"Turf","weather":"Windy"},"merged_from":["picklebet park warwick","warwick"],"official_venue_norm":"picklebet park warwick","venue":"picklebet park warwick","venue_id":"picklebet-park-warwick","venue_norm":"picklebet park warwick","venue_site":"picklebet-park-warwick"}],"note":"…"}},"TrainerRef":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id","description":"Topaz trainerId. Stable across meetings."},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","title":"TrainerRef"},"UsageOut":{"properties":{"plan":{"type":"string","title":"Plan"},"account_email_masked":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Account Email Masked"},"credits_used":{"type":"integer","title":"Credits Used"},"credits_limit":{"type":"integer","title":"Credits Limit"},"credits_remaining":{"type":"integer","title":"Credits Remaining"},"reset_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reset At"},"period_start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period Start"},"next_reset_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Reset At"},"burn_rate_per_day":{"type":"number","title":"Burn Rate Per Day","default":0.0},"projected_month_end_credits":{"type":"integer","title":"Projected Month End Credits","default":0},"projected_exhaustion_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Projected Exhaustion Date"},"warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warning"},"upgrade":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Upgrade"},"all_time":{"type":"object","title":"All Time","default":{}},"all_time_by_endpoint":{"items":{"type":"object"},"type":"array","title":"All Time By Endpoint","default":[]},"usage_by_endpoint":{"items":{"type":"object"},"type":"array","title":"Usage By Endpoint","default":[]},"recent_activity":{"items":{"type":"object"},"type":"array","title":"Recent Activity","default":[]},"usage_by_endpoint_period":{"items":{"type":"object"},"type":"array","title":"Usage By Endpoint Period","default":[]},"recent_activity_period":{"items":{"type":"object"},"type":"array","title":"Recent Activity Period","default":[]},"plans":{"additionalProperties":{"type":"object"},"type":"object","title":"Plans","default":{}}},"type":"object","required":["plan","credits_used","credits_limit","credits_remaining","reset_at"],"title":"UsageOut","example":{"all_time":{"calls":310,"credits":402,"endpoints":1},"all_time_by_endpoint":[{"calls":310,"credits":402,"endpoint":"/v1/sports"}],"burn_rate_per_day":6.0,"credits_limit":1000,"credits_remaining":958,"credits_used":42,"next_reset_at":"2026-08-01T00:00:00","period_start":"2026-07-01T00:00:00","plan":"free","plans":{"free":{"credits_per_month":500,"price_aud":0}},"projected_month_end_credits":186,"recent_activity":[{"credits":1,"endpoint":"/v1/sports","ts":"2026-06-07T02:20:00"}],"recent_activity_period":[{"credits":1,"endpoint":"/v1/sports","ts":"2026-06-07T02:20:00"}],"reset_at":"2026-07-01T00:00:00","upgrade":{"checkout":{"body":{"email":"you@example.com","plan":"hobby"},"endpoint":"https://api.puntersedge.online/v1/signup","method":"POST"},"covers_projected_need":true,"credits_per_month":7500,"price_aud":9,"recommended_plan":"hobby","sized_for_credits_per_month":186,"url":"https://puntersedge.online/api/pricing?plan=hobby&utm_source=api_usage"},"usage_by_endpoint":[{"calls":310,"credits":402,"endpoint":"/v1/sports"}],"usage_by_endpoint_period":[{"calls":42,"credits":42,"endpoint":"/v1/sports"}]}},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VenueOut":{"properties":{"venue_id":{"type":"string","title":"Venue Id"},"venue_canonical":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Canonical"},"venue_site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Venue Site"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"categories":{"items":{"type":"string"},"type":"array","title":"Categories"},"spellings":{"items":{"type":"string"},"type":"array","title":"Spellings"}},"type":"object","required":["venue_id","venue_canonical","venue_site","country","categories","spellings"],"title":"VenueOut","example":{"categories":["horse"],"country":"AU","spellings":["Sandown - HILLSIDE"],"venue_canonical":"Sandown Hillside","venue_id":"sandown-hillside","venue_site":"sandown"}},"WebhookCreate":{"properties":{"url":{"type":"string","title":"Url","description":"HTTPS endpoint that receives the POST. Plain http:// is rejected with 422.","examples":["https://example.com/hooks/puntersedge"]},"events":{"items":{"type":"string"},"type":"array","title":"Events","description":"Events to subscribe to. Two events have live producers: `arb.opportunity` (a cross-book arb opened) and `race.odds_open` (the first bookmaker prices for a race just landed — fires once per race, so you can fetch the card on push instead of schedule-polling). Since the early-market lanes of 2026-09-01 it fires HOURS ahead, not minutes: measured over the 48h to 2026-09-03 11:00 UTC, the first price landed a median 10.1h before the jump on AU thoroughbreds, 5.3h on greyhounds, 6.8h on harness - typically overnight or early morning for the day's card, from one or two early books; the full board fills inside about an hour of the jump. `odds.change` is delivered solely by POST /v1/webhooks/{id}/test; `race.result` and `market.suspend` are accepted but never fire.","examples":[["arb.opportunity"],["race.odds_open"]]},"min_edge_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Edge Pct","description":"Only deliver arb.opportunity when the theoretical edge is at least this percentage. Omit for every opportunity.","examples":[1.5]},"sports":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sports","description":"Only deliver events for these sports. Sport keys as returned by GET /v1/sports; racing events (arbs and race.odds_open) arrive as racing_horse, racing_greyhound or racing_harness. Omit for all sports.","examples":[["nrl","afl"],["racing_horse","racing_greyhound"]]}},"type":"object","required":["url"],"title":"WebhookCreate"},"WebhookDeliveryResponse":{"properties":{"id":{"type":"string","title":"Id"},"webhook_id":{"type":"string","title":"Webhook Id"},"event_type":{"type":"string","title":"Event Type"},"payload":{"anyOf":[{},{"type":"null"}],"title":"Payload"},"status":{"type":"string","title":"Status","description":"delivered (receiver answered 2xx) or failed (both attempts refused, see `error`). Rows written before 2026-09-03 read `pending`.","default":"pending"},"attempt_count":{"type":"integer","title":"Attempt Count","description":"POST attempts made for this row, at most 2.","default":0},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"delivered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Delivered At"}},"type":"object","required":["id","webhook_id","event_type","payload","status_code","error","delivered_at"],"title":"WebhookDeliveryResponse"},"WebhookResponse":{"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"events":{"items":{"type":"string"},"type":"array","title":"Events"},"min_edge_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Edge Pct"},"sports":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sports"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"last_delivery_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Delivery At"},"failure_count":{"type":"integer","title":"Failure Count"},"secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret"},"delivery_entitled":{"type":"boolean","title":"Delivery Entitled","default":true},"delivery_paused_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Delivery Paused Reason"}},"type":"object","required":["id","url","events","is_active","created_at","last_delivery_at","failure_count"],"title":"WebhookResponse"},"Problem":{"type":"object","properties":{"type":{"type":"string","format":"uri","description":"Stable URI identifying the error class.","example":"https://puntersedge.online/errors/rate-limit"},"title":{"type":"string","description":"Short human-readable summary."},"status":{"type":"integer","description":"HTTP status code.","example":429},"detail":{"description":"Explanation of this occurrence. A string for most errors; an array of field errors for 422; an object for 429."}},"required":["type","title","status"]}},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"**Get a free key** at [puntersedge.online/api](https://puntersedge.online/api).\n\nPaste your key in the **Authorize** dialog (lock icon, top right of Swagger UI) to try all endpoints live."}}},"tags":[{"name":"Sports","description":"Sports catalogue and market odds endpoints for AFL, NRL, NBA, tennis, cricket, rugby and more. Racing is NOT here — horse, harness and greyhound live under the Racing tag at /v1/racing/next-to-go and /v1/racing/events. There is no racing sport_key."},{"name":"Racing","description":"Australian and New Zealand racing endpoints for next-to-go races, race events, runners and bookmaker prices. AU carries thoroughbred, harness and greyhound; NZ carries thoroughbred and harness — there are no New Zealand greyhounds in the feed."},{"name":"Best Odds","description":"Best available price per selection, bookmaker comparison and arbitrage indicators."},{"name":"Arb & Value","description":"Sports arbitrage scanners and overlay/value price comparison. Everything under this tag is SPORTS-ONLY and takes a sport_key — there is no racing arb scanner here. For cross-book racing value use GET /v1/racing/best-odds under the Racing tag: a market_percentage under 100 means the best prices across the 14 Australian bookmakers beat the field."},{"name":"Account","description":"Authenticated usage, credit balance, connector health and developer key utilities. Subscribers manage billing here too — GET /v1/billing/portal returns a Stripe portal link for changing the card, pulling invoices or cancelling, authenticated by the API key itself, so there is no separate account to create. It costs no credits."},{"name":"Webhooks","description":"Subscribe to real-time arb opportunity events via HTTP webhook. Payloads are HMAC-SHA256 signed. Creating a subscription and sending a test fire require Standard, Plus, Business, Platform, Unlimited or Enterprise; Free and Hobby get 403. Legacy Starter, Pro and Growth keys also qualify. Listing your own subscriptions, reading their delivery log and deleting them stay open on every plan, so a downgraded key can still see and stop a subscription it owns."},{"name":"Signup","description":"Developer signup and API key creation endpoints."}],"security":[{"ApiKeyAuth":[]}]}