{
  "info": {
    "name": "PuntersEdge \u2014 Australian Odds API",
    "description": "Live odds from Australian bookmakers through one REST/JSON API \u2014 15 Australian bookmakers on horse, greyhound and harness racing, including a next-to-go endpoint. Sports coverage is thinner, and this says so rather than implying a uniform market: at most 3 bookmakers on a single NRL fixture, 6 on NBA, and 1 on some competitions. Plus best-odds comparison, arbitrage and value endpoints.\n\n**Getting started**\n1. The `Sandbox (no key)` folder runs immediately \u2014 no signup, live data.\n2. For everything else, get a free key (no credit card) at https://puntersedge.online/api and set the `apiKey` collection variable.\n\nFull reference: https://puntersedge.online/developers/api-reference\nSchema: https://puntersedge.online/openapi.json\nPython SDK: https://github.com/Propertyscout001/puntersedge-python\nThis collection: https://api.puntersedge.online/postman.json \u2014 re-import that URL to pick up new endpoints.\n\nRequests cost credits; a typical racing or odds call is 1\u20133 credits. 18+ only. Odds data is for information and application development, not betting advice.",
    "version": "1.0.0",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Sandbox (no key)",
      "item": [
        {
          "name": "Sample next-to-go races (no key required)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/demo/racing/next-to-go",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "demo",
                "racing",
                "next-to-go"
              ],
              "query": [],
              "variable": []
            },
            "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.\n\nNo API key required."
          },
          "response": []
        },
        {
          "name": "Sample best-odds + arb detection (no key required)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/demo/best-odds?sport=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "demo",
                "best-odds"
              ],
              "query": [
                {
                  "key": "sport",
                  "value": "",
                  "disabled": true,
                  "description": "Optional sport_key; auto-picks a live sport if omitted"
                }
              ],
              "variable": []
            },
            "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.\n\nNo API key required."
          },
          "response": []
        },
        {
          "name": "Live book+sport snapshot (no key; powers SEO page differentiation)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/demo/book-sport?book=sportsbet&sport=horse-racing",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "demo",
                "book-sport"
              ],
              "query": [
                {
                  "key": "book",
                  "value": "sportsbet",
                  "disabled": false,
                  "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. (required)"
                },
                {
                  "key": "sport",
                  "value": "horse-racing",
                  "disabled": false,
                  "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/*). (required)"
                }
              ],
              "variable": []
            },
            "description": "Public, book-specific live odds for one sport. Truncated to a few events/races. Cached 90s.\n\nNo API key required."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Racing",
      "item": [
        {
          "name": "Next races to run",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/next-to-go?num_races=&limit=&categories=&category=&bookmakers=&include_unresolved=&country=&venue=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "next-to-go"
              ],
              "query": [
                {
                  "key": "num_races",
                  "value": "",
                  "disabled": true,
                  "description": "Up to 200. Pass 200 to bulk-pull every currently quoted race in one call instead of paging."
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": "Alias for `num_races`, accepted because it is the near-universal REST spelling and because /v1/racing/results already takes it \u2014 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."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "horse,greyhound,harness or omit for all"
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones."
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing."
                },
                {
                  "key": "include_unresolved",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 58.8% of horse races in a measured window \u2014 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."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "description": "ISO country codes, comma-separated (e.g. AU, AU,NZ or GB,IE). Omit for every country. GB,IE is the British and Irish card, priced by the UK-licensed bookmakers (ladbrokes_uk, williamhill, betway_uk \u2014 region gb) alongside the Australian books that quote it; UK and Irish racecards number races by running order, and the UK books' place_price is derived from their each-way terms (see the field notes). 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. Outside AU, NZ and GB/IE a race carries a median of ONE bookmaker, 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 \u2014 typically the later races on the card \u2014 so it is not a clean AU/foreign split. Omit it and read `country` per race if you need those."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "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."
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits for up to 20 races, 3 for 21\u201360, 4 for 61\u2013200 \u2014 the default request of 10 races costs 2, and keys issued before 22 September 2026 pay 2 at any size. Returns the next races to run with runners and prices \u2014 EVERY country by default, not only AU and NZ. Measured over 10.5 days, 60.8% of the race catalogue is non-AU, and a race outside AU/NZ carries a median of ONE bookmaker against 13 for an Australian one, so pass `country=AU` (pairing it with `include_unresolved=true`, per that parameter) if you want an Australian card. `categories` can filter horse, greyhound and harness racing. NZ runs thoroughbred and harness only \u2014 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 \u2014 the practical \"bulk odds\" pull. It looks 24\nhours ahead, so on an Australian morning it returns the whole day's card. 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` \u2014 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 \u2014 drop that leg. `win_price` is the fixed win price\nand `place_price` the bookmaker's place price; for the UK-licensed books (`ladbrokes_uk`,\n`williamhill`, `betway_uk`) no place market is quoted, so `place_price` is DERIVED from the\neach-way terms the book publishes for the race \u2014 1 + (win \u2212 1) \u00d7 the each-way fraction \u2014 and\nthe race's `places_paid` is the number of places those terms pay. It is what the place half of\nan each-way bet returns, not a quoted place market. Each entry's `age_basis` says where\nits age counts from: `poll` from our own read of the book, `push_receipt` from when a pushed\nupdate reached us, in which case the true age is longer by the sender's own delay. Every\nage is recomputed when the response is sent, including on a race served from the stored\nboard (`cached: true`, `cache_age_seconds`). The full rules:\nhttps://puntersedge.online/developers/freshness\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 \u2014 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 \u2014 and every Betr price outside half an hour \u2014 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. A scratching no Australian bookmaker has\nreported, while two or more of them still price the runner, is held back: the runner stays\nin `runners`, and the scratching is listed once a bookmaker reports it.\n\n**Every bookmaker leg carries `source_url` \u2014 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) \u2014\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 \u2014 a\nguessed link that lands a punter on the wrong page is worse than a missing one \u2014 which\ntoday means betgold, boostbet and the Betfair exchange (no provable race-page route;\nmeasured 2026-09-16 over 48 hours, every other book links 100% of its quotes).\n**This list moves, so do not code against it.** betdeluxe published no usable route when\nthis was last measured on 2026-09-01 and links 100% of its quotes now; the Betfair\nexchange was missing from the sentence entirely. Branch on whether `source_url` is\npresent on the quote in front of you, never on this list.\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 \u2014 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` \u2014 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` \u2014 /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. **Plus and above** (since 21 September 2026):\nthe betslip fields are served on Plus, Business, Platform and Scale, and a Standard or\nHobby key issued before that date keeps them - a key never loses what it was sold. On every\nother plan the fields are simply absent, exactly as on a book with no route; `source_url`,\nthe race-page link, is on every plan and is unaffected. 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 \u2014\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** \u2014 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. `horse_ref` is the\nexception, because it is derived from the runner's name rather than read from anywhere:\na Hong Kong thoroughbred carries one, and it joins that runner to itself across Hong\nKong meetings. Coverage follows\nthe HKJC's own calendar rather than ours \u2014 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 \u2014 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 \u2014 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 `jockey`/`trainer`/`weight`/`form` where\nno book supplies them, `runner_ref` (Racing Australia's code for the runner, from its\nlatest read of the field \u2014 on race day the same value `/v1/racing/results` and the archives\ncarry for this race; Racing Australia issues a new code for the same horse each day, so join\na horse across meetings on `horse_ref`), 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\n**barrier: for AU thoroughbreds the drawn barrier from Racing Australia; for greyhounds\nand harness as the book publishes it \u2014 BetRight, BetGold and BoostBet renumber after\nscratchings, BetDeluxe does not (measured 2026-09-28).** That renumbering was measured on\nthoroughbred fields against Racing Australia's draw (39% of those three books' rows one or\nmore gates LOWER than RA's), which is why, since\n2026-09-29, RA's draw REPLACES every book's own value on an AU thoroughbred RA's\nacceptances name, rather than only filling a gap. A runner RA does not name \u2014 any NZ\nrace, or a name RA spells differently \u2014 still carries whatever the books sent.\n**`jockey` is a person's name** (2026-09-29): an apprentice's weight claim is stripped\nrather than served glued to it (\"Sharni Webster(A3.0)\" is served \"Sharni Webster\").\nGreyhound runners carry the trainer in `trainer` and `jockey` null; harness runners carry\nthe driver in `jockey`. A greyhound `jockey` value you stored from a response before\n2026-09-29 may hold the trainer.\n\n**`horse_ref` is the one runner field that is DERIVED rather than reported** (2026-09-21).\nEvery thoroughbred runner here carries `horse_ref`, `pe:<name>` \u2014 the registered name\nfolded to letters and digits \u2014 because it is computed from the name the panel already\nagreed on rather than fetched from anywhere, so it is present on races no book enriched\nand on foreign meetings alike. It is the same value on /v1/racing/results runners and\nplacings, the closing-line and price-path archives, /v1/racing/horses/form and\n/v1/racing/horses/runs, and it is the key to join this runner to itself at another\nmeeting \u2014 which `runner_ref` cannot do. Null on greyhound and harness runners, where\n`runner_ref` (`grv:`/`grsa:`) is already a per-dog registry id that persists.\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` \u2014 the driver \u2014 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`. Conditional requests: every 200 carries an `ETag`; send it back as `If-None-Match` and an unchanged board is answered `304 Not Modified` for 0 credits (served from the stored board, which a paid key reuses for at most 20 s and a Free key for at most 65 s; a poll that lands on a cache miss is billed as usual)."
          },
          "response": []
        },
        {
          "name": "Upcoming race list",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/events?hours_ahead=&date=&categories=&category=&country=&include_unresolved=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "events"
              ],
              "query": [
                {
                  "key": "hours_ahead",
                  "value": "",
                  "disabled": true,
                  "description": "How many hours ahead to list, from now. Your ceiling depends on the plan and is returned on every response as X-Horizon-Hours: 24 on Free and Hobby, 48 on Standard, 168 (7 days) on Plus and above. Asking past your ceiling is a free 403 naming it, never a silent clamp. Mutually exclusive with date."
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Single-day alias: date=YYYY-MM-DD lists that Australian Eastern meeting day instead of a rolling hours_ahead window. Mutually exclusive with hours_ahead. A day is reachable when its last moment is inside your horizon, so this is bounded by the same X-Horizon-Hours ceiling and is not a separate entitlement. Forward-looking only -- a past date is a free 422 pointing at /v1/racing/results and /v1/racing/closing-lines."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "description": "ISO country codes, comma-separated (e.g. AU, AU,NZ or GB,IE for the British and Irish card). 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."
                },
                {
                  "key": "include_unresolved",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 58.8% of horse races in a measured window \u2014 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."
                }
              ],
              "variable": []
            },
            "description": "Cost: 1 credit. The CARD - every race scheduled in the requested window, optionally\nfiltered by racing category and country. EVERY country is served by default, not only\nAU and NZ \u2014 pass `country=AU` (pairing it with `include_unresolved=true`, per that\nparameter) if you want an Australian card.\n\n**How far ahead you can see (2026-09-22).** `hours_ahead` reaches 24h on Free and\nHobby, 48h on Standard, and 168h (7 days) on Plus and above. Every response carries\nyour ceiling as `X-Horizon-Hours` and the window actually used as\n`X-Horizon-Used-Hours`, so you never have to discover it by trial. Asking past it is a\n403 naming the tier that reaches further, and costs no credit -- it is never a silent\nclamp to a shorter window. `date=YYYY-MM-DD` is the single-day alias for an Australian\nEastern meeting day; a day is reachable when its last moment is inside your horizon,\nso it is bounded by the same ceiling rather than being a second entitlement.\n\nNothing lost anything here: 24h was the hard cap for every plan, free included, until\nthis landed, and Free and Hobby still have exactly that. 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 \u2014 Happy Valley on Wednesday evenings and Sha Tin on Saturday or Sunday\nafternoons, AEST \u2014 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 (`horse_ref` is derived from the\nrunner's name, so it is there). 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 \u2014 `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 \u2014 the actual off, where `start_time` is the\nadvertised one and moves. Both are POLL-OBSERVED: each is the first poll at which the state\nwas seen, so it lands up to one polling pass after it, and passes are most frequent in the\nminutes before the jump. 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 \u2014 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 \u2014 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."
          },
          "response": []
        },
        {
          "name": "Canonical venue directory",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/venues",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "venues"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 1 credit. Every venue this API has actually served recently, with its canonical\nidentity \u2014 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 \u2014 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, 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."
          },
          "response": []
        },
        {
          "name": "Settled race results",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/results?hours_back=&date=&categories=&category=&venue=&country=&status=&limit=&offset=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "results"
              ],
              "query": [
                {
                  "key": "hours_back",
                  "value": "",
                  "disabled": true,
                  "description": "How far back to look, in hours (max 7 days)"
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Single-day alias: date=YYYY-MM-DD returns the races that start on that calendar date in Sydney time (AET), instead of a rolling hours_back window (YYYYMMDD is read the same way). It is the same meeting_date_aet that /v1/racing/closing-lines stores, so the two join on date, venue and race_number. Each race belongs to exactly one date: a race that starts after midnight Sydney time (the last races of a Perth night meeting, British and Irish afternoon and evening racing) is under the next date. Mutually exclusive with hours_back. Also accepted by /v1/racing/closing-lines and /v1/racing/acceptances. NOT by /v1/racing/events, which is forward-looking and takes hours_ahead."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "horse,greyhound,harness or omit for all"
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Case-insensitive exact venue match, comma-separated for several, e.g. 'Rosehill' or 'Randwick,Eagle Farm,Flemington' \u2014 the same spelling /v1/racing/next-to-go takes"
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "description": "ISO country codes, comma-separated, case-insensitive (e.g. AU or AU,NZ). Matched against the result's `country`. Which countries have results at all is published by /v1/racing/results/coverage and the results_scope note on /v1/racing/closing-lines/coverage; GB and IE thoroughbred results come from Betway's settlement feed from 2026-10-06; a country with prices but no results source returns no rows here. A result whose country was never resolved (null) is excluded by any value here, so omit the filter if you need those rows."
                },
                {
                  "key": "status",
                  "value": "",
                  "disabled": true,
                  "description": "Filter to 'final', 'interim' or 'abandoned'"
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "offset",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits. Settled AU/NZ race results \u2014 finishing order with the bookmaker's own\nsettled fixed prices, plus scratchings and their deductions \u2014 and, from 2026-10-06, British and\nIrish thoroughbred results from Betway's settlement feed: every runner with its starting price\n(`sp` on placings and runners) and its finishing position where Betway publishes one (the whole\norder on the British races measured; on the Irish, the first four first and the rest within\nabout 25 minutes of the off, while the result is still `interim`), non-runners as deductions with\nno Rule 4 figure, no dividends and no fixed settling prices (`win_price`/`place_price` null). UK racecards\npublish no race numbers; a GB/IE result takes the running-order number of the race it matched.\nFrom 2026-10-12, British and Irish greyhound results come from PointsBet's settled race card: the\npaid places (three on a six-dog field) with PointsBet's own settled fixed prices as\n`win_price`/`place_price`, the whole field as `runners` with a position on each placed dog,\nscratchings as deductions; no starting prices and no dividends on these races.\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) \u2014 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'`; a result written before that date carries such a runner as\n`status: ran` with a null `position`. Such a row\ncan still carry `sp` \u2014 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` \u2014 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 \u2014 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 \u2014 not our consensus, and\nnot the fixed price the bookmaker settled at. The two differ, and both are returned so you\ncan compare them. Thoroughbred SP is Racing Australia's (its favouritism marker lands in\n`sp_note`), greyhound SP is our own collection's (Victorian tracks via FastTrack, every other\nstate via the national results pages, from 2026-09-13), and harness SP is always null because\nno source we hold publishes it. A null `sp` means unknown \u2014 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 \u2014 use it rather than matching on venue + race number + start time.\n\n**Joining one RACE across endpoints: `runner_ref`. Joining one HORSE across meetings:\n`horse_ref`.** Every thoroughbred entry in `runners` carries `runner_ref` (`ra:<code>`),\nthe code Racing Australia published for the runner on race day \u2014 the same value the\nclosing-line, price-path and markets archives carry and the live board carried that day,\nso it joins this result to every other row about the race. Racing Australia issues a new\ncode for the same horse each day, so grouping a career on `runner_ref` returns one row per\nstart. For a career, every thoroughbred entry in `runners` and `placings` carries\n`horse_ref`, `pe:<name>` \u2014 the registered name folded to letters and digits \u2014 the same\nvalue on the live board, the closing-line and price-path archives, /v1/racing/horses/form\nand /v1/racing/horses/runs. `horse_ref` is derived at serve time from the name already on\nthe row, so it is on every historical result too, and it is null on greyhound and harness\nrows \u2014 a greyhound's `runner_ref` (`grv:`/`grsa:`) is a per-dog registry id and already\npersists.\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 \u2014 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 \u2014\nthe actual off, where `start_time` is the advertised one and moves. Both are POLL-OBSERVED\n(polling passes are most frequent in the minutes before the jump), so each is the first\npoll at which the state was seen rather than the exact instant, and both are SET ONCE.\nThey 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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. AU thoroughbred fields come from\nRacing Australia and AU greyhound fields from our own collection; AU harness, NZ\nthoroughbred and NZ harness fields have come from TAB New Zealand since 2026-10-04\n(`field_source: tabnz`, hourly, seven-day window). An entry carrying `origin: board` is a\ncopy of the pre-race board (driver, trainer, barrier, form, positions for placegetters\nonly, no times, SP or margins), not an official finishing field. 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 \u2014 'Good (4)' becomes `going: \"Good\"`,\n`going_rating: 4` \u2014 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."
          },
          "response": []
        },
        {
          "name": "What the results field coverage actually is",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/results/coverage?days=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "results",
                "coverage"
              ],
              "query": [
                {
                  "key": "days",
                  "value": "",
                  "disabled": true,
                  "description": "Sydney days to count, ending with today (a part day)."
                }
              ],
              "variable": []
            },
            "description": "Cost: 1 credit. The results store's own measurement of its FULL-FIELD coverage, per\nSydney day, category and venue \u2014 the aggregate answer to the question `field_status`\nanswers per race on /v1/racing/results.\n\n`runners: null` on its own can mean 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 exists for (`unsupported` \u2014 since 2026-10-04 only foreign races an\nAU book listed; AU harness and both NZ codes, 1,254 of the 5,577 stored results as at\n2026-09-10, are collected from TAB New Zealand from that day), 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 \u2014 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 \u2014 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."
          },
          "response": []
        },
        {
          "name": "Racing price history / closing line",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/price-history?race_id=&venue=&race_number=&date=&bookmakers=&include_points=&max_points=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "price-history"
              ],
              "query": [
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "Race id. If omitted, give venue + race_number + date."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue name, case-insensitive (with race_number + date)"
                },
                {
                  "key": "race_number",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Race date, YYYY-MM-DD (YYYYMMDD accepted): the UTC day of the race's start, or, when that finds nothing, the Australian Eastern meeting date, so the date /v1/racing/results and /v1/racing/closing-lines give a race resolves it here too. A date that resolves nothing is a free 404 naming the venue's races with history around it."
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing."
                },
                {
                  "key": "include_points",
                  "value": "",
                  "disabled": true,
                  "description": "False returns open/close/high/low only"
                },
                {
                  "key": "max_points",
                  "value": "",
                  "disabled": true,
                  "description": "Cap on returned points; response flags truncation"
                }
              ],
              "variable": []
            },
            "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` \u2014 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\u2192close, 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 \u2014 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.)"
          },
          "response": []
        },
        {
          "name": "Racing steamers and drifters",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/movers?direction=&min_move_pct=&min_books=&categories=&category=&country=&include_unresolved=&max_mins_to_jump=&limit=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "movers"
              ],
              "query": [
                {
                  "key": "direction",
                  "value": "",
                  "disabled": true,
                  "description": "firming, drifting, or omit for both"
                },
                {
                  "key": "min_move_pct",
                  "value": "",
                  "disabled": true,
                  "description": "Minimum consensus move, percent"
                },
                {
                  "key": "min_books",
                  "value": "",
                  "disabled": true,
                  "description": "Only report a move confirmed by at least this many books"
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "horse,greyhound,harness or omit for all"
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "include_unresolved",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 measured 3 labelled AU against 21 unresolved in a live window \u2014 so country=AU alone can return far fewer races than are running, or none at all."
                },
                {
                  "key": "max_mins_to_jump",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "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 \u2014 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 \u2014 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` \u2014 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."
          },
          "response": []
        },
        {
          "name": "Best racing price per runner across books",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/best-odds?hours_ahead=&date=&num_races=&limit=&categories=&category=&venue=&bookmakers=&country=&include_unresolved=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "best-odds"
              ],
              "query": [
                {
                  "key": "hours_ahead",
                  "value": "",
                  "disabled": true,
                  "description": "How many hours ahead to scan for priced races, from now. Default 6, which is what this endpoint has always used. The ceiling is your plan horizon, returned as X-Horizon-Hours: 24 Free and Hobby, 48 Standard, 168 Plus and above. Raising it past about 12h returns races no book has opened yet -- the first price lands a median 10.1h before the jump on AU thoroughbreds -- so a wide window is for building tomorrow's board, not for finding more prices today."
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Single-day alias: date=YYYY-MM-DD scans that Australian Eastern meeting day instead of a rolling hours_ahead window. Mutually exclusive with hours_ahead, bounded by the same X-Horizon-Hours ceiling, forward-looking only. num_races still caps the rows returned, so pair a date with num_races=150 for a full card."
                },
                {
                  "key": "num_races",
                  "value": "",
                  "disabled": true,
                  "description": "Up to 150. Pass 150 to compare every race on the day's card in one call instead of paging."
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": "Alias for `num_races`, accepted because it is the near-universal REST spelling and because /v1/racing/results already takes it \u2014 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."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "horse,greyhound,harness or omit for all"
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 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."
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 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."
                },
                {
                  "key": "include_unresolved",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 58.8% of horse races in a measured window \u2014 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."
                }
              ],
              "variable": []
            },
            "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** \u2014 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 \u2014 a book can quote the win and not the place on\nthe same runner \u2014 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\u26a0\ufe0f `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 \u2014 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 \u2014 `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. Not every book quotes New Zealand, so an NZ race is compared across\nfewer books than an AU race; the live per-country book counts are published at\nhttps://puntersedge.online/coverage-report. 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 \u2014 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 \u2014 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` \u2014 `horse_ref` alone is present, because it is\nderived from the runner's name and needs no Racing Australia read. Unlike the rest of\nthe 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 \u2014 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`.\nOn Plus and above both also carry `betslip_url`, the runner-level betslip link of that\nsame book - null when the book has no such route, and absent on plans below Plus. See the\ntable 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 \u2014 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. **Plus and above** (since 21 September 2026):\nthe betslip fields are served on Plus, Business, Platform and Scale, and a Standard or\nHobby key issued before that date keeps them - a key never loses what it was sold. On every\nother plan the fields are simply absent, exactly as on a book with no route; `source_url`,\nthe race-page link, is on every plan and is unaffected. 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."
          },
          "response": []
        },
        {
          "name": "Greyhound form history for one dog, from our own results",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/greyhounds/form?dog=VELOCITY YIN&limit=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "greyhounds",
                "form"
              ],
              "query": [
                {
                  "key": "dog",
                  "value": "VELOCITY YIN",
                  "disabled": false,
                  "description": "Dog name (matched on letters and digits, case-insensitive, whole name), or a runner_ref like grv:1055038365 / grsa:441535 to restrict to one registry id. (required)"
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": "Most recent runs to return."
                }
              ],
              "variable": []
            },
            "description": "Cost: 3 credits. Every run we hold for one greyhound, newest first, from our own collection.\n\nSOURCE. Not a third-party form feed: every run comes off the official public results pages we\nread ourselves \u2014 Victorian tracks from FastTrack (`source: fasttrack`, `runner_ref: grv:<id>`)\nand every other state from the national results pages (`source: grsa`, `runner_ref:\ngrsa:<id>`). `origin` says which half of the store a run is in: `results` = a race we also\nsettled and serve on /v1/racing/results, `history` = a race our history collector read back\noff the same page for a date our settled store does not cover, whose `result_id` is a\nsynthetic `hist:\u2026` and joins to nothing. `source.coverage_from` is the oldest run actually\nheld and `source.results_from` the date the settled half begins, so `runs_total` is the dog's\nrecord over a stated window and not its career.\n\nIDENTITY. Pass a name and the runs are grouped on its letters and digits (\"Ando's Mac\" and\n\"ANDOS MAC\" are one query; the whole name must match). Our two collectors number dogs in\ndifferent registries, so a dog that races in Victoria and interstate has two refs; the name\njoins them and `runner_refs` lists every id seen. Pass a ref and you get that ref's runs \u2014\nplus, when the registry map has proved the two refs are one dog (one grv id, one grsa id,\ntrainers that agree or do not contradict), that dog's runs in BOTH registries. A name\nmatching nothing is a 404 and costs nothing.\n\nREADING A RUN. `position` is the finishing place and is null when the dog did not run (`status`\nsays `scratched` or `reserve`) or did not complete the course. `time_s` is the dog's own time\nand `win_time_s` the winner's in the same race. `sp` is the official starting price as\npublished with the result; null means unknown, never zero. `margin` is the source's own text:\nlengths behind the winner, and on Victorian tracks the winner's row carries its winning margin.\n`field_size` is how many dogs ran.\n\nTHE MEASURED COLUMNS, AND WHY THEY ARE OFTEN NULL. `first_split_s`, `second_split_s`,\n`weight_kg`, `pir` and `grade` are printed by the source pages and have been carried through\nsince 2026-09-21. Each is null wherever that page printed nothing, and the gaps are\nstructural, not random: FastTrack publishes one split and no second; PIR comes from\nFastTrack's own column on Victorian tracks and, since 2026-09-29, from the national page's In\nRace Positions card, which only some tracks print (see `pir`); Western Australian meetings\nmostly print no weight; a rug 9/10 reserve that never started carries no weight, split or PIR\nat all. Sampled on 2026-09-21 over\n46 freshly read race pages, a grade appeared on 46/46 races and 362/362 runner rows, a first\nsplit on 288/362 and a weight on 295/362. \u26a0\ufe0f A STORED FIELD IS NEVER REWRITTEN, so a run\nsettled before 2026-09-21 carries none of the five however well its page printed them \u2014 read\nthem per run, never as a property of the dog."
          },
          "response": []
        },
        {
          "name": "Greyhound record by track, distance, box or grade, from our own results",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/greyhounds/stats?dog=VELOCITY YIN&by=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "greyhounds",
                "stats"
              ],
              "query": [
                {
                  "key": "dog",
                  "value": "VELOCITY YIN",
                  "disabled": false,
                  "description": "Dog name or runner_ref, resolved exactly as on /v1/racing/greyhounds/form. (required)"
                },
                {
                  "key": "by",
                  "value": "",
                  "disabled": true,
                  "description": "Dimensions to group by: track, distance, box, grade. Comma-separate to cross them, e.g. track,distance or grade,track."
                }
              ],
              "variable": []
            },
            "description": "Cost: 3 credits. One greyhound's record from our own results, split by track, distance,\nbox, grade, or any combination \u2014 plus the same figures over every run, in `overall`.\n\nSOURCE. The same store as /v1/racing/greyhounds/form: the official results pages we read\nourselves, Victorian tracks via FastTrack and every other state via the national results\npages, rebuilt hourly, in two halves (`origin` on the form endpoint says which). These are\nfigures over `source.coverage_from`..`source.coverage_to` only, not a career.\n\nGROUPING. `by=track,distance` returns one row per track-and-distance pair the dog has started\nat; `by=box` one row per box; `by=grade` one row per grade. The dimensions you did not request\ncome back null on each row. Rows are ordered by starts, most first.\n\nGRADE (a dimension since 2026-09-29). Measured 2026-09-28 21:07 UTC: 6,217 of the 6,217 runs\nin the last seven days carry a grade (100.0%). It is the grade in the SOURCE'S OWN WORDS \u2014\n\"5\", \"M\", \"4/5\" on the national pages, a race type such as \"Grade 5\" or \"Mixed 4/5\" on\nFastTrack \u2014 and the two vocabularies are NOT mapped onto each other, so a dog that raced in\nVictoria and interstate shows a FastTrack row and a national row for what may be the same\nclass. Compare grade rows within one source. Older runs are covered too: history runs have\ncarried a grade from the start, and after the 2026-09-29 re-read of every settled race since\n2026-09-13 (whose pages were stored before the grade was collected) 100.0% of `ran` entries\ncarry one on every Sydney day from 2026-09-13 to 2026-09-28. A run without a grade would\ngroup under null rather than be dropped.\n\nTHE NUMBERS. `starts` counts runs with status `ran`; scratchings and unused reserves are\nexcluded from every figure, which is why `starts` is lower than `runs_total` on the form\nendpoint. `wins` is place 1, `places` a finish in the first three, both percentages over\n`starts`. `avg_time_s` and `best_time_s` are over the runs with a recorded time; `avg_sp` over\nthe runs that carry an official starting price; `avg_first_split_s` over the runs whose page\nprinted a first sectional, which is FEWER than `starts` \u2014 some tracks print none, so it is\nnot an average over the group you might assume. On a small group these describe a handful of\nruns, so read `starts` alongside them. A misspelt `by` is a 422 and costs nothing; a name\nmatching nothing is a free 404."
          },
          "response": []
        },
        {
          "name": "Thoroughbred form, career record and profile for one horse",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/horses/form?horse=Zweigelt",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "horses",
                "form"
              ],
              "query": [
                {
                  "key": "horse",
                  "value": "Zweigelt",
                  "disabled": false,
                  "description": "Horse name, the `pe:` horse_ref published on every thoroughbred surface ('pe:custo'), or the ra: entry code from runner_ref on results/live runners. Names match case- and punctuation-insensitively on the whole name, and a name and its horse_ref resolve identically. (required)"
                }
              ],
              "variable": []
            },
            "description": "Cost: 3 credits. One AU thoroughbred's recent form, career record and profile,\nparsed from Racing Australia's public horse page \u2014 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 \u2014 a 404, 502 or 503 costs nothing.\n\nRESOLVING `horse`. Pass a name, or the `pe:` horse_ref that every thoroughbred surface\nnow publishes \u2014 they resolve identically, because the horse_ref is the folded name. An\n`ra:` code from `runner_ref` is accepted too, but prefer the name or horse_ref: Racing\nAustralia issues a new code for the same horse each day. Names resolve against\nhorses seen on our own feeds: acceptances roughly three days ahead, plus every archived\nresult runner (from 2026-09-01) \u2014 and, failing both, our own run store and stored form\npages, so a horse that last raced months ago still resolves (2026-10-03). Where we hold\nruns but no readable RA page, those runs are the answer (`source.feed`\n'puntersedge_results'). A name we have never seen returns 404 even when RA knows the\nhorse \u2014 there is deliberately no name-search against RA itself.\n\nONE NAME, ONE HORSE. A name resolves to the horse, and the most recently seen of its Racing\nAustralia codes is read \u2014 a horse that has raced more than once holds several codes,\nbecause Racing Australia issues a new one each day. `ambiguous` is always false and\n`candidates` always empty; both stay on the response for compatibility. Two different\nhorses sharing one folded name cannot be told apart here; the AU register does not allow\ntwo ACTIVE horses to share one (a name is released about 17 years after its horse retires).\n\n`horse_ref` ON THE RESPONSE is that identity, `pe:<name>`, and it is the same value on\n/v1/racing/results runners and placings, the live board, the closing-line and price-path\narchives, /v1/racing/horses/runs and the backfill. `runner_ref` is the code the page was\nread under; it changes from day to day. Join a horse on `horse_ref`; join the rows about\none race on `runner_ref`.\n\nTHE STORE IS KEYED ON THE HORSE. A page collected under ANY of the horse's codes \u2014 by you,\nby another customer, or by the paced backfill \u2014 answers every later lookup of that horse.\n\nREADING A RUN. Runs come newest first: the page's recent-form table, typically the\nlast ten starts plus barrier trials (`trial: true` \u2014 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 \u2014 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 \u2014 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 enriched\nlive runners is the same identifier FOR ONE MEETING, so a pre-race model row joins to that\nday's official result and to the form read for it. Across meetings, join on `horse_ref`\ninstead \u2014 it is on all of those surfaces too and it does not change. `jockey_code` and\n`meet_code` are RA's own identifiers for the rider and the meeting, and those do persist.\n\nCached 10 minutes per query on top of the six-hour page store."
          },
          "response": []
        },
        {
          "name": "Queue a batch of horses for paced form collection",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/horses/backfill",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "horses",
                "backfill"
              ],
              "query": [],
              "variable": []
            },
            "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 \u2014 against acceptances (about\nthree days ahead) and every archived result runner \u2014 and so do the `pe:` horse_refs\npublished on results, the live board, the archives and /v1/racing/horses/runs, which is\nthe spelling to submit if you are working from those. A horse we have never seen comes\nstraight back in `not_found` rather than being queued, and costs nothing. `ambiguous` is\nalways empty: a name resolves to one horse (several Racing Australia codes under one name\nare the same horse's codes from different days, not several horses).\n\nHorses we already hold a fresh copy of skip the queue and are ready at once \u2014 matched on\nthe HORSE, so a page collected under any of its codes counts and no read of Racing\nAustralia is spent re-fetching it. A horse already 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 \u2014 you do not have to wait for the whole batch.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"horses\": [\n    \"HORSES\"\n  ],\n  \"label\": \"LABEL\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Progress of a backfill job",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/horses/backfill/:job_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "horses",
                "backfill",
                ":job_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "job_id",
                  "value": "YOUR_JOB_ID",
                  "description": "job_id path parameter"
                }
              ]
            },
            "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 \u2014 collection resumes when your\ncredits reset on the 1st, or immediately if you upgrade.\n\n`upstream` says whether Racing Australia is answering form reads at all. `state: walled`\nmeans RA is serving its bot-protection challenge to every fresh read, so NO horse can be\ncollected \u2014 by us or by anyone \u2014 until that stops; `walled_since` and `walled_hours` say\nhow long it has been going on, which is the difference between a queue that is slow and a\nqueue that is stopped. Your rows stay queued and unbilled throughout and collection\nresumes on its own; there is nothing to re-submit. While it lasts,\nGET /v1/racing/horses/runs answers from our own settled results instead and needs no\nRacing Australia read at all. The block is additive: every other field, and all billing\nand queue behaviour, is exactly as it was before 2026-09-21."
          },
          "response": []
        },
        {
          "name": "Cancel the horses a job has not collected yet",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/horses/backfill/:job_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "horses",
                "backfill",
                ":job_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "job_id",
                  "value": "YOUR_JOB_ID",
                  "description": "job_id path parameter"
                }
              ]
            },
            "description": "Cost: free. Drops this job's queued rows. Delivered horses stay collectable, and\nnothing already billed is re-billed."
          },
          "response": []
        },
        {
          "name": "Collect the form of horses this job has delivered",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/horses/backfill/:job_id/results?limit=&offset=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "horses",
                "backfill",
                ":job_id",
                "results"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "offset",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": [
                {
                  "key": "job_id",
                  "value": "YOUR_JOB_ID",
                  "description": "job_id path parameter"
                }
              ]
            },
            "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 \u2014 profile,\ncareer, runs \u2014 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.\n\n`horse_ref` (`pe:<name>`, added 2026-09-21) is the horse this row is about, and it is the\nsame value on /v1/racing/results runners and placings, the live board, the closing-line\nand price-path archives, /v1/racing/horses/form and /v1/racing/horses/runs \u2014 so a\ncollected batch joins straight onto your price rows without matching names. `runner_ref`\nbeside it is Racing Australia's code for the entry we read, which changes from meeting to\nmeeting and does not identify the horse."
          },
          "response": []
        },
        {
          "name": "One thoroughbred's runs in our own settled results",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/horses/runs?horse=Zweigelt&limit=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "horses",
                "runs"
              ],
              "query": [
                {
                  "key": "horse",
                  "value": "Zweigelt",
                  "disabled": false,
                  "description": "Horse NAME (matched on letters and digits, case- and punctuation-insensitive, whole name), or the `horse_ref` any thoroughbred surface publishes ('pe:custo') \u2014 either gathers the whole record, and they resolve identically. A Racing Australia horsecode from `runner_ref` ('ra:MjgwNjM0ODg2NDA' or the bare code) is accepted too, but it pins one results entry, usually one meeting's runs, not the horse. (required)"
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": "Most recent runs to return."
                }
              ],
              "variable": []
            },
            "description": "Cost: 3 credits. Every run we hold for one AU thoroughbred, newest first, out of our own\nsettled results.\n\nTHIS IS NOT A CAREER RECORD. Each run here is a race WE collected and settled: the same rows\n/v1/racing/results serves, flattened one-per-horse. The store begins at the first complete card\non `source.coverage_from` and `source.coverage_to` is the latest race in it, so `runs_total` is\nthis horse's record SINCE THAT DATE, not its lifetime. The same run never changes once written,\nso the record only grows \u2014 build a longer history forward from here.\n\nWHY YOU WOULD USE IT INSTEAD OF /v1/racing/horses/form. That endpoint is Racing Australia's own\nform page: deeper (career splits, sectionals, position-in-running, the price path to SP) but\ndependent on RA serving us the page. Since 2026-09-21 RA's edge answers every fresh form read\nwith a human-verification challenge, so horses we have not already stored cannot be read at\nall, and we do not automate that challenge. This endpoint has no such dependency \u2014 it reads\nonly what we already hold \u2014 and it is the honest answer to \"what do you have on this horse\ntoday\". Use both: form for depth when RA lets us in, runs for availability always.\n\nRESOLVING `horse` \u2014 PASS THE NAME, OR ITS `horse_ref`. A name is folded to letters and digits\n(the trailing country parenthetical stripped first) and must match in full \u2014 the same rule\n/v1/racing/horses/form applies, so one name resolves identically on both. `pe:custo`, the\n`horse_ref` published on every thoroughbred runner, placing, live-board entry and archive row,\nis the same fold already done and resolves to exactly the same runs. A Racing Australia\nhorsecode (`ra:MjgwNjM0ODg2NDA`, or the bare code) is accepted too, but it finds one run\nrather than a horse: Racing Australia issues a new code for the same horse each day, so a code\nfilter returns the one run that carries it. `runner_refs` lists every code published under the\nname; more than one is normal. The other side of grouping on a name is\nthat a registered name reused by two horses returns the union of both. A horse we hold no run\nfor is a 404 and costs nothing; an unknown query parameter is a 422 and costs nothing.\n\nREADING A RUN. `position` is the finishing place, null when the horse did not start (`status`\nis `scratched`) or started and did not finish \u2014 `abnormal` is then RA's own reason cell,\nverbatim and from no closed list. `sp` is the official starting price and `sp_note` RA's\nfavouritism marker; null `sp` means unknown, never zero. `margin` is RA's own text in lengths.\n`time_s` is the WINNER's time only: RA prints one time per race, in its race header, and no\nper-runner time in the results table, so since 29 September 2026 a position-1 run carries the\nrace time and every other run's `time_s` is null. Every run of a race also carries the race-level\n`race_time_s` and `last_600m_s` (RA's header, seconds) and `prize_total`, and each placed run its\n`prize_won` off RA's prize line (dead heats share the places they tie for; 0 outside the paid\nplaces); all four are null on runs stored before that date until the meeting is re-read, and\nbefore 29 September no run carried any time (0 of 17,344 stored runs, measured 2026-09-28).\n`trainer` and `jockey` are the ones of that day, which is\nwhat the result published \u2014 `profile.trainer` on the form endpoint is the CURRENT trainer.\n`field_size` counts the horses that ran. `weight_kg` is the allotted weight and `carried_kg`\nthe weight actually carried when RA printed one (a claim under it, or an overweight rider over it); `race_class` is RA's own leading\nconditions term ('Maiden', 'BenchMark 64'). All three are kept since 28 September 2026. On\n`origin: tabtouch` runs, and on runs from 10 August to 27 September 2026 whose result lacked\nthem, `race_class` and `weight_kg` come from TABtouch's race page instead: its own class wording\n('BM58 HCP', 'MDN-SW') and its printed weight, with `carried_kg` null. `result_id` joins back to /v1/racing/results, except on `origin: history` and `origin: tabtouch` runs, whose synthetic 'hist:...' id joins to nothing.\n\n`stats` is computed over every held run, not just the `limit` returned, and counts only starts\n(`status: ran`).\n\nCached 10 minutes per query; the store itself refreshes hourly at :59."
          },
          "response": []
        },
        {
          "name": "Every runner in one race with its recent runs, from our own results",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/form?race_id=&venue=&meeting_date=&race_number=&runs=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "form"
              ],
              "query": [
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "The race_id from /v1/racing/next-to-go, /events, /acceptances or /results. The primary form; use venue + meeting_date + race_number only when you do not hold it."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue name as the feeds spell it (case-insensitive). Needs meeting_date and race_number."
                },
                {
                  "key": "meeting_date",
                  "value": "",
                  "disabled": true,
                  "description": "AET meeting date, YYYY-MM-DD."
                },
                {
                  "key": "race_number",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "runs",
                  "value": "",
                  "disabled": true,
                  "description": "Most recent runs to return per runner. The stats cover every held run regardless."
                }
              ],
              "variable": []
            },
            "description": "Cost: 3 credits. Every runner in one race with its recent runs from our own settled AU\nthoroughbred results, in one call: the field's form, not one horse's.\n\n**Naming the race.** `race_id` from /v1/racing/next-to-go, /events, /acceptances or /results is\nexact. Failing that, `venue` + `meeting_date` (the AET meeting date) + `race_number`; two races\nsharing those three answer 409 `race_ambiguous` and cost nothing. A race a bookmaker has not\nframed yet has no race_id (every AU book frames a race 16 hours before its jump), so the evening\nbefore a meeting name it by venue + meeting_date + race_number exactly as /v1/racing/acceptances\nlists it: the answer comes from Racing Australia's acceptance card (`card_source: acceptances`,\n`race_id` null), and the whole of tomorrow's card can be read the night before.\n\n**The field.** While the race is open the runners come from the live card (`card_source: live`):\nsaddlecloth, barrier, jockey, trainer and allotted weight as the books and Racing Australia's\nacceptances publish them, with a horse the card has since scratched kept in the field and marked\n`scratched: true`. Once the race has been run and the live card purged, the field is the settled\nresult's (`card_source: results`). Until then Racing Australia's acceptance card is folded in\n(`acceptances_folded`): every accepted runner, emergencies aside, is in the field even before the\nbooks list it, named as RA names it, with RA's `last10` and `last_start_date`.\n\n**The runs.** Under each runner, its newest `runs` runs (default 10, at most 20) out of the same\nstore /v1/racing/horses/runs reads -- our own collected results plus the history collected behind\nthem -- and a starts, wins and places summary over every run held. Each run carries date, venue,\nrace, distance, class, track condition, saddlecloth, barrier, position, margin, official SP,\nweight, jockey, trainer and field size, and since 29 September 2026 the race's winning time and\nlast-600m time, its total prize money and the place money this runner won (the winner's\n`time_s` is the race time; RA publishes no other runner's time). ONLY RUNS BEFORE THIS RACE'S START are included\n(`form_as_at`), so a race already run answers with what was knowable at the jump and never with\nits own result. A runner with no run held is still in the field, with `runs: []` and\n`runs_total: 0`; `source.coverage_from` is the floor of the store, and nothing earlier exists here.\n\n**Identity.** Runners are gathered on `horse_ref` (`pe:<name>`), the fold every thoroughbred\nsurface publishes. `runner_ref` is the Racing Australia code the card carries for this runner,\non race day the join to /v1/racing/next-to-go, /v1/racing/acceptances and /v1/racing/results;\nit is not a horse identity, because Racing Australia issues a new code for the same horse each day.\n\nNot a career record: this is what we hold, honestly bounded, and it grows with every meeting we\ncollect. For Racing Australia's own form page (career splits, sectionals, price paths) use\n/v1/racing/horses/form, one horse at a time. A race we cannot find is a free 404, a race we hold\nno field for a free 404, an unknown query parameter a free 422. Thoroughbreds only; greyhounds\nhave /v1/racing/greyhounds/form. Cached 10 minutes; the store refreshes hourly."
          },
          "response": []
        },
        {
          "name": "Jockey premiership leaderboards and lookup",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/jockeys/stats?name=&state=&scope=&season=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "jockeys",
                "stats"
              ],
              "query": [
                {
                  "key": "name",
                  "value": "",
                  "disabled": true,
                  "description": "Find one jockey by (partial) name across every state and scope."
                },
                {
                  "key": "state",
                  "value": "",
                  "disabled": true,
                  "description": "AUS (national) or a state. Default AUS, unless `name` widens the search."
                },
                {
                  "key": "scope",
                  "value": "",
                  "disabled": true,
                  "description": "all, metro, provincial, country, picnic. Default all, unless `name` widens the search."
                },
                {
                  "key": "season",
                  "value": "",
                  "disabled": true,
                  "description": "current or previous RA season (1 Aug - 31 Jul)."
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits. Jockey premiership leaderboards \u2014 1st-5th finishes, prize\n    money, strike rate and starts \u2014 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 \u2014 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 \u2014 a rider prominent only in one\n    state's country scope is found without knowing which \u2014 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 \u2014 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 \u2014 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."
          },
          "response": []
        },
        {
          "name": "Trainer premiership leaderboards and lookup",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/trainers/stats?name=&state=&scope=&season=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "trainers",
                "stats"
              ],
              "query": [
                {
                  "key": "name",
                  "value": "",
                  "disabled": true,
                  "description": "Find one trainer by (partial) name across every state and scope."
                },
                {
                  "key": "state",
                  "value": "",
                  "disabled": true,
                  "description": "AUS (national) or a state. Default AUS, unless `name` widens the search."
                },
                {
                  "key": "scope",
                  "value": "",
                  "disabled": true,
                  "description": "all, metro, provincial, country, picnic. Default all, unless `name` widens the search."
                },
                {
                  "key": "season",
                  "value": "",
                  "disabled": true,
                  "description": "current or previous RA season (1 Aug - 31 Jul)."
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits. Trainer premiership leaderboards \u2014 1st-5th finishes, prize\n    money, strike rate and starts \u2014 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 \u2014 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 \u2014 a rider prominent only in one\n    state's country scope is found without knowing which \u2014 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 \u2014 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 \u2014 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."
          },
          "response": []
        },
        {
          "name": "Races and prices that changed since a timestamp",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/changes?since={{$isoTimestamp}}&categories=&category=&country=&include_unresolved=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "changes"
              ],
              "query": [
                {
                  "key": "since",
                  "value": "{{$isoTimestamp}}",
                  "disabled": false,
                  "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. (required)"
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "horse, greyhound, harness \u2014 comma-separated. Omit for all three. An unknown value is refused rather than silently ignored."
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `categories`. Both spellings are accepted on every racing endpoint. An unknown value is a free 422 naming the valid ones."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 pair with include_unresolved=true or you will lose most of an Australian card."
                },
                {
                  "key": "include_unresolved",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 58.8% of horse races in a measured window \u2014 so country=AU alone can return almost nothing."
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits. Everything that moved on the live racing board since `since` \u2014 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 \u2014 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 \u2014 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**Betslip links, Plus and above** (since 21 September 2026). A `changed_runners` entry\nfrom a book with a runner-level betslip route (Unibet and TAB on the web, Ladbrokes and\nNeds in their apps) also carries `betslip_win_url`, `betslip_place_url` and\n`betslip_app_only` - the same fields, the same per-book rules and the same daily check as\n/v1/racing/next-to-go - so a poller holds the move and the link that acts on it in one\nobject. The fields are absent on every other book, and on plans below Plus (a Standard or\nHobby key issued before that date keeps them). `source_url` is not carried here; take it\nfrom next-to-go.\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."
          },
          "response": []
        },
        {
          "name": "Track condition, weather and rail: current state and today's change log",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/track-conditions?date=&venue=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "track-conditions"
              ],
              "query": [
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Meeting day, Australia/Sydney, YYYY-MM-DD. Defaults to today. Any day from the store's first day (`coverage_from` in the response; an earlier date is a free 422 naming it) up to 3 days ahead."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "One meeting, case-insensitive ('Belmont', 'sandown hillside'). Omit for every meeting on the day."
                }
              ],
              "variable": []
            },
            "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 \u2014 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 \u2014 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'); both spellings are 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` \u2014 the\ninstant of the latest logged transition for that race's meeting \u2014 so a poller reading\n/v1/racing/changes need not call this to know something moved; call this for the\nsequence.\n\n**How far back** (changed 2026-09-29). Any day from `coverage_from` \u2014 the first meeting day\nthe store holds, read from the tables on every call \u2014 up to three days ahead. Two floors sit\nunder it and the response names both: the change log reaches back to 2026-09-07, while\nRacing Australia's own meeting block (`official_block_from`) is held only from 2026-09-26,\nbecause until 2026-09-29 it was deleted three days after each meeting; it is kept\npermanently since. On a day between the two, `current` and `changes` come from the log\nalone \u2014 RA did publish that day, its block is simply gone. A `date` before `coverage_from`\nis a free 422 whose detail names the floor, and so is a malformed one."
          },
          "response": []
        },
        {
          "name": "Full-day AU thoroughbred acceptance card: every meeting, race and runner",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/acceptances?date=&venue=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "acceptances"
              ],
              "query": [
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Meeting day, Australia/Sydney, YYYY-MM-DD. Defaults to today. Any day from the store's first day (`coverage_from` in the response; an earlier date is a free 422 naming it) up to 3 days ahead."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "One meeting, case-insensitive ('Moe', 'sandown hillside'). Omit for every meeting on the day."
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits. The day's complete AU thoroughbred card as Racing Australia\npublishes it \u2014 meeting, then race, then runners in saddlecloth order \u2014 with no\ndependency on any bookmaker having framed a market. Per runner: saddlecloth number,\nbarrier, jockey, trainer, weight, `runner_ref` (Racing Australia's code from its latest read\nof the field; once race day's read has landed it is the code the results and archives carry\nfor the race \u2014 RA issues a new code for the same horse each day), 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 \u2014 as RA's raw\n`track_condition` and, parsed from it, `going` plus `going_rating`, the same pair\n/v1/racing/horses/form publishes \u2014 weather, penetrometer, its own `as_of`,\n`emergency_data`, `scratching_data` and `withheld_acceptance_data`.\n\n**Identity (since 2026-10-12).** Every runner carries `horse_ref`, `pe:<folded name>`:\nthe HORSE, the same value on /v1/racing/form, /v1/racing/horses/form and\n/v1/racing/horses/runs, stable across dates and meetings. `runner_ref` is Racing\nAustralia's code for THIS nomination and is new every time the horse is entered; join\nhistories on `horse_ref`, never on `runner_ref`.\n\n**Fields-page extras (since 2026-09-29).** Every runner also carries what Racing\nAustralia prints under the acceptance table for that horse, verbatim and never inferred:\n`age`, `sex`, `colour`, `sire`, `dam` (without the dam's sire), `breeder`, `owner` (RA's\nOwners line), `gear_changes` for this start (\"Tongue Tie OFF FIRST TIME\"), the apprentice\n`claim_kg` read off the jockey cell (\"(a3/52kg)\" -> 3; 0 is an apprentice who no longer\nclaims), `probable_weight_kg` and `penalty_kg` from their own columns, and the horse's LAST\nSTART \u2014 the most recent dated start that is not a trial or jump-out: `last_start_date`,\n`last_start_position`, `last_start_jockey`, `last_start_weight_kg`, `last_start_barrier`,\n`last_start_winner` (the horse itself when it won) and `last_start_rating`, the \"Rtg\" RA\nprints on that start's line. Null where RA prints nothing; null on every row stored before\n2026-09-29 until the meeting is re-read. MEASURED on the first pass that read them\n(2026-09-28 20:58Z, 10 meetings, 501 acceptances): pedigree/breeder/owner on 501 (100%), a\nlast start on 423 (84%), a last-start Rtg on 261 (52%), a claim on 107, gear changes on\n111. `last_start_rating` is NOT a substitute for `hcp_rating`: of the runners with no Hcp\nRating, 5 carried a last-start Rtg on that pass (0 of 43 on the saved Moe card of\n2026-09-08) \u2014 the unrated are overwhelmingly horses whose last start printed no Rtg either.\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 \u2014 /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 collected before\nemergency acceptances were recorded (2026-09-08 01:01:39 UTC) \u2014 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 \u2014 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 \u2014 and each\nmeeting's sheet is collected separately, so one meeting can be missing its sheet while\nits acceptances are present. A row from the sheet is the only positive\nevidence it was read, so each meeting publishes `scratching_data`:\n  * `\"parsed\"` \u2014 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\"` \u2014 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\"` \u2014 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 \u2014 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` \u2014 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 \u2014 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 \u2014 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 \u2014 a horse that may well be jumping \u2014 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 \u2014 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: the horse appears in neither race, not in one of them.\n**Do not de-duplicate by `runner_key`\nto recover them \u2014 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 \u2014 for saddlecloth holes, against\nthe independently-keyed scratching sheet where one exists, and against the withheld\nslots recorded as withheld when the card was collected \u2014 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 \u2014\n`duplicate_runner_key` (the collision is visible, not assumed: the same runner_key is\nheld elsewhere on this card, or the slot was recorded as withheld),\n`declared_preferred` (a declared acceptance elsewhere on the day won the key and this\nrace's emergency slot was dropped \u2014 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` \u2014 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 \u2014 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 \u2014 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 \u2014 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) \u2014 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\". Acceptances are collected for today and forward only, 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) \u2014 calling again cannot help,\nand the meeting's `publication_note` says so; and the scratchings are frozen at the\nlast collection pass of that day, roughly midday race-day, with no later movement recoverable\nfrom anywhere, /v1/racing/changes included. On \"today\" or \"future\" the next collection pass CAN\nadd both, every two hours \u2014 but that is not a promise that the card converges. Racing\nAustralia stops listing a meeting once\nit is UNDERWAY, 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 \u2014 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 data only updates every two hours \u2014 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 \u2014 not empty, absent. History is kept from 2026-09-29 on:\nuntil then the reference tables were deleted three days after each meeting, so the store\nbegins at the oldest day that survived (2026-09-26) and grows by a day each day.\n`coverage_from` is that first day, read from the tables on every call; any `date` from it\nup to three days ahead is served, and an earlier one is a free 422 naming the floor rather\nthan a truthful-looking empty card. A past day is the card as the LAST read of that day\nleft it \u2014 see the past-date paragraph above.\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 \u2014 nothing is charged."
          },
          "response": []
        },
        {
          "name": "Score bets against the closing line (CLV)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/clv",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "clv"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 5 credits per request, up to 200 bets. Score a list of bets you struck against the\nclosing line each race actually closed at, and get the record back as numbers: per bet the\nclosing price (the named book's, or the market's best or median), the closing-line value in\npercent, whether it beat the close, the result and the flat-stake P&L; and across the list\nthe average and median CLV, the beat-the-close rate, the win rate and the ROI.\n\n**Naming a race.** Send the `race_id` you already hold from next-to-go, events, results or\nthe archive; failing that, `venue` + `meeting_date` (AET) + `race_number`. **Naming a\nrunner.** `runner_ref` (the ra:/grv: registry id on results and the archive) is exact;\n`runner_number` is the saddlecloth; `runner_name` is matched the way the feeds match names.\n\n**What you are measured against.** A closing line is a book's last price within 300 s of\nthe jump. Name a `bookmaker` and `clv_pct` is against that book's close (`close_book`);\notherwise against the market `reference` \u2014 `best` (the highest close across books, the\nstrictest test) or `median`. `close_best` and `close_median` come back on every bet\neither way, so you can re-cut the record yourself.\n\n**Coverage.** The archive holds AU and NZ races from `archive_floor` onward; a bet before\nthat, or on a race no book priced at the close, resolves with a `reason` and is left out\nof the averages rather than guessed. Results are settled only when `result_status` is\n`final`; `won` and `pnl` are null until then. Betfair exchange prices are withheld.\n\nPlus and above. Free and paid tiers below Plus receive a 403 and are not charged; the 403\ncarries a `sample`: five example bets on one recent race, scored the way yours would be.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"bets\": [\n    {\n      \"bookmaker\": \"sportsbet\",\n      \"price\": 4.2,\n      \"race_id\": \"au-flemington-2026-09-19-r5\",\n      \"ref\": \"bet-1\",\n      \"runner_name\": \"Example Horse\",\n      \"stake\": 50\n    },\n    {\n      \"meeting_date\": \"2026-09-19\",\n      \"price\": 6.0,\n      \"race_number\": 7,\n      \"ref\": \"bet-2\",\n      \"runner_number\": 3,\n      \"venue\": \"Randwick\"\n    }\n  ],\n  \"reference\": \"best\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Every market on one race: win, place, Top 2/3/4 and exotic dividends",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/markets?race_id=&venue=&meeting_date=&race_number=&bookmakers=&markets=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "markets"
              ],
              "query": [
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "The race_id from /v1/racing/next-to-go, /events, /results or the archive. The primary form; use venue + meeting_date + race_number only when you do not hold it."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue name as the feeds spell it (case-insensitive). Needs meeting_date and race_number."
                },
                {
                  "key": "meeting_date",
                  "value": "",
                  "disabled": true,
                  "description": "AET meeting date, YYYY-MM-DD."
                },
                {
                  "key": "race_number",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys to restrict to. Omit for every book quoting the race."
                },
                {
                  "key": "markets",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated subset of win, place, top2, top3, top4, exotics. Omit for all of them."
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits. One race, every market the panel quotes on it, book by book: the win\nprice, the place price, the Same Race Multi legs (**Top 2, Top 3 and Top 4**), each book's\nmarket percentage, the best price per runner in each market, and \u2014 once the race has been\nrun \u2014 the exotic dividends it paid.\n\n**Naming the race.** `race_id` is exact. Failing that, `venue` + `meeting_date` (the AET\nmeeting date) + `race_number`; two races sharing those three answer 409 `race_ambiguous`\nand cost nothing, because picking one of a venue split silently would be worse than\nrefusing.\n\n**Who quotes what.** Every book in the panel prices win, and all but the exchange price\nplace. Top 2/3/4 come from **sportsbet, ladbrokes_au, neds and pointsbetau** \u2014 no other\nbook in the feed publishes Same Race Multi legs, so an empty `top3` is a fact about the\nmarket and not a gap in the feed. Each leg is validated against the chain\nwin >= top2 >= top3 >= top4 > 1.0 before it is stored, so a leg that would break it is\nabsent rather than wrong.\n\n**overround_pct** is \u03a3 (1/price) \u00d7 100 over the runners that book quoted in that market \u2014\n100% is a book taking no margin. It is null where a book quoted fewer than two runners,\nwhich is not a market. Place and Top-N percentages are naturally far above 100 because\nseveral runners can win the bet.\n\n**Live or stored.** A race whose market is still open is read live (`source: \"live\"`). Once\nit has been run the live rows are purged, and the answer comes from the minute-by-minute\ncapture of the last price each book showed before the jump (`source: \"closes\"`). Races that\njumped before that capture began carry no market rows and say so in `note`.\n\n**exotics** are SETTLED DIVIDENDS \u2014 what the tote paid \u2014 not prices. Nothing in this feed\nquotes fixed-odds quinella, exacta, trifecta or first four before a race, so `exotics` is\nnull until the race is resulted, and then carries the result's own `status`: `interim`\ndividends can still move, `final` cannot.\n\n**quote_detail** (since 2026-09-29) lists, per book and runner, what the book publishes\nbeside its prices: `open_price` (the book's own opening price \u2014 not the archive's first\ncaptured price), `flucs` (its recent prices, most recent first), `tote_win` / `tote_place`\n(its tote dividends keyed by product code; `PROV` is an indicative pre-race dividend that\nmoves until the pool closes), `boost_price` / `boost_type`, and the exchange fields. Each\nentry names `bookmaker`, `number`, `name`, `runner_ref` and `seen_at` \u2014 when the book was\nlast read \u2014 and only the fields that book carries. Who carries them, measured on the AU\nboard of 2026-09-29 before its first race (20 races, 2,219 book-runner rows): flucs at\nbetdeluxe, betgold, boostbet and betright on every row, open_price at betright on every\nrow, tote_win at tabtouch on 89 of 194 rows and tote_place on 26 (a provisional dividend\nappears as the pool forms), no boost that morning. The exchange fields are Betfair's, and\nBetfair is withheld from customer answers. For a race that has been run the values are\nthe last read before the jump.\n\n**scratchings** (since 2026-09-29) are the race's scratched runners \u2014 `name`, `number`,\n`barrier`, `scratched_at` (the scratching book's own time, UTC) and `vacant` for an empty\nbox \u2014 from the open race while it is live, and once it has been purged from the capture\ntaken at the jump. A race run before 2026-09-29 answers an empty list after its purge. A\nscratching no Australian bookmaker has reported, while two or more of them still price the\nrunner, is held back: the runner stays in the markets until a bookmaker reports it.\n\n**tote_series** (since 2026-10-06) is each book's tote approximate as the pools formed:\none entry per book and runner, with points at fixed marks before the jump \u2014 3600, 1800,\n900, 600, 300, 180, 120 and 60 seconds \u2014 each the first read the minute-by-minute capture\nsaw at or inside its mark, carrying `mark_seconds`, `read_at`, `secs_to_jump` and\n`tote_win` / `tote_place` by product code. TABtouch publishes a provisional dividend\n(`PROV`) for every runner hours out; BetRight its products in the final quarter hour. A\nrace first seen inside a mark has no earlier points: nothing is filled in. The last read\nbefore the jump stays in `quote_detail`.\n\n**book_scratchings** (since 2026-10-06) says, book by book, when a scratching reached each\nfeed: `reported_at` is the first time that book's own feed listed it, `book_scratched_at`\nthe scratch time the book itself states (Ladbrokes, Neds, BetRight and the BetMakers books\ncarry one), and `first_quoted_at` / `last_quoted_at` with `last_quoted_win` the polls, to\nthe minute, at which that book's feed still carried a win price for the runner after the\nrace's scratchings named it. Ladbrokes and Neds keep a scratched runner's last price on the\ncard beside their own flag, so for a book that flags it the price is recorded once, at the\nflag: its price at the scratching. A book that carries a price with no flag is recorded\neach minute until it clears the runner. None of these prices is served as a price: a\nscratched runner is dropped from every price endpoint.\n\nPlus and above, and Standard keys issued before 20 September 2026. Every other key below\nPlus receives a 403 and is not charged; the 403 carries a `sample`: the winner of one\nrecent race, its best price in each market and the book that offered it."
          },
          "response": []
        },
        {
          "name": "The market consensus on one race: margin-free probability per runner",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/consensus?race_id=&venue=&meeting_date=&race_number=&method=&bookmakers=&min_books=&include_stale=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "consensus"
              ],
              "query": [
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "The race_id from /v1/racing/next-to-go or /events. The primary form; use venue + meeting_date + race_number only when you do not hold it."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue name as the feeds spell it (case-insensitive). Needs meeting_date and race_number."
                },
                {
                  "key": "meeting_date",
                  "value": "",
                  "disabled": true,
                  "description": "AET meeting date, YYYY-MM-DD."
                },
                {
                  "key": "race_number",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "method",
                  "value": "",
                  "disabled": true,
                  "description": "How each book's margin is removed before the books are combined. `power` (default) raises the implied probabilities to the exponent that makes them sum to one, which takes more margin off the longshots and is calibrated across the price range. `multiplicative` divides them by their sum and reproduces from the published overround."
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys to build the consensus from. Omit for every visible book."
                },
                {
                  "key": "min_books",
                  "value": "",
                  "disabled": true,
                  "description": "Included books a runner needs before it carries a consensus probability."
                },
                {
                  "key": "include_stale",
                  "value": "",
                  "disabled": true,
                  "description": "Count books whose quote is older than their refresh-tier threshold. Off by default; they are listed under books_excluded with reason stale."
                }
              ],
              "variable": []
            },
            "description": "Cost: 3 credits. The market's consensus on one race while its market is open: for\nevery runner, the implied probability after each book's margin is removed and the books\nare combined, the price that probability corresponds to, the median, mean, best and\nworst price, and how far the best available price sits from the consensus. The inputs\nare printed beside the answer \u2014 which books were counted, which were left out and why.\n\n**Naming the race.** `race_id` from /v1/racing/next-to-go or /v1/racing/events is exact.\nFailing that, `venue` + `meeting_date` (the AET meeting date) + `race_number`; two races\nsharing those three answer 409 `race_ambiguous` and cost nothing.\n\n**How the number is made.** Each included book's win prices on the field are turned into\nimplied probabilities (1 / price) and its margin removed \u2014 `power` (default) raises them\nto the exponent that makes them sum to one, `multiplicative` divides them by their sum\n\u2014 so every book contributes a field that sums to 1. The consensus for a runner is the\nMEDIAN of those per-book probabilities, and the medians are scaled once so the field\nsums to 1 again; `market.consensus_sum_before_normalisation` shows how far from 1 they\nwere before scaling. Every included book counts once. Nothing is weighted by an opinion\nof which book is sharp.\n\n**Which books count.** A book is included when it is visible to you (the exchange and the\nnon-Australian reference books are withheld here as on every racing endpoint), its quote\nis inside its refresh-tier threshold (the same per-book rule next-to-go publishes: 120 s\ninside three hours of the jump, 1800 s on the morning card, with Betr and NextBet on\ntheir own horizons), and it prices the complete field \u2014 `market.field_size` is the number\nof runners a complete-field book quotes, and a book pricing fewer is `partial_field`.\nPlaceholder prices are dropped quote by quote on the rule /v1/racing/best-odds applies\n(a long price at least four times the median of three or more other books) and listed in\n`excluded_quotes`. Scratched runners are dropped and listed under `scratchings`.\n\n**Reading `consensus_prob`.** It is the market's implied probability after margin, not a\nprobability of winning and not a fair price. `best_vs_consensus_pct` is\nbest_price / consensus_price \u2212 1: negative is normal, because every quoted price carries\nmargin and the best quote usually sits below the panel's margin-free price (about \u221220 %\non a 130 % greyhound market); it turns positive only when one book prices a runner above\nthe panel's margin-free view. It says where the best quote sits, nothing more. The\nfields a reader would use to judge how much to trust a figure \u2014 `books`, the spread, the\nages, `secs_to_jump` \u2014 are separate on purpose; there is no single confidence score.\n\n**Live only.** The consensus is read from the open market. A race that has left the live\nboard answers 404; the consensus at the close will arrive as columns on the permanent\narchive, where it belongs. A race inside the ten minutes after its jump is still served\n(`status: jumped`) while the books drop off, with a note.\n\nEvery plan, Free included. 3 credits, charged after the race is found; a race we cannot\nfind, an unparseable parameter and an unknown bookmaker key are free."
          },
          "response": []
        },
        {
          "name": "Market intelligence on one race: consensus at fixed marks, time-anchored moves, breadth, first mover, signal",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/intelligence?race_id=&venue=&meeting_date=&race_number=&min_books=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "intelligence"
              ],
              "query": [
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "The race_id from /v1/racing/next-to-go or /events."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue as the feeds spell it; needs meeting_date and race_number."
                },
                {
                  "key": "meeting_date",
                  "value": "",
                  "disabled": true,
                  "description": "AET meeting date, YYYY-MM-DD."
                },
                {
                  "key": "race_number",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "min_books",
                  "value": "",
                  "disabled": true,
                  "description": "Included books a runner needs before it carries a consensus at a mark."
                }
              ],
              "variable": []
            },
            "description": "Cost: 3 credits. One race on the live board: for every runner, the market consensus\nprice now and at fixed marks before the jump (T-60, T-30, T-15, T-10, T-5, T-2), its high\nand low since capture began, movement measured against those marks and against the last\nfive and two minutes, the number of ticks in the last five minutes, the best price against\nthe consensus, how many books moved with the headline, the first attributable book to move,\nand a signal class whose rule is printed in the response.\n\n**Naming the race.** `race_id` from /v1/racing/next-to-go or /v1/racing/events is exact.\nFailing that, `venue` + `meeting_date` (AET) + `race_number`; a venue split answers 409 and\ncosts nothing.\n\n**Time-anchored, not open-anchored.** Every movement figure compares the consensus NOW with\nthe consensus at a fixed time, computed by the same arithmetic as GET /v1/racing/consensus\nand the archive's consensus columns: each book's win prices with its margin removed, the\nmedian across books, the field scaled to one. `move_from_t60_pct` is negative for a runner\nthat shortened. A mark is null when the race had not entered the capture window by then \u2014\ncapture begins 60 minutes before the jump, so a race further out than that carries `now`\nonly and `window.open` is false.\n\n**Breadth.** `with_history` counts books quoting now with two or more captured prices on\nthe runner; `firming`, `drifting` and `unchanged` partition them by each book's own price\nagainst its price at T-60 (or its first tick, when it joined later). A headline move that\nmost of `with_history` did not make is one book repricing, not the market.\n\n**First mover** names the earliest attributable book to move at least 5 % in the headline\ndirection, with its lead over the next book and the median lag of the followers. It is\n`undetermined` when fewer than two attributable books moved or when the lead is inside the\n30-second attribution floor (the books are polled every 8\u201317 seconds). Books whose\ntimestamps cannot order a move are excluded and named in `method`: push-fed books (the stamp\nis the receipt, not the read) and books polled slower than the floor.\n\n**Signal** is one of `strong_firm`, `firm`, `steady`, `drift`, `strong_drift` or\n`insufficient`, from `move_from_t60_pct` and the breadth, by the rule in `method.signal_rule`.\nIt is a classification of what the market did, not a prediction; its strike rate against\nresults will be published on the market report once the archive carries it.\n\n**Live only.** A race that has left the live board answers 404; a race inside the ten\nminutes after its jump is still served while the books drop off, with a note.\n\nEvery plan, Free included. 3 credits, charged after the race is found."
          },
          "response": []
        },
        {
          "name": "Scan the live window for runners that moved: time-anchored, breadth-gated",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/intelligence/scan?window=&min_move_pct=&min_books=&direction=&signal=&country=&categories=&limit=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "intelligence",
                "scan"
              ],
              "query": [
                {
                  "key": "window",
                  "value": "",
                  "disabled": true,
                  "description": "Which movement gates the result: 60m (move_from_t60_pct), 15m (move_from_t15_pct) or 5m (move_last_5m_pct)."
                },
                {
                  "key": "min_move_pct",
                  "value": "",
                  "disabled": true,
                  "description": "Minimum absolute consensus move, in percent."
                },
                {
                  "key": "min_books",
                  "value": "",
                  "disabled": true,
                  "description": "Minimum books with history on the runner (breadth)."
                },
                {
                  "key": "direction",
                  "value": "",
                  "disabled": true,
                  "description": "firming or drifting; omit for both."
                },
                {
                  "key": "signal",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated signal classes to keep, e.g. strong_firm,firm."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "description": "Country code, e.g. AU or NZ."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated subset of horse, greyhound, harness."
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "description": "Cost: 5 credits. Every race inside the 60-minute capture window, analysed exactly as\nGET /v1/racing/intelligence analyses one race, with the runners that cleared the filters\nreturned largest move first. This is the time-anchored, breadth-gated form of\n/v1/racing/movers, and /movers keeps serving unchanged.\n\n`window` selects the movement figure the gate applies to: `60m` for the move since the\nT-60 consensus, `15m` since T-15, `5m` over the last five minutes \u2014 the late-window\nfigure a last-minute mover product wants. `min_books` is the breadth gate: a runner needs\nthat many books with two or more captured prices, so a single book repricing cannot clear\nit. `signal` keeps only the named classes. The response carries the race header beside\neach runner so nothing has to be joined.\n\nServed from a 15-second cache shared by every caller with the same filters; `cached`\nsays so and `as_of` is the moment the scan was computed.\n\nPlus and above. 5 credits. An unknown parameter value is a free 422."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Racing Archive",
      "item": [
        {
          "name": "Permanent closing-line and result archive",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/closing-lines?from=&to=&date=&venue=&bookmakers=&category=&categories=&country=&race_id=&closing_only=&resulted_only=&include_flagged=&include_scratched=&format=&limit=&offset=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "closing-lines"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time on the race start. Defaults to the archive floor."
                },
                {
                  "key": "to",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time. Defaults to now."
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue name, case-insensitive, exact match."
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing."
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "horse, harness or greyhound \u2014 comma-separated for more than one. `categories=` is accepted as an alias (2026-09-24); an unknown value is a free 422."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `category`. Comma-separated horse, harness, greyhound. Both spellings are accepted on every racing endpoint."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "description": "Two-letter country code, case-insensitive; comma-separated for more than one, e.g. AU, or GB,IE for British and Irish racing."
                },
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "Single race, joinable to /v1/racing/price-history while that race is still inside the 45-day snapshot window."
                },
                {
                  "key": "closing_only",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "resulted_only",
                  "value": "",
                  "disabled": true,
                  "description": "Only rows from races that have a result."
                },
                {
                  "key": "include_flagged",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "include_scratched",
                  "value": "",
                  "disabled": true,
                  "description": "Include runners the result marked scratched. False by default: four books keep pricing a scratched runner, so the archive holds a \"close\" for a runner that never started (rows carry scratched=true). Only AU thoroughbred and greyhound results name their scratchings, so on harness and NZ rows scratched=false means not known, never ran."
                },
                {
                  "key": "format",
                  "value": "",
                  "disabled": true,
                  "description": "csv streams a flat table with a stable column order \u2014 the format a modeller actually wants."
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "offset",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "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.** Plus and above read the whole archive back to its first race; `window_days`\ncomes back null for them. Standard reads a rolling 90-day window: `window_days` is 90,\nthe response carries `X-Archive-Window-Days`, and a range entirely older than the window\nis a free 403 that says so. Free and Hobby get a free 403 that carries a `sample`: five\nof this endpoint's rows, for one recent race. Standard keys issued before\n2026-09-20 keep the whole archive they were sold; the `window_days` field tells you\nwhich case yours is. Legacy `starter`, `pro` and `growth` keys keep their access but\ncannot be bought.\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` \u2014 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` \u2014 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.** Before 2026-10-04 harness and NZ\n  results named the placegetters only, so most runners in a resulted race there have no\n  published position; AU harness and NZ results carry the full field from 2026-10-04,\n  and GB and IE thoroughbred results from 2026-10-06.\n  AU thoroughbred and greyhound results carry the whole field, and there every finisher has\n  its position (5th, 6th, \u2026) \u2014 applied back to 2026-09-14 on 2026-09-29; before that only\n  1-4 were stored. Check `result_status`: NULL there means the race has no result at all.\n* `venue_split_suspect` / `name_fragment_suspect` \u2014 the archive's record of its own known\n  contamination. Excluded by default.\n* `scratched` \u2014 the result marked this runner scratched, yet the row holds a price: four\n  books (Betfair, Sportsbet, Ladbrokes, Neds) keep quoting a scratched runner and the\n  archive kept their last price as its \"close\". Excluded by default since 2026-09-28\n  (`include_scratched=true` to see them). Only AU thoroughbred and greyhound results name\n  their scratchings, so on harness and NZ rows false means not known, never ran.\n\n**Three columns appended LAST on 2026-09-29, after `scratched`** \u2014 every earlier column\nkeeps its position:\n\n* `open_place_price` / `close_place_price` \u2014 the book's PLACE price at the first and the\n  last observation of the series. NULL on rows archived before 2026-09-29 (the capture\n  began storing place that day) and at books that quote no place price (Betfair, and any\n  book that prices win only). The place price is recorded at the moments the WIN price\n  moved, not at every place move, so a close_place_price can be older than the book's\n  final place price by up to the time since its last win move.\n* `scratched_at` \u2014 when the runner was scratched, as the scratching book stamped it on the\n  live board, captured at the jump from 2026-09-29. NULL for a runner that ran, for a\n  scratching the board carried without a time, and for every race before that date.\n\n**Six Top-N columns appended LAST on 2026-10-05, after `scratched_at`:**\n`open_top2_price`, `open_top3_price`, `open_top4_price`, `close_top2_price`,\n`close_top3_price`, `close_top4_price` \u2014 the book's Same Race Multi Top 2 / Top 3 / Top 4\nprice at the first and the last observation of the series. Only **sportsbet, ladbrokes_au,\nneds and pointsbetau** publish these legs, so they are NULL at every other book; NULL on\nevery race before 2026-09-29, when the capture began holding them. Like the place price,\nthey are recorded at the moments the WIN price moved, not at every Top-N move.\n\n**Five consensus columns appended LAST on 2026-10-05, after `close_top4_price`:**\n`consensus_close_prob`, `consensus_close_price`, `books_in_consensus_close`,\n`consensus_open_price`, `books_in_consensus_open` \u2014 the market's margin-free probability\nfor the runner at the close, the price it corresponds to, and how many books it rests on,\nplus the same price at the 60-minute open. The arithmetic is GET /v1/racing/consensus's\ndefault (power method): each included book's implied probabilities raised to the exponent\nthat makes them sum to one, the median across books per runner, the field scaled to one.\nThe archive is one method throughout \u2014 the rows written under the multiplicative default\non 5 October 2026 were recomputed when power became the default on 6 October. A book is included at the close when its\nrow is a genuine closing line (`is_closing_line`), it prices the complete field, it is not\nwithheld from customers and the runner was not scratched; placeholder quotes leave on the\nbest-odds rule. The open figure counts only series that began at the baseline\n(`open_is_baseline`), so it rests on fewer books and is NULL where fewer than two qualify.\nThe value is the same on every bookmaker row of a runner, like `finish_position`, and it is\nrecomputed by the hourly archiver whenever a row changes. It describes the market, not the\noutcome: it is what the panel implied, with margin removed, not a probability of winning.\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` joins a row to everything about its race; `horse_ref` joins a HORSE across\nmeetings.** `runner_ref` (the column after `venue_site`, since 2026-09-05) is the registry\nid stored on the row \u2014 `ra:<horsecode>` for thoroughbreds, `grv:<dogId>` for greyhounds \u2014\nthe same value `/v1/racing/results` carries on every `runners[]` entry and\n`/v1/racing/markets` carries for the race, so an archive row joins to its result and its\nmarket rows without matching names. On a THOROUGHBRED it is the code Racing Australia\npublished on race day; Racing Australia issues a new code for the same horse each day, so\nit changes from meeting to meeting and a season keyed on it splits into one group per\nstart \u2014 `horse_ref` is the key for that. On greyhounds `grv:`/`grsa:` are per-dog registry\nids and do persist. Filled from Racing Australia acceptances for AU thoroughbreds and from\nthe result itself for every code once it lands, so it can be null on a row whose race has\nnot resulted yet, on harness (no registry feed), and on rows archived before the\nidentifiers existed (late August 2026). Appended after `venue_site` so positional CSV\nreaders keep working.\n\n**`horse_ref`** (the column after `runner_ref`, added 2026-09-21) is the horse identity:\n`pe:<name>`, the registered name folded to letters and digits with any trailing country\nparenthetical removed. It is DERIVED at serve time from `runner_name`, not stored, so every row the\narchive has ever held carries it from today \u2014 no backfill, and nothing already exported\nmoved: it is appended after `runner_ref`, exactly as `runner_ref` was appended after\n`venue_site`. It is the same value on `/v1/racing/results` runners and placings, the live\nboard, `/v1/racing/horses/form`, `/v1/racing/horses/runs` and the form backfill, so a\nhorse's whole price history across meetings is one `GROUP BY horse_ref`. Null on\ngreyhound and harness rows, and on any row whose `runner_name` is missing.\n\nBetfair Exchange rows are withheld from customer responses pending a data licence, so\n`close_lay_price` \u2014 which only an exchange quotes \u2014 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/results` and `/v1/racing/acceptances` take `date` and callers reasonably\nexpect the same spelling here. `/v1/racing/events` does NOT \u2014 it is forward-looking\nand takes `hours_ahead`.\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."
          },
          "response": []
        },
        {
          "name": "What the closing-line archive actually holds",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/closing-lines/coverage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "closing-lines",
                "coverage"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 1 credit. The archive's own measurements of itself \u2014 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 from `results_from` onward,\nand for British and Irish thoroughbreds from 2026-10-06 (Betway's settlement feed: every\nfinisher Betway ranks; on Irish races it ranks the first four first and the rest within\nabout 25 minutes of the off). British and Irish races\nare closed by the UK-licensed books\n(`ladbrokes_uk`, `williamhill`, `betway_uk`) and the Australian books that quote them;\nraces in every other country 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 \u2014 the figure to read if you intend to settle against\nthe archive. `by_category` carries `resulted_races` per code for the same reason.\n\n`finish_position_pct` is the finish-position fill rate: of the rows whose race has a\nresult (runners marked scratched left out), the share that carry a position. AU\nthoroughbred and greyhound results publish every finisher's position, and AU harness\nand NZ results carry the full field from 2026-10-04, so all of them run near 100% on\nraces from then on; before 2026-10-04 harness and NZ results named the placegetters\nonly, which holds their whole-archive figure near the placed share of the field.\n`by_category` carries it per code.\n\n`consensus_close_pct` (from 2026-10-05) is the share of genuine closing lines that carry\n`consensus_close_prob`, the market's margin-free probability stored at the close. Rows\nwithout one belong to races where fewer than two complete-field books closed.\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, refreshed at most every ten minutes. Open to every key, Free\nincluded, so you can check the archive before paying for it; the numbers describe the\nwhole archive. Reading the rows themselves (/v1/racing/closing-lines) is a paid endpoint."
          },
          "response": []
        },
        {
          "name": "Bulk export of the permanent price-movement archive",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/price-paths?from=&to=&date=&venue=&bookmakers=&category=&categories=&country=&race_id=&include_flagged=&format=&limit=&offset=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "price-paths"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time on the race start."
                },
                {
                  "key": "to",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time."
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Single-day alias: date=YYYY-MM-DD is exactly from=YYYY-MM-DD&to=YYYY-MM-DD. Mutually exclusive with from/to."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue name, case-insensitive, exact match."
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys."
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "horse, harness or greyhound \u2014 comma-separated for more than one. `categories=` is accepted as an alias (2026-09-24); an unknown value is a free 422."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `category`. Comma-separated horse, harness, greyhound. Both spellings are accepted on every racing endpoint."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "description": "Two-letter country code, case-insensitive; comma-separated for more than one, e.g. AU, or GB,IE for British and Irish racing."
                },
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "Single race. With race_id the window guard does not apply."
                },
                {
                  "key": "include_flagged",
                  "value": "",
                  "disabled": true,
                  "description": "Include rows flagged venue_split_suspect or name_fragment_suspect. False by default."
                },
                {
                  "key": "format",
                  "value": "",
                  "disabled": true,
                  "description": "csv streams a flat table with a stable column order \u2014 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) \u2014 pd.read_parquet(BytesIO(r.content)). Same credit cost and row cap as csv."
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "offset",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "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 \u2014 `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 \u2014 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 \u2014 page a season week by week. A reversed or\nover-wide range is a free 422.\n\n**`runner_ref`** (the column after `venue_site`, since 2026-09-05) is the registry id\nstored on the row \u2014 `ra:<horsecode>` for thoroughbreds, `grv:<dogId>` for greyhounds \u2014 identical to the\n`runner_ref` on `/v1/racing/results` `runners[]` entries, on `/v1/racing/closing-lines` and\n`/v1/racing/markets`, and on the live board on race day, so every point in this export\njoins to its result and to the race's other rows 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.\nOn a thoroughbred it is the code Racing Australia published on race day, not the horse:\nRacing Australia issues a new code for the same horse each day, so it changes from meeting\nto meeting. A greyhound's does not \u2014 `grv:`/`grsa:` are per-dog registry ids.\n\n**`horse_ref`** (the column after `runner_ref`, added 2026-09-21) is the horse identity\nthis export was missing: `pe:<name>`, the registered name folded to letters and digits (trailing country\nparenthetical removed). Derived at serve time from `runner_name` rather than stored, so\nevery point ever archived carries it from today, and appended after `runner_ref` so no\npositional reader moves. Group a horse's whole price history on it \u2014 across meetings,\ntracks and seasons \u2014 and use `runner_ref` to join the rows about one race. Null on\ngreyhound and harness rows.\n\n**`place_price`** (LAST column, added 2026-09-29) is the book's PLACE price at that point.\nNULL on every point archived before 2026-09-29 and at books that quote no place price. A\npoint is written when the WIN (or exchange lay) price moves \u2014 a place-only move does not\ncreate one \u2014 so it is the place price at each win move, not a separate place series.\n\n**`top2_price`, `top3_price`, `top4_price`** (LAST three columns, added 2026-10-05) are the\nbook's Same Race Multi Top 2 / Top 3 / Top 4 price at that point, on the same win-move\ncadence. Only sportsbet, ladbrokes_au, neds and pointsbetau publish them \u2014 NULL at every\nother book \u2014 and NULL on every point before 2026-09-29.\n\n**Plans.** Plus and above, and Standard keys issued before 2026-09-20. Any other key gets\na free 403 that carries a `sample`: the last five moves in one book's price for the\nwinner of one recent race, as rows of this export.\n\nBetfair Exchange rows are withheld pending a data licence, so `lay_price` \u2014 which only an\nexchange quotes \u2014 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."
          },
          "response": []
        },
        {
          "name": "The market's path per runner, stored at the jump: consensus at fixed marks, moves, breadth, first mover, signal",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/racing/market-summary?from=&to=&date=&venue=&category=&categories=&country=&race_id=&signal=&format=&limit=&offset=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "racing",
                "market-summary"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time on the race start. Defaults to the table's floor."
                },
                {
                  "key": "to",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time. Defaults to now."
                },
                {
                  "key": "date",
                  "value": "",
                  "disabled": true,
                  "description": "Single-day alias: date=YYYY-MM-DD is exactly from=YYYY-MM-DD&to=YYYY-MM-DD. Mutually exclusive with from/to."
                },
                {
                  "key": "venue",
                  "value": "",
                  "disabled": true,
                  "description": "Venue name, case-insensitive, exact match."
                },
                {
                  "key": "category",
                  "value": "",
                  "disabled": true,
                  "description": "horse, harness or greyhound \u2014 comma-separated for more than one."
                },
                {
                  "key": "categories",
                  "value": "",
                  "disabled": true,
                  "description": "Alias of `category`. Comma-separated horse, harness, greyhound. Both spellings are accepted on every racing endpoint."
                },
                {
                  "key": "country",
                  "value": "",
                  "disabled": true,
                  "description": "Two-letter country code, e.g. AU, NZ."
                },
                {
                  "key": "race_id",
                  "value": "",
                  "disabled": true,
                  "description": "One race."
                },
                {
                  "key": "signal",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated signal classes to keep: strong_firm, firm, steady, drift, strong_drift, insufficient."
                },
                {
                  "key": "format",
                  "value": "",
                  "disabled": true,
                  "description": "csv streams a flat table with a stable column order."
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "offset",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "description": "Cost: 5 credits (JSON) / 20 credits (CSV). The market summary archive: one row per race\nand runner, written at the jump, carrying the market consensus price at T-60, T-30, T-15,\nT-10, T-5 and T-2 and at the close, its high and low through the capture window,\n`move_from_t60_pct` / `move_from_t15_pct` / `move_t5_close_pct` (close against those\nmarks; negative is a runner that shortened), the breadth of the move (books with history,\nfirming, drifting, unchanged), the first attributable book to move with its lead and the\nfollowers' median lag, the signal class, and the best closing price against the consensus.\n\n**It is GET /v1/racing/intelligence, kept.** The same analysis, run at the jump with the\narchive's own `consensus_close_price` as the close (`close_source: archive`; a runner the\narchive could not score falls back to the ticks at the jump, `close_source: ticks`), so a\nlive reading and the stored row are one quantity. The ticks it is built from live in the\n45-day snapshot store; this row is permanent, from `summary_from` forward.\n\n**Joined at serve time, appended last:** `venue_id`, `venue_site`, `horse_ref`, and the\nrunner's `finish_position` and `result_status` from the closing-line archive, which land\nafter the summary is written. Scratched runners carry no row.\n\n**The signal classes against results** are what this table exists for. `signal` filters\non them; joined to `finish_position` the table says, for any window, how often each class\nwon and at what price. Those strike rates belong on the market report; until they are\nprinted there, compute them from this table rather than trusting the label.\n\n**Plans.** Plus and above read the whole table; Standard a rolling 90-day window\n(`window_days` 90, `X-Archive-Window-Days`); Free and Hobby a free 403. Standard keys\nissued before 2026-09-20 keep the whole archive they were sold. `date=` is a single-day\nalias for `from`/`to`; an unknown parameter, signal or date is a free 422."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Sports",
      "item": [
        {
          "name": "List available sports",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sports",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sports"
              ],
              "query": [],
              "variable": []
            },
            "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."
          },
          "response": []
        },
        {
          "name": "Get odds by sport",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sports/:sport_key/odds?markets=&bookmakers=&competition=&include_unknown_competition=&oddsFormat=&maxAgeMinutes=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sports",
                ":sport_key",
                "odds"
              ],
              "query": [
                {
                  "key": "markets",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated: h2h,spreads,totals"
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "competition",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 see `include_unknown_competition` for why that distinction matters. See the `competition` field on each event."
                },
                {
                  "key": "include_unknown_competition",
                  "value": "",
                  "disabled": true,
                  "description": "When filtering by `competition`, ALSO return events whose competition is null \u2014 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."
                },
                {
                  "key": "oddsFormat",
                  "value": "",
                  "disabled": true,
                  "description": "decimal or american"
                },
                {
                  "key": "maxAgeMinutes",
                  "value": "",
                  "disabled": true,
                  "description": "Exclude bookmaker markets older than this many minutes; default 360 allows 3-hour supplemental feeds while hiding multi-day stale prices."
                }
              ],
              "variable": [
                {
                  "key": "sport_key",
                  "value": "afl",
                  "description": "sport_key path parameter"
                }
              ]
            },
            "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 \u2014 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.)"
          },
          "response": []
        },
        {
          "name": "Historical odds snapshots",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sports/:sport_key/odds/history?from=&to=&bookmakers=&market=&offset=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sports",
                ":sport_key",
                "odds",
                "history"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time; defaults to 7 days ago"
                },
                {
                  "key": "to",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time; defaults to now"
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing."
                },
                {
                  "key": "market",
                  "value": "",
                  "disabled": true,
                  "description": "Market type: h2h (default), spreads or totals."
                },
                {
                  "key": "offset",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": [
                {
                  "key": "sport_key",
                  "value": "afl",
                  "description": "sport_key path parameter"
                }
              ]
            },
            "description": "Cost: 5 credits. Paid tiers only \u2014 403 on Free and Hobby.\n\nSnapshots of one market, newest first, 500 per page (`offset`), each about ten minutes\napart per bookmaker and only while the fixture has not started \u2014 so the last snapshot\nbefore `commence_time` is the closing price, and /v1/sports/{sport_key}/closing-lines\nreturns that row per book with the consensus already worked out.\n\n`home_point`/`away_point` are the line the prices are attached to: each side's handicap\non the spreads market (-6.5 / +6.5) and the total on the totals market, where Over is\nstored as the home side and Under as the away side. Lines exist from 2026-09-29; rows\nrecorded before that carry prices only. AFL and AFLW history is kept permanently; every\nother sport is a rolling 90-day window.\n\nAn unknown `sport_key`, or a date range that runs backwards, returns an error and costs\nnothing."
          },
          "response": []
        },
        {
          "name": "Price movement feed",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sports/:sport_key/odds/movements?since=&bookmakers=&market=&min_delta=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sports",
                ":sport_key",
                "odds",
                "movements"
              ],
              "query": [
                {
                  "key": "since",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time; defaults to 24 hours ago"
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys, so a typo costs nothing."
                },
                {
                  "key": "market",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "min_delta",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": [
                {
                  "key": "sport_key",
                  "value": "afl",
                  "description": "sport_key path parameter"
                }
              ]
            },
            "description": "Cost: 5 credits. Returns detected home/draw/away price movements from historical snapshots.\n\nPaid tiers only \u2014 403 on Free and Hobby. Same gate as the history endpoint above; the 403\nbody names the plans that include it.\n\nAn unknown `sport_key` returns 404 and costs nothing."
          },
          "response": []
        },
        {
          "name": "Closing price and line per bookmaker, with the consensus",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/sports/:sport_key/closing-lines?from=&to=&bookmakers=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "sports",
                ":sport_key",
                "closing-lines"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time. Matches that START on or after this; defaults to 7 days ago."
                },
                {
                  "key": "to",
                  "value": "",
                  "disabled": true,
                  "description": "ISO date/time. Matches that start on or before this; defaults to now. At most 92 days after `from` \u2014 page by month for a season."
                },
                {
                  "key": "bookmakers",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated bookmaker keys, case-insensitive. An unrecognised key is a free 422 naming the valid keys."
                }
              ],
              "variable": [
                {
                  "key": "sport_key",
                  "value": "afl",
                  "description": "sport_key path parameter"
                }
              ]
            },
            "description": "Cost: 5 credits. Paid tiers only \u2014 403 on Free and Hobby.\n\nOne row per match that started in the range. For each bookmaker: the last head-to-head\nprice, handicap line and total we recorded before the start \u2014 the closing snapshot, taken\nwithin about ten minutes of the first bounce. Then a consensus across those books: the\nmedian closing price on head-to-head, and on the line and total markets the most common\nclosing line with the median price at that exact line.\n\n`home_point`/`away_point` are the handicap on each side (-6.5 / +6.5) on the spreads\nmarket and the total on both sides of the totals market, where Over is the home side and\nUnder the away side. The lines exist from 2026-09-29; rows recorded before that carry\nprices only. AFL and AFLW price history is kept permanently, so their closes accumulate\nseason on season; every other sport is a rolling 90-day window. Betfair is never included.\nAn unknown `sport_key`, a backwards range or a range over 92 days is an error and costs\nnothing."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Best Odds",
      "item": [
        {
          "name": "Best price per selection",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/best-odds/:sport_key",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "best-odds",
                ":sport_key"
              ],
              "query": [],
              "variable": [
                {
                  "key": "sport_key",
                  "value": "afl",
                  "description": "sport_key path parameter"
                }
              ]
            },
            "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."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Arb & Value",
      "item": [
        {
          "name": "Sports arb scanner",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/arb/sports?sport_key=&maxAgeMinutes=&min_profit_pct=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "arb",
                "sports"
              ],
              "query": [
                {
                  "key": "sport_key",
                  "value": "",
                  "disabled": true,
                  "description": "Filter by sport e.g. afl, nrl"
                },
                {
                  "key": "maxAgeMinutes",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "min_profit_pct",
                  "value": "",
                  "disabled": true,
                  "description": "Min guaranteed profit % (0 = show all value)"
                }
              ],
              "variable": []
            },
            "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."
          },
          "response": []
        },
        {
          "name": "Best prices comparison",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/arb/best-prices?maxAgeMinutes=&sport_key=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "arb",
                "best-prices"
              ],
              "query": [
                {
                  "key": "maxAgeMinutes",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "sport_key",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "description": "Cost: 2 credits. Returns the best available price per selection plus full bookmaker comparison, worst price, average price, spread and overlay percentage."
          },
          "response": []
        },
        {
          "name": "Spreads/totals line arb",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/arb/lines?sport_key=&maxAgeMinutes=&min_profit_pct=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "arb",
                "lines"
              ],
              "query": [
                {
                  "key": "sport_key",
                  "value": "",
                  "disabled": true,
                  "description": "Filter by sport e.g. afl, nrl"
                },
                {
                  "key": "maxAgeMinutes",
                  "value": "",
                  "disabled": true,
                  "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."
                },
                {
                  "key": "min_profit_pct",
                  "value": "",
                  "disabled": true,
                  "description": "Min guaranteed profit % (0 = also show the best line comparison when not an arb)"
                }
              ],
              "variable": []
            },
            "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."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Data Quality",
      "item": [
        {
          "name": "Data quality summary",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/data-quality/summary",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "data-quality",
                "summary"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 1 credit. Summary of connector freshness, canonical mappings and recent audit status."
          },
          "response": []
        },
        {
          "name": "Latest bookmaker audits",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/data-quality/audit/latest?sport_key=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "data-quality",
                "audit",
                "latest"
              ],
              "query": [
                {
                  "key": "sport_key",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": []
            },
            "description": "Latest bookmaker audits"
          },
          "response": []
        },
        {
          "name": "Audit result detail",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/data-quality/audit/:run_id/results?status=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "data-quality",
                "audit",
                ":run_id",
                "results"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ],
              "variable": [
                {
                  "key": "run_id",
                  "value": "YOUR_RUN_ID",
                  "description": "run_id path parameter"
                }
              ]
            },
            "description": "Audit result detail"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Health",
      "item": [
        {
          "name": "Per-connector freshness",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/health/connectors?stale_after_s=&coverage_stale_after_s=&include_sports=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "health",
                "connectors"
              ],
              "query": [
                {
                  "key": "stale_after_s",
                  "value": "",
                  "disabled": true,
                  "description": "age (s) past which a connector is 'stale'"
                },
                {
                  "key": "coverage_stale_after_s",
                  "value": "",
                  "disabled": true,
                  "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 \u2014 past it the book is effectively absent for that sport."
                },
                {
                  "key": "include_sports",
                  "value": "",
                  "disabled": true,
                  "description": "include the per-sport coverage breakdown"
                }
              ],
              "variable": []
            },
            "description": "Cost: 1 credit. Per-connector last-success age + a stale flag, so a stalled feed is\nvisible.\n\n`sports` breaks that down PER SPORT, because connector-level health cannot see a book\nthat has stopped covering one competition while it keeps writing others: a book can be\ncurrent to the minute on NRL and AFL while its rows for another sport are days old."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Account",
      "item": [
        {
          "name": "Check API usage",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/usage?format=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "usage"
              ],
              "query": [
                {
                  "key": "format",
                  "value": "",
                  "disabled": true,
                  "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."
                }
              ],
              "variable": []
            },
            "description": "Free \u2014 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 \u2014 `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` \u2014 that is not a\n  discrepancy, the two are measuring different things.\n* CURRENT BILLING PERIOD \u2014 `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."
          },
          "response": []
        },
        {
          "name": "Self-service billing portal link",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/portal",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "portal"
              ],
              "query": [],
              "variable": []
            },
            "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. No website account or separate login is needed: the key\nthat holds the subscription opens its portal, so a customer who subscribed through the API\ncan change a failing card, pull invoices or cancel without contacting us.\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."
          },
          "response": []
        },
        {
          "name": "Billing portal link (console; confirms your password)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/portal",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "portal"
              ],
              "query": [],
              "variable": []
            },
            "description": "Same reply as GET /v1/billing/portal. Cost: 0 credits.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"PASSWORD\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "A signed link to your prorated upgrade page",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/upgrade-link",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "upgrade-link"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. For the signed-in account page (2026-09-28): /v1/billing/upgrade\nidentifies its reader by X-API-Key or by an emailed HMAC token, and a browser navigating to a\npage sends neither \u2014 so this mints the same 30-minute token for the caller's own customer\nid and returns the page URL. Without a subscription there is nothing to prorate and the\nanswer is the ordinary pricing page. Like the portal URL, treat the link as a bearer\ncredential for the subscription."
          },
          "response": []
        },
        {
          "name": "Prorated upgrade link (console; confirms your password)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/upgrade-link",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "upgrade-link"
              ],
              "query": [],
              "variable": []
            },
            "description": "Same reply as GET /v1/billing/upgrade-link. Cost: 0 credits.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"PASSWORD\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Connector health status",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "health"
              ],
              "query": [],
              "variable": []
            },
            "description": "Requires auth. Shows last_ok, status, records_written per connector."
          },
          "response": []
        },
        {
          "name": "Public uptime stats",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/uptime",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uptime"
              ],
              "query": [],
              "variable": []
            },
            "description": "Public uptime stats \u2014 no auth required, 0 credits.\n\n**`uptime_30d_pct` is nullable \u2014 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. A missing measurement is never reported as availability.\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."
          },
          "response": []
        },
        {
          "name": "Rotate your API key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/rotate?immediate=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "rotate"
              ],
              "query": [
                {
                  "key": "immediate",
                  "value": "",
                  "disabled": true,
                  "description": "Revoke the old key NOW instead of after the grace window. Use this when rotating because the key leaked \u2014 a grace window keeps a compromised credential alive and is the wrong default for that case."
                }
              ],
              "variable": []
            },
            "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."
          },
          "response": []
        },
        {
          "name": "Email yourself a fresh API key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/resend",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "resend"
              ],
              "query": [],
              "variable": []
            },
            "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\u26d4 ALWAYS 202, WHATEVER HAPPENS \u2014 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 \u2014 the credential is the thing that is lost.\n\n\u26a0\ufe0f 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 \u2014 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.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"you@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "API key metadata",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/info",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "info"
              ],
              "query": [],
              "variable": []
            },
            "description": "Returns metadata about the authenticated key including plan, credits, org, and last-used info.\nCredits are the POOL's when this is a team key (its own counter is never charged);\n`team` describes the pool on accounts that hold or may hold team keys."
          },
          "response": []
        },
        {
          "name": "Set IP whitelist for your key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/ip-whitelist",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "ip-whitelist"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Restrict this key to specific IPs or CIDR ranges. Other plans get 403.\nPass an empty list to remove restrictions.",
            "body": {
              "mode": "raw",
              "raw": "[\n  \"203.0.113.7/32\",\n  \"198.51.100.0/24\"\n]",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Usage breakdown by endpoint",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/usage/analytics?days=&period=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "usage",
                "analytics"
              ],
              "query": [
                {
                  "key": "days",
                  "value": "",
                  "disabled": true,
                  "description": "Rolling window length. Ignored when period=billing."
                },
                {
                  "key": "period",
                  "value": "",
                  "disabled": true,
                  "description": "rolling = the last `days` days (default, unchanged). billing = the CURRENT billing period, so the totals here match credits_used and credits_remaining exactly."
                }
              ],
              "variable": []
            },
            "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 at 00:00 UTC on the 1st of each month,\nso 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\u26a0\ufe0f `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 \u2014 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."
          },
          "response": []
        },
        {
          "name": "Email yourself a one-time login link for the key console",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/login/request",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "login",
                "request"
              ],
              "query": [],
              "variable": []
            },
            "description": "Always 202 with an identical body. Cost: 0 credits. Unauthenticated by necessity.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"you@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Redeem a login link for a console session",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/login/exchange",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "login",
                "exchange"
              ],
              "query": [],
              "variable": []
            },
            "description": "Redeem a login link for a console session",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"TOKEN\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "End a console session",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/logout",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "logout"
              ],
              "query": [],
              "variable": []
            },
            "description": "End a console session"
          },
          "response": []
        },
        {
          "name": "Issue a fresh key and return it (signed-in console only)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/reveal",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "reveal"
              ],
              "query": [],
              "variable": []
            },
            "description": "Issue a fresh key and return it (signed-in console only)",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"PASSWORD\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Log in with your email and password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "login"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Returns the same 30-day console session that the emailed link mints;\npresent `session_token` in the X-API-Key header exactly like a key. The failure reply is\nidentical whether the address is unknown, has no password yet, or the password is wrong.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"you@example.com\",\n  \"password\": \"PASSWORD\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Email yourself a one-time password reset link",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/password/forgot",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "password",
                "forgot"
              ],
              "query": [],
              "variable": []
            },
            "description": "Always 202 with an identical body \u2014 the same existence-oracle discipline as\n/v1/login/request. Cost: 0 credits. Unauthenticated by necessity. Works for an account that\nhas never set a password: the link is how an existing customer sets their first one.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"you@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Choose a new password from a reset link",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/password/reset",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "password",
                "reset"
              ],
              "query": [],
              "variable": []
            },
            "description": "Consumes the token (single use), sets the password, signs out every other device and\nreturns a fresh session so the page can land the customer straight in their account.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"TOKEN\",\n  \"new_password\": \"NEW_PASSWORD\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Set or change your account password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/account/password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "account",
                "password"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Works with a signed-in console session or the raw key. Setting a FIRST\npassword needs nothing else \u2014 the caller already proved the account by holding the key or by\nredeeming an emailed link. Changing an existing one requires `current_password`; forgotten,\nthe reset link from the login page is the path. Other devices are signed out; the session\nmaking this call (if any) stays signed in.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"new_password\": \"NEW_PASSWORD\",\n  \"current_password\": \"CURRENT_PASSWORD\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Your account: email, plan, password, billing and consent status",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/account",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "account"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. What the account page needs in one call. `email` is the caller's own\naddress in full \u2014 it is their key or session asking. `billing_attached` says whether\n/v1/billing/portal has a subscription to open; `password_set` whether /v1/login works yet."
          },
          "response": []
        },
        {
          "name": "Turn promotional emails on or off for your account",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/account/marketing",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "account",
                "marketing"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Appends a consent record \u2014 the account page's toggle.\nTurning it ON from a signed-in account also lifts an `unsubscribed` suppression on the\naddress (never a bounce or a provider block): the person is asking, in their own account, to\nhear from us again. Messages about the account and key are unaffected either way.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"opt_in\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Sign out of every device",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/logout/all",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "logout",
                "all"
              ],
              "query": [],
              "variable": []
            },
            "description": "Revokes every console session on the account, including this one (2026-09-28). The API\nkey itself is untouched \u2014 rotate it from the console if it is the key you suspect."
          },
          "response": []
        },
        {
          "name": "Mint a team key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/team",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "team"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Mint another key on this account. It shares this account's monthly\ncredit pool and plan, has its own rate-limit bucket, its own IP whitelist and its own line\nin the usage breakdown, and can be revoked on its own. Business accounts may hold 3 keys\nin total, Platform 5, Scale 10 \u2014 the primary key counts as one.\n\nPass `monthly_cap` to limit what this one key may spend per month inside the pool \u2014 the\nkey refuses with 402 at its cap and the pool is not touched, so one client, one worker or\none trial cannot spend the account's month. Change or remove it later with\n`PATCH /v1/keys/team/{key_id}`. Caps reset with the pool on the 1st.\n\nThe key is returned once. It is stored hashed, like every key here, so if it is lost the\nanswer is revoke and mint again, not recovery.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"LABEL\",\n  \"monthly_cap\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "List this account's keys",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/team",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "team"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. The primary key and every active team key on this account, with each\none's calls and credits this billing period, its monthly cap if it has one, and the\nshared pool's balance."
          },
          "response": []
        },
        {
          "name": "Change a team key's cap or label",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/team/:key_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "team",
                ":key_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "key_id",
                  "value": "YOUR_KEY_ID",
                  "description": "key_id from GET /v1/keys/team."
                }
              ]
            },
            "description": "Cost: 0 credits. Set, change or remove one team key's monthly credit cap, and/or\nrelabel it. The key string does not change and nothing needs redeploying.\n\n`{\"monthly_cap\": 5000}` caps this key at 5,000 credits for the rest of this period \u2014\nagainst the counter it has ALREADY spent, so lowering a cap below what a key has used\nthis month refuses it until the 1st rather than rewriting history. `{\"monthly_cap\":\nnull}` removes the cap and lets the key spend the whole pool. Leave a field out and it is\nnot touched: `{\"label\": \"Jo's laptop\"}` alone renames the key and leaves its cap alone.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"LABEL\",\n  \"monthly_cap\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Revoke a team key",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys/team/:key_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys",
                "team",
                ":key_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "key_id",
                  "value": "YOUR_KEY_ID",
                  "description": "key_id from GET /v1/keys/team."
                }
              ]
            },
            "description": "Cost: 0 credits. Deactivate one team key immediately. Its usage history stays in the\naccount's breakdown; the key string stops authenticating on the next request."
          },
          "response": []
        },
        {
          "name": "Create a white-label widget",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widgets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widgets"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Create an embeddable odds widget that carries YOUR branding.\n\nThe widget renders from the same feed as our own public widget, in your colours, with\nyour bookmaker links and no PuntersEdge credit. Embed it with the `embed` snippets in\nthe response; the `csp` line is what an embedding page with a Content-Security-Policy\nneeds to allow the frame.\n\nEach RENDER is metered against this account \u2014 2 credits for a racing fill, 1 for a\nsports fill \u2014 but the page is cached for 60 seconds per parameter set, so the cost\nfollows time, not traffic: a widget on a page with ten thousand readers costs the same\nas one on a page with one.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"LABEL\",\n  \"config\": {}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "List this account's widgets",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widgets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widgets"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Every widget on this account, with how many times each has been\nfilled (`fills_total`, exact per widget) and what widgets have cost this ACCOUNT so far\nthis billing period (`credits_this_period`, the account total \u2014 usage_logs is keyed by\nkey and endpoint, so a per-widget split of the spend does not exist)."
          },
          "response": []
        },
        {
          "name": "One widget",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widgets/:widget_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widgets",
                ":widget_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "widget_id",
                  "value": "YOUR_WIDGET_ID",
                  "description": "wgt_\u2026 from POST /v1/widgets."
                }
              ]
            },
            "description": "Cost: 0 credits. The stored (normalised) config, the embed snippets and the counters."
          },
          "response": []
        },
        {
          "name": "Change a widget's label or config",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widgets/:widget_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widgets",
                ":widget_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "widget_id",
                  "value": "YOUR_WIDGET_ID",
                  "description": "wgt_\u2026 from POST /v1/widgets."
                }
              ]
            },
            "description": "Cost: 0 credits. `config` is a FULL replacement, validated the same way as on create \u2014\na merge would make it impossible to remove an allowed origin or a per-book override, and\n\"send it back with your change in it\" is the only rule that has one meaning. The widget\nid and the embed snippets do not change, so nothing on the customer's page needs redeploying.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"LABEL\",\n  \"config\": {}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Revoke a widget",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/widgets/:widget_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "widgets",
                ":widget_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "widget_id",
                  "value": "YOUR_WIDGET_ID",
                  "description": "wgt_\u2026 from POST /v1/widgets."
                }
              ]
            },
            "description": "Cost: 0 credits. Stop serving this widget. Any iframe still embedding it renders the\nneutral \"odds unavailable\" frame from the next fill on \u2014 never an error page on someone\nelse's website. The row is kept (active=false) so its counters survive."
          },
          "response": []
        },
        {
          "name": "Overage: state, rate and ceiling",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/overage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "overage"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Whether this account may run past its monthly allowance, what that\ncosts, where it stops, and how much of it this period has used.\n\n`ceiling` is the number the API actually enforces: when overage is off it equals your\ncredit limit, which is the hard 402 every plan has always had. `max_credits` is what the\nplan allows when it is on. `estimated_cents_this_period` is what would be invoiced if the\nmonth ended now \u2014 the same arithmetic the billing run uses, rounded up to whole 1,000s.\n\nA team key sees the POOL's figures: overage belongs to the account, and a team key's own\nmonthly cap is hard."
          },
          "response": []
        },
        {
          "name": "Switch overage on or off",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/billing/overage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "billing",
                "overage"
              ],
              "query": [],
              "variable": []
            },
            "description": "Cost: 0 credits. Primary key only, commercial tiers only.\n\nSwitching it ON lets calls continue past your allowance, up to `ceiling`, and the excess\nis invoiced at `rate_per_1000_cents` per 1,000 credits on your next invoice. Switching it\nOFF restores the hard 402 at the allowance immediately \u2014 including mid-period, in which\ncase the overage already used is still invoiced, because it was already served.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"enabled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Webhooks",
      "item": [
        {
          "name": "Create Webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ],
              "query": [],
              "variable": []
            },
            "description": "Create Webhook",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://example.com/hooks/puntersedge\",\n  \"events\": [\n    \"arb.opportunity\"\n  ],\n  \"min_edge_pct\": 1.5,\n  \"sports\": [\n    \"nrl\",\n    \"afl\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "List Webhooks",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ],
              "query": [],
              "variable": []
            },
            "description": "List Webhooks"
          },
          "response": []
        },
        {
          "name": "Delete Webhook",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/:webhook_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                ":webhook_id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "YOUR_WEBHOOK_ID",
                  "description": "webhook_id path parameter"
                }
              ]
            },
            "description": "Delete Webhook"
          },
          "response": []
        },
        {
          "name": "List Webhook Deliveries",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/:webhook_id/deliveries",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                ":webhook_id",
                "deliveries"
              ],
              "query": [],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "YOUR_WEBHOOK_ID",
                  "description": "webhook_id path parameter"
                }
              ]
            },
            "description": "List Webhook Deliveries"
          },
          "response": []
        },
        {
          "name": "Test Webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/:webhook_id/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                ":webhook_id",
                "test"
              ],
              "query": [],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "YOUR_WEBHOOK_ID",
                  "description": "webhook_id path parameter"
                }
              ]
            },
            "description": "Test Webhook"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Bulk data",
      "item": [
        {
          "name": "Bulk dataset catalogue",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/bulk/datasets?dataset=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "bulk",
                "datasets"
              ],
              "query": [
                {
                  "key": "dataset",
                  "value": "",
                  "disabled": true,
                  "description": "Restrict to one dataset name."
                }
              ],
              "variable": []
            },
            "description": "Cost: 0 credits. Every dataset this API publishes as pre-built files, with the daily\nfiles and monthly bundles that exist right now: rows, bytes per format, digest, when each\nwas generated, and the URL that serves it. Read it before pulling, and re-read it nightly\n\u2014 a day's file is regenerated for three nights after the day (late results, late closing\nlines) and its digest changes when its rows do.\n\nDaily files are Business and above; monthly bundles are Platform and above. Both formats\nare always present: `format=parquet` (typed columns, snappy) and `format=csv` (gzip)."
          },
          "response": []
        },
        {
          "name": "Download one dataset file",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/bulk/:dataset/:period?format=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "bulk",
                ":dataset",
                ":period"
              ],
              "query": [
                {
                  "key": "format",
                  "value": "",
                  "disabled": true,
                  "description": "parquet (typed, snappy) or csv (gzip-compressed)."
                }
              ],
              "variable": [
                {
                  "key": "dataset",
                  "value": "YOUR_DATASET",
                  "description": "Dataset name from /v1/bulk/datasets."
                },
                {
                  "key": "period",
                  "value": "YOUR_PERIOD",
                  "description": "YYYY-MM-DD for a Sydney meeting day, YYYY-MM for a calendar month."
                }
              ]
            },
            "description": "Cost: 20 credits for a daily file, 100 for a monthly bundle, charged when the file is\nserved; a 304 costs nothing. One request returns the whole day or month as one file, with\nexactly the rows the paged endpoints would give you: Betfair withheld, split-flagged rows\nexcluded, the field-disclosure rule applied to results.\n\n`pd.read_parquet(BytesIO(r.content))` reads the Parquet; `format=csv` is a gzip file\n(`pd.read_csv(BytesIO(r.content), compression=\"gzip\")`) with the same column order,\nwhich is the order listed in the catalogue.\n\nDaily files are Business and above. Monthly bundles are Platform and above; a Business\nkey asking for a month gets a 403 that says so and is not charged."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Referrals",
      "item": [
        {
          "name": "Your referral link and earnings",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/referral",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "referral"
              ],
              "query": [],
              "variable": []
            },
            "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 \u2014 `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."
          },
          "response": []
        },
        {
          "name": "Choose how your referral reward is paid",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/referral",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "referral"
              ],
              "query": [],
              "variable": []
            },
            "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 \u2014 we do not retroactively convert a paid-out dollar into credits or\nthe other way round.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reward_type\": \"REWARD_TYPE\",\n  \"payout_email\": \"you@example.com\",\n  \"code\": \"CODE\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.puntersedge.online",
      "description": "API base URL."
    },
    {
      "key": "apiKey",
      "value": "",
      "description": "Your API key. Not needed for the Sandbox folder."
    }
  ]
}