Krok Odds

Krok Odds API

134 REST endpoints covering live odds from 140+ Australian bookmakers, the full AU racing stack, built-in arb/+EV/SGM scanners, AI tips and historical archives. JSON in, JSON out, no SDK required.

curl -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/opportunities/positive-ev?sport=aussierules_afl&limit=10"

Getting Started

Authentication

Every request (except /status) needs an API key. Pass it as the X-API-Key header, or as a ?api_key= / ?apikey= query parameter if you can't set custom headers. Get a free key at /api-access – no credit card.

Base URL & format

REST over HTTPS, JSON in and out. https://krokodds.com.au/api/v1 is the root for every endpoint below. No SDK needed – works from any language with an HTTP client. CORS is open (Access-Control-Allow-Origin: *) so you can call it directly from the browser.

Response envelope

Every endpoint returns the same shape. meta always carries count, tier and timestamp, plus endpoint-specific fields. A query that matches nothing returns data: [] with HTTP 200 (sometimes 204) – and the credit is refunded, so empty results are free.

{
  "success": true,
  "data": [ /* array of results, or a single object for a few endpoints */ ],
  "meta": {
    "count": 12,
    "tier": "free",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": { "limit": 100, "remaining": 47, "reset": "2026-10-01T00:00:00.000Z" }
  }
}

Errors

Errors use the same envelope with success: false and an error string. A handful of legacy endpoints omit success on error responses – always check the HTTP status code, don't rely solely on the body shape.

StatusMeaning
400Bad request – a query parameter failed validation
401Missing or invalid API key
402Monthly credit limit exceeded – top up or enable overage
403Your tier doesn’t include this feature (upgrade to the API plan)
404Resource not found (e.g. unknown event/horse/date)
429Rate limit exceeded – back off and retry with the Retry-After header
500Internal error – safe to retry

Credit System

Requests are metered in credits, not flat request counts – a heavy archive pull costs more than a simple lookup. Every response carries X-Credits-Cost and X-Credits-Remaining headers so you can meter your own usage without a dashboard round-trip. A request that returns zero rows is refunded in full.

Free tier

  • 50 credits / month
  • 5 requests / minute
  • 100-row cap per request (most endpoints)
  • 300s minimum cache – good for evaluating the API, not production traffic
  • No access to historical or bulk_export endpoints

API plan – A$49/mo

  • 50,000 credits / month included
  • 500 requests / minute
  • Up to 500–5,000 rows per request depending on endpoint
  • 15s minimum cache
  • Full access, including historical archives and bulk export
  • Optional pay-as-you-go overage past 50,000 credits (A$0.005/credit, capped)

What things cost

CategoryCreditsExamples
Standard1Most live endpoints – gameday, opportunities, racing meetings, odds feed
Archive5Results, odds/CLV history, historical player/team stats – paid tier only
Bulk25/bulk, /export, /odds-feed/racing/history – paid tier only

106 of 134 endpoints are reachable on the free tier – the rest (28) require the API plan (deep historical archives and bulk exports).

Endpoints by Category

Gameday

8 endpoints

Per-event head-to-head odds, alternate lines, best prices across books, player props, live state and AI-generated summaries.

GET/api/v1/gameday/alt-linesFree tier

Alternate-line (non-standard spread/total) odds for events, best price per line plus the full raw line list.

No creditCost concept in this route — auth is monthly-usage based (meta.rate_limit), not per-request credits. event_id does a direct Supabase doc lookup instead of a query. No results returns data: [] (HTTP 200), not 404. Response cached via unstable_cache 300s (free) / 60s (api). CORS open (Access-Control-Allow-Origin: *).

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (also accepts `sport`), e.g. `basketball_nba`. Lowercased.
sportstringNoAlias for sport_key.
event_idstringNoReturn alt-lines for a single event only (direct doc lookup).
marketstringNoFilter lines/best-by-line to a single market key, e.g. `spreads`. Lowercased.
limitnumberNo50Max events returned. Clamped to 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/alt-lines?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "basketball_nba_20260903_lal_bos",
      "sport_key": "basketball_nba",
      "sport_title": "NBA",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "commence_time": "2026-09-03T23:10:00Z",
      "best_by_line": [
        {
          "key": "spreads|-4.5",
          "market_key": "spreads",
          "selection": "Boston Celtics -4.5",
          "point": -4.5,
          "bookmaker": "sportsbet",
          "odds": 1.9,
          "bet_link": "https://www.sportsbet.com.au/..."
        }
      ],
      "all_lines": [
        {
          "market_key": "spreads",
          "selection": "Boston Celtics -4.5",
          "point": -4.5,
          "bookmaker": "ladbrokes",
          "odds": 1.87,
          "bet_link": "https://www.ladbrokes.com.au/..."
        }
      ],
      "line_count": 12,
      "updated_at": "2026-09-03T10:05:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": "basketball_nba",
    "event_id": null,
    "market": null,
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/gameday/best-pricesFree tier

Best available price per market/selection per event, aggregated across bookmakers.

Cache 120s (free) / 30s (api) — tighter than most gameday routes since best prices move fastest. `bookmaker_count` reflects how many books were quoting that exact price, not total books on the market.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (also accepts `sport`).
sportstringNoAlias for sport_key.
event_idstringNoFilter to a single event via query (not a direct doc get, unlike alt-lines).
marketstringNoFilter the prices array to a single market. Lowercased.
limitnumberNo50Max events returned. Clamped to 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/best-prices?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "basketball_nba_20260903_lal_bos",
      "event_id": "basketball_nba_20260903_lal_bos",
      "sport_key": "basketball_nba",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "commence_time": "2026-09-03T23:10:00Z",
      "prices": [
        {
          "market": "h2h",
          "selection": "Boston Celtics",
          "point": null,
          "price": 1.65,
          "bookmaker": "sportsbet",
          "bookmaker_title": "Sportsbet",
          "bookmaker_count": 9,
          "bet_link": "https://www.sportsbet.com.au/..."
        }
      ],
      "updated_at": "2026-09-03T10:04:30Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "markets_filter": null,
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/gameday/event/{id}Free tier

Bundled single-event fetch — pulls summary, h2h, best_prices, alt_lines, odds_history, live and props in one call, selectable via `parts`.

400 error if sport_key is missing while parts includes h2h or live. Each requested part is read via Promise.allSettled, so a single collection failure returns null for that part rather than failing the whole request. `parts` output shapes vary per sub-collection (not normalized to a common schema) — summary/h2h/best_prices/alt_lines/odds_history/live are single objects (or null), props is an array.

Path parameters
NameTypeRequiredDefaultDescription
idstringYesGameday event id (path segment), e.g. `basketball_nba_20260903_lal_bos`.
Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoRequired only if `parts` includes h2h or live (they key off `${sportKey}_${eventId}`); also accepts `sport`.
sportstringNoAlias for sport_key.
partsstring (comma-separated enum)Noall of: summary,h2h,best_prices,alt_lines,props,odds_history,liveComma-separated subset of parts to fetch; invalid values are silently dropped.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/event/abc123?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": {
    "event_id": "basketball_nba_20260903_lal_bos",
    "sport_key": "basketball_nba",
    "summary": {
      "headline": "Celtics look to cover at home against shorthanded Lakers",
      "summary": "Boston is 8-2 ATS in its last 10 home games...",
      "key_storylines": [
        "Lakers missing starting PG",
        "Celtics on a 5-game win streak"
      ],
      "generated_at": "2026-09-03T08:00:00Z"
    },
    "h2h": {
      "summary": {
        "total_meetings": 12,
        "home_wins": 7,
        "away_wins": 5
      },
      "last_meetings": [
        {
          "date": "2026-02-14",
          "home_team": "Boston Celtics",
          "away_team": "Los Angeles Lakers",
          "home_score": 118,
          "away_score": 109
        }
      ]
    },
    "best_prices": {
      "id": "basketball_nba_20260903_lal_bos",
      "prices": []
    },
    "alt_lines": {
      "bestByLine": {},
      "allLines": []
    },
    "odds_history": null,
    "live": null,
    "props": [
      {
        "id": "prop123",
        "playerName": "Jayson Tatum",
        "marketKey": "player_points",
        "line": 27.5,
        "odds": 1.9,
        "bookmaker": "sportsbet",
        "bet_link": "https://..."
      }
    ]
  },
  "meta": {
    "tier": "free",
    "parts": [
      "summary",
      "h2h",
      "best_prices",
      "alt_lines",
      "props",
      "odds_history",
      "live"
    ],
    "event_id": "basketball_nba_20260903_lal_bos",
    "sport_key": "basketball_nba",
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/gameday/h2hFree tier

Head-to-head historical matchup summaries and recent meeting results between the two teams in an event.

`team` filter is applied client-side after fetching up to limit*2 rows from Supabase, then sliced to `limit` — so a narrow team filter combined with a high limit can under-return relative to what actually exists. Draws is always 0 for sports without draws.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (also accepts `sport`).
sportstringNoAlias for sport_key.
event_idstringNoFilter to a single event.
teamstringNoSubstring match (case-insensitive) against either home_team or away_team.
limitnumberNo50Max rows returned. Clamped to 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/h2h?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "basketball_nba_20260903_lal_bos",
      "event_id": "basketball_nba_20260903_lal_bos",
      "sport_key": "basketball_nba",
      "sport_title": "NBA",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "commence_time": "2026-09-03T23:10:00Z",
      "summary": {
        "total_meetings": 12,
        "home_wins": 7,
        "away_wins": 5,
        "draws": 0,
        "home_win_pct": 58.3,
        "away_win_pct": 41.7
      },
      "last_meetings": [
        {
          "date": "2026-02-14",
          "home_team": "Boston Celtics",
          "away_team": "Los Angeles Lakers",
          "home_score": 118,
          "away_score": 109,
          "venue": "TD Garden"
        }
      ],
      "updated_at": "2026-09-03T09:00:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/gameday/liveFree tier

Live/in-progress and recently-finished game states — scores, status (pre/live/final), completion flag.

Shortest cache window of the gameday group: 60s free / 15s api, reflecting the need for near-real-time scores. Ordered commence_time descending (most recently started first) unlike other gameday routes which order ascending.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (also accepts `sport`).
sportstringNoAlias for sport_key.
event_idstringNoFilter to a single event.
statusenum(pre|live|final)NoFilter to a specific game status; other values are ignored (no filter applied).
limitnumberNo50Max rows returned. Clamped to 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/live?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "basketball_nba_20260903_lal_bos",
      "sport_key": "basketball_nba",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "commence_time": "2026-09-03T23:10:00Z",
      "home_score": 54,
      "away_score": 49,
      "completed": false,
      "status": "live",
      "last_update": "2026-09-03T23:45:12Z",
      "updated_at": "2026-09-03T23:45:12Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": "basketball_nba",
    "event_id": null,
    "status": "live",
    "timestamp": "2026-09-03T23:45:20Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/gameday/propsFree tier

Player prop best-odds, aggregated across bookmakers and grouped by event; underlying storage is one row per (bookmaker, market, outcome) quote.

`limit` counts events, but rows are per-prop-quote, so the route over-fetches up to limit*40 (max 2000) prop rows to try to cover ~limit events after grouping — high-volume slates can still under-return events. Each prop is a best-odds aggregate across books (bet_link points to the best-odds book only, other books in book_count have no exposed URL). Cache floor enforced at >=300s regardless of declared tier value (see CLAUDE.md Firestore-read-cost rule).

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (also accepts `sport`).
sportstringNoAlias for sport_key.
event_idstringNoFilter to a single event.
marketstringNoFilter to a single prop market key, e.g. `player_points`. Lowercased.
player_slugstringNoFilter to a single player by slugified name (lowercase, hyphenated).
limitnumberNo50Max EVENTS returned (not prop rows). Clamped to 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/props?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "basketball_nba_20260903_lal_bos",
      "sport_key": "basketball_nba",
      "sport_title": "NBA",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "commence_time": "2026-09-03T23:10:00Z",
      "props": [
        {
          "market_key": "player_points",
          "player_name": "Jayson Tatum",
          "player_slug": "jayson-tatum",
          "line": 27.5,
          "side": "over",
          "best_odds": 1.95,
          "best_bookmaker": "sportsbet",
          "book_count": 6,
          "bet_link": "https://www.sportsbet.com.au/..."
        }
      ],
      "prop_count": 1,
      "updated_at": "2026-09-03T10:00:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": "basketball_nba",
    "event_id": null,
    "market": null,
    "player_slug": null,
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/gameday/summariesFree tier

AI-generated pre-game summaries/headlines/storylines and best-market snapshot per event.

Longest cache window of the gameday group: 600s free / 120s api (summaries change slowly, tied to the CLAUDE.md Firestore-read-cost 600s-window rule for gameday-data.ts). event_id does a direct doc get; otherwise a sport-filtered query ordered by commence_time ascending.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (also accepts `sport`).
sportstringNoAlias for sport_key.
event_idstringNoDirect doc lookup for a single event's summary.
limitnumberNo50Max rows returned. Clamped to 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/summaries?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "basketball_nba_20260903_lal_bos",
      "sport_key": "basketball_nba",
      "sport_title": "NBA",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "commence_time": "2026-09-03T23:10:00Z",
      "summary": "Boston enters as 4.5-point favorites off a five-game win streak...",
      "headline": "Celtics eye sixth straight win as Lakers battle injuries",
      "key_storylines": [
        "Lakers PG questionable",
        "Celtics 8-2 ATS at home"
      ],
      "best_h2h": {
        "selection": "Boston Celtics",
        "odds": 1.65,
        "bookmaker": "sportsbet"
      },
      "best_spread": {
        "selection": "Boston Celtics -4.5",
        "odds": 1.9,
        "bookmaker": "ladbrokes"
      },
      "best_total": {
        "selection": "Over 224.5",
        "odds": 1.87,
        "bookmaker": "sportsbet"
      },
      "generated_at": "2026-09-03T08:00:00Z",
      "updated_at": "2026-09-03T08:00:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": "basketball_nba",
    "event_id": null,
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/lineupsFree tier

Named team lineups for upcoming fixtures \u2014 the confirmed squad each club names ahead of game day.

Reads from team lineup tables. Post-filters by date/team/round/confirmed since the Supabase read returns all rows. Sorted by date descending. Cached 300s.

Query parameters
NameTypeRequiredDefaultDescription
sportstringNoFilter by sport_key ("aussierules_afl" or "rugbyleague_nrl"). Omit to return both.
datestringNoFilter by match date (YYYY-MM-DD, exact match).
teamstringNoFilter by team name (partial match, case-insensitive).
roundnumberNoRound number (integer).
confirmedstringNo"true" or "false" to filter by confirmation status.
limitnumberNo100Max results, clamped 1-200 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/lineups?sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "lineup_afl_20260906_abc",
      "gameId": "afl_20260906_abc",
      "date": "2026-09-06",
      "sportKey": "aussierules_afl",
      "homeTeamName": "Collingwood",
      "awayTeamName": "Carlton",
      "home": {
        "players": [
          {
            "playerId": "p001",
            "playerName": "Nick Daicos",
            "position": "Midfielder",
            "order": 1
          }
        ],
        "formation": null
      },
      "away": {
        "players": [
          {
            "playerId": "p002",
            "playerName": "Patrick Cripps",
            "position": "Midfielder",
            "order": 1
          }
        ],
        "formation": null
      },
      "venue": "MCG",
      "venueCity": "Melbourne",
      "roundNumber": 23,
      "confirmed": true,
      "_fetchedAt": "2026-09-05T10:00:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "filter": {
      "sport": "aussierules_afl",
      "date": null,
      "team": null,
      "round": null,
      "confirmed": null
    },
    "sport_breakdown": {
      "aussierules_afl": 1
    },
    "note": "Named team lineups for upcoming fixtures."
  }
}

Opportunities

7 endpoints

Krok's built-in scanners — arbitrage, positive EV, middles, low-holds, player props and same-game multi (SGM) picks, already devigged against a blended sharp baseline.

GET/api/v1/opportunitiesFree tier

Umbrella/legacy endpoint that dispatches to the same underlying data as the dedicated opportunity-type routes, selected via `type`.

Legacy/umbrella wrapper around `fetchOpportunitiesData` (src/lib/v1-opportunities-data.ts) — response shape per row depends on `type` and is not normalized to one schema across types. Response includes a static `endpoints` hint block pointing to unrelated racing endpoints (`/v1/racing/arbs`, `/v1/racing/movers`), which do not correspond to any of the 14 files audited here — likely stale/aspirational documentation baked into the payload. Rate-limit headers use `limit` (the row cap) rather than the usual free/api tier request cap seen on other routes.

Query parameters
NameTypeRequiredDefaultDescription
typeenum(all|positive-ev|snipes|middles|racing|playerprops)NoallWhich opportunity category to fetch; `positive-ev` is aliased internally to `snipes`. Determines the feature-gate applied (snipes→positive_ev, middles→middles, racing→racing, playerprops→player_props; `all` has no per-type gate).
sport_keystringNoFilter by sport key. Lowercased.
sportstringNoFilter by sport name; uppercased before matching.
minvaluenumberNo0Minimum opportunity value/edge threshold (also accepts `min_value`).
min_valuenumberNo0Alias for minvalue.
limitnumberNo100Max rows returned. Clamped to 100 (free) / 500 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities?type=all"
Example response
{
  "success": true,
  "data": [
    {
      "id": "snipe_abc123",
      "sport": "NBA",
      "sport_key": "basketball_nba",
      "event": "Boston Celtics vs Los Angeles Lakers",
      "market": "h2h",
      "selection1": "Boston Celtics",
      "bookmaker1": "sportsbet",
      "odds1": 2.1,
      "value": 6.4,
      "commence_time": "2026-09-03T23:10:00Z"
    }
  ],
  "meta": {
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "timestamp": "2026-09-03T10:06:00Z"
  },
  "endpoints": [
    {
      "path": "/v1/racing/arbs",
      "description": "Australian racing arbitrage opportunities",
      "parameters": [
        "venue",
        "race_type (T/H/G)",
        "minedge",
        "limit"
      ]
    },
    {
      "path": "/v1/racing/movers",
      "description": "Racing steamers and drifters (significant market movers)",
      "parameters": [
        "venue",
        "race_type (T/H/G)",
        "movement_type (steamer|drifter)",
        "min_movement",
        "limit"
      ]
    }
  ]
}
GET/api/v1/opportunities/arbitrageFree tier

Cross-bookmaker arbitrage (surebet) opportunities — two legs that lock in guaranteed profit regardless of outcome.

Supports keyset pagination via cursor/next_cursor and field projection via `fields`. Drops arbs for events that started >12h ago and rows whose freshness stamp is >5min old or with insane odds (isPriceFresh/hasSaneOdds). Betfair Exchange legs are stripped for hobby-equivalent (free) tier via filterExchangeForHobby. `status` (live/upcoming) is derived from commence_time, not stored. Cache 300s both tiers.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key. Lowercased.
sportstringNoFilter by sport (also accepts sport_key as fallback); uppercased.
minvaluenumberNo0Minimum arb value/edge % (also accepts min_value).
min_valuenumberNo0Alias for minvalue.
bookmakerstringNoSubstring match against either leg's bookmaker (case-insensitive).
limitnumberNo100Max rows per page. Clamped to 100 (free) / 10000 (api).
cursorstringNoOpaque keyset-pagination cursor from a prior response's meta.next_cursor.
fieldsstring (comma-separated)NoField projection — restrict each row to only the named fields.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities/arbitrage?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "arb_9f2c1a",
      "event": "Boston Celtics vs Los Angeles Lakers",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "sport": "NBA",
      "sport_key": "basketball_nba",
      "market": "h2h",
      "selection1": "Boston Celtics",
      "selection2": "Los Angeles Lakers",
      "bookmaker1": "sportsbet",
      "bookmaker2": "ladbrokes",
      "odds1": 2.05,
      "odds2": 2.15,
      "bet_link1": "https://www.sportsbet.com.au/...",
      "bet_link2": "https://www.ladbrokes.com.au/...",
      "line": null,
      "value": 2.35,
      "instructions": "Stake $511.20 on Boston Celtics @ sportsbet, $488.80 on Los Angeles Lakers @ ladbrokes",
      "tool_type": "surebet",
      "status": "upcoming",
      "commence_time": "2026-09-03T23:10:00Z",
      "updated_at": "2026-09-03T09:55:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "next_cursor": "eyJ2IjoyLjM1LCJpZCI6ImFyYl85ZjJjMWEifQ==",
    "filters": {
      "sport": null,
      "sport_key": null,
      "bookmaker": null,
      "min_value": null
    },
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/opportunities/low-holdsFree tier

Two-way markets where the bookmaker overround (vig) across two books is unusually low, near break-even for bettors.

Fastest-refreshing opportunities route: cache 60s (free) / 15s (api) vs 300s for arbitrage/middles/positive-ev. Sorted ascending by hold percentage (lowest hold first). Same staleness (12h commence cutoff) and price-freshness (5min) filtering, plus filterExchangeForHobby, as arbitrage. Keyset pagination + field projection supported.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key. Lowercased.
sportstringNoFilter by sport (also accepts sport_key fallback); uppercased.
minvaluenumberNo0Minimum value threshold (also accepts min_value); checks value/holdPct alias chain.
min_valuenumberNo0Alias for minvalue.
max_holdnumberNoUpper bound on hold percentage (holdPct or value).
bookmakerstringNoSubstring match against either leg's bookmaker.
limitnumberNo100Max rows per page. Clamped to 100 (free) / 10000 (api).
cursorstringNoOpaque keyset-pagination cursor.
fieldsstring (comma-separated)NoField projection list.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities/low-holds?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "lowhold_7ab391",
      "sport": "AFL",
      "sport_key": "aussierules_afl",
      "event": "Collingwood vs Essendon",
      "home_team": "Collingwood",
      "away_team": "Essendon",
      "market": "h2h",
      "commence_time": "2026-09-03T08:40:00Z",
      "bookmaker1": "betright",
      "selection1": "Collingwood",
      "odds1": 1.87,
      "bookmaker2": "tab",
      "selection2": "Essendon",
      "odds2": 2.05,
      "bet_link1": "https://www.betright.com.au/...",
      "bet_link2": "https://www.tab.com.au/...",
      "line": null,
      "value": 1.8,
      "hold_pct": 1.8,
      "profit_percent": 1.8,
      "instructions": "Combined hold across both books is 1.8% — near break-even.",
      "tool_type": "low_hold",
      "status": "upcoming",
      "updated_at": "2026-09-03T09:50:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "next_cursor": null,
    "filters": {
      "sport": "AFL",
      "sport_key": null,
      "bookmaker": null,
      "min_value": null,
      "max_hold": null
    },
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/opportunities/middlesFree tier

Middle opportunities — two different lines/points across bookmakers where both bets can win if the result lands between them.

Only middles route without a `filters` block in meta (present on arbitrage/low-holds/positive-ev but not middles). Same 12h staleness + 5min freshness filtering as arbitrage. No filterExchangeForHobby call here (unlike arbitrage/low-holds), so Betfair Exchange legs are NOT stripped for free tier on this route. Cache 300s both tiers.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key. Lowercased.
sportstringNoFilter by sport (also accepts sport_key fallback); uppercased.
minvaluenumberNo0Minimum middle value (also accepts min_value).
min_valuenumberNo0Alias for minvalue.
limitnumberNo100Max rows per page. Clamped to 100 (free) / 10000 (api).
cursorstringNoOpaque keyset-pagination cursor.
fieldsstring (comma-separated)NoField projection list.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities/middles?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "middle_44d1c9",
      "event": "Sydney Swans vs Geelong Cats",
      "home_team": "Sydney Swans",
      "away_team": "Geelong Cats",
      "sport": "AFL",
      "sport_key": "aussierules_afl",
      "market": "spreads",
      "selection1": "Sydney Swans -6.5",
      "selection2": "Geelong Cats +9.5",
      "bookmaker1": "sportsbet",
      "bookmaker2": "pointsbet",
      "odds1": 1.91,
      "odds2": 1.95,
      "bet_link1": "https://www.sportsbet.com.au/...",
      "bet_link2": "https://www.pointsbet.com.au/...",
      "value": 4.2,
      "line1": -6.5,
      "line2": 9.5,
      "middle_window": 3,
      "worst_case_loss": -12.5,
      "best_case_profit": 87.5,
      "instructions": "Bet Sydney -6.5 @ sportsbet and Geelong +9.5 @ pointsbet.",
      "tool_type": "middle",
      "status": "upcoming",
      "commence_time": "2026-09-03T08:40:00Z",
      "updated_at": "2026-09-03T09:52:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "next_cursor": null,
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/opportunities/player-propsFree tier

Individual player-prop bets with a positive expected-value edge, optionally enriched with historical hit-rate stats.

`historical_stats` is only populated when include_stats=true AND tier !== 'free' — free-tier requests get `historical_stats: undefined` (omitted key) even if include_stats=true. Underlying query scans a 12h commence_time window (max 5000 rows) then sorts by ev_percentage in memory rather than at the DB level, because there's no index for a global ev_percentage sort (avoids Postgres statement timeout). No cursor/fields projection support (unlike arbitrage/low-holds/middles/positive-ev). Filters out rows where player_name looks like a market/selection label (regex-based artifact cleanup). Cache 60s free / 15s api.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key. Lowercased.
sportstringNoFilter by sport (also accepts sport_key fallback); lowercased (unlike other opportunities routes which uppercase).
marketstringNoExact match against market_key, e.g. `player_points`.
playerstringNoSubstring match against player_name (case-insensitive).
bookmakerstringNoSubstring match against bookmaker.
min_evnumberNo0Minimum ev_percentage threshold.
include_statsbooleanNofalseWhen `true` (and tier is not free), joins in historical_stats (hit rates, streaks) per prop from player_props_stats.
limitnumberNo100Max rows returned. Clamped to 100 (free) / 10000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities/player-props?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "prop_e8a112",
      "event_id": "basketball_nba_20260903_lal_bos",
      "event": "Boston Celtics vs Los Angeles Lakers",
      "sport": "basketball_nba",
      "sport_key": "basketball_nba",
      "market_key": "player_points",
      "player_name": "Jayson Tatum",
      "line": 27.5,
      "side": "over",
      "odds": 1.95,
      "bookmaker": "sportsbet",
      "bet_link": "https://www.sportsbet.com.au/...",
      "ev_percentage": 7.8,
      "commence_time": "2026-09-03T23:10:00Z",
      "historical_stats": {
        "sample_size": 22,
        "hit_rate_over": 0.68,
        "hit_rate_under": 0.32,
        "last_5_results": [
          true,
          true,
          false,
          true,
          true
        ],
        "last_5_hit_rate": 0.8,
        "streak": "over_2",
        "line_bucket": "25-30",
        "updated_at": "2026-09-02T22:00:00Z"
      }
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 100,
    "requested_limit": 100,
    "filters": {
      "sport": null,
      "sport_key": "basketball_nba",
      "player": null,
      "bookmaker": null,
      "market": null,
      "min_ev": null
    },
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/opportunities/positive-evFree tier

Single-leg bets priced favorably against a sharp reference price (e.g. Betfair Exchange fair odds) — positive expected value snipes.

`bookmaker2` falls back to the synthetic string 'Betfair Fair' (a reference price, not a bettable book) when no second real book is quoted; bet_link2 is only populated when a genuine second bookmaker exists. `value_indicator` (overpriced/underpriced/fair) is computed by comparing odds1 to sharpPrice. Same 12h staleness + 5min freshness filtering as arbitrage/middles. No filterExchangeForHobby applied. Cache 300s both tiers.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key. Lowercased.
sportstringNoFilter by sport (also accepts sport_key fallback); uppercased.
minvaluenumberNo0Minimum EV value (also accepts min_value).
min_valuenumberNo0Alias for minvalue.
bookmakerstringNoSubstring match against either leg's bookmaker.
min_oddsnumberNo0Minimum odds1 threshold (also accepts minOdds).
minOddsnumberNo0Alias for min_odds.
limitnumberNo100Max rows per page. Clamped to 100 (free) / 10000 (api).
cursorstringNoOpaque keyset-pagination cursor.
fieldsstring (comma-separated)NoField projection list.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities/positive-ev?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "snipe_5c02af",
      "event": "Boston Celtics vs Los Angeles Lakers",
      "home_team": "Boston Celtics",
      "away_team": "Los Angeles Lakers",
      "sport": "NBA",
      "sport_key": "basketball_nba",
      "market": "h2h",
      "selection1": "Boston Celtics",
      "bookmaker1": "sportsbet",
      "bookmaker2": "Betfair Fair",
      "odds1": 1.95,
      "odds2": null,
      "bet_link1": "https://www.sportsbet.com.au/...",
      "bet_link2": null,
      "line": null,
      "value": 5.6,
      "value_indicator": "underpriced",
      "sharp_price": 1.85,
      "confidence": "high",
      "instructions": "Back Boston Celtics @ sportsbet 1.95, sharp fair price implies 1.85.",
      "tool_type": "positive_ev",
      "status": "upcoming",
      "commence_time": "2026-09-03T23:10:00Z",
      "updated_at": "2026-09-03T09:58:00Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "next_cursor": null,
    "filters": {
      "sport": null,
      "sport_key": "basketball_nba",
      "bookmaker": null,
      "min_value": null,
      "min_odds": null
    },
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}
GET/api/v1/opportunities/sgm-picksFree tier

AI-generated Same-Game-Multi (SGM) leg combinations with confidence tier, fair/minimum odds, and combined win probability.

Only opportunities route that reads via getAdminDb() (Firestore) as its primary path, with an isSupabase('sgm_picks') feature-flagged Supabase attempt first that falls back to Firestore on failure — returns 503 'Database unavailable' if Firestore admin isn't initialized, even if Supabase would have worked (503 check happens before the Supabase attempt). Only future events are returned (commenceTime >= now); no staleness/freshness filtering like the other opportunities routes since these are pre-game AI picks, not live odds edges. Cache 60s free / 15s api. No meta.limit/requested_limit/next_cursor fields in the response (unlike arbitrage/middles/etc).

Query parameters
NameTypeRequiredDefaultDescription
sportstringNoFilter by sport key (also accepts sport_key). Lowercased.
sport_keystringNoAlias for sport.
tierenum(safe|value|longshot)NoFilter to a risk tier; any other value is ignored (no filter).
min_confidencenumberNo1Minimum confidence score, clamped to 1-5.
limitnumberNo20Max rows returned. Clamped to 50 (free) / 1000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities/sgm-picks?sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "sgm_pick_a91cd0",
      "event_id": "basketball_nba_20260903_lal_bos",
      "event": "Boston Celtics vs Los Angeles Lakers",
      "sport": "NBA",
      "sport_key": "basketball_nba",
      "commence_time": "2026-09-03T23:10:00Z",
      "tier": "value",
      "confidence": 4,
      "confidence_label": "High",
      "legs": [
        {
          "market": "player_points",
          "selection": "Jayson Tatum Over 27.5",
          "odds": 1.9
        },
        {
          "market": "h2h",
          "selection": "Boston Celtics",
          "odds": 1.65
        }
      ],
      "fair_odds_conservative": 2.9,
      "minimum_acceptable_odds": 2.6,
      "combined_probability": 0.365,
      "suggested_bookmaker": "sportsbet",
      "bet_link": "https://www.sportsbet.com.au/...",
      "resolution": "pending"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "timestamp": "2026-09-03T10:06:00Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00Z"
    }
  }
}

Racing

17 endpoints

The full Australian racing stack — thoroughbred, harness and greyhound. Meetings and runner fields, form, sectionals, ratings, market movers, exchange arbs, tote pools and results.

GET/api/v1/racing/arbsFree tier

Live cross-bookmaker arbitrage opportunities on Australian racing (win markets), read from the racing_opportunities/arbs doc (Supabase-first, Firestore fallback).

Reads a single pre-aggregated doc (racing_opportunities/arbs), not a live scan; filters applied in JS after fetch. Cache 60s free / 15s api. Legacy inline auth boilerplate (not using shared authorizeRacing helper).

Query parameters
NameTypeRequiredDefaultDescription
venuestringNoCase-insensitive substring match on meeting venue name.
race_typestringNoExact match on race type code (e.g. T/H/G) as stored on the arb record.
minedgenumberNo0Minimum arb edge (as a fraction, e.g. 0.03 = 3%) to include.
limitintegerNo50Max rows returned; capped at 100 (free) / 500 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/arbs?venue=Randwick"
Example response
{
  "success": true,
  "data": [
    {
      "id": "arb_20260903_randwick_r5_furioustempo",
      "venue": "Randwick",
      "race_number": 5,
      "race_name": "Group 3 Show County Quality",
      "race_type": "T",
      "jump_time": "2026-09-03T04:35:00.000Z",
      "runner": "Furious Tempo",
      "arb_type": "win",
      "leg1_bookmaker": "Sportsbet",
      "leg1_odds": 4.8,
      "leg1_bet_link": "https://www.sportsbet.com.au/search?q=Furious%20Tempo",
      "leg2_bookmaker": "Betfair",
      "leg2_odds": 5.4,
      "leg2_bet_link": "https://www.betfair.com.au/exchange/plus/search?q=Furious%20Tempo",
      "edge": 0.0231,
      "stake1_pct": 52.9,
      "stake2_pct": 47.1,
      "all_legs": null,
      "detected_at": "2026-09-03T03:58:12.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/connectionsFree tier

Jockey/trainer profile lookup by name — Betfair form win-strike-rate, FormFav per-track stats, and Racing.com career/current-season profile, merged.

Names always echo back as rows with null strike rates when unmatched — `meta.matched` (not `count`) is the true 'has data' signal. Slug resolution tries multiple candidate spellings (connectionSlugCandidates); dedupes by resolved slug.

Query parameters
NameTypeRequiredDefaultDescription
namesstring (comma-separated)YesOne or more jockey/trainer names to look up (max 50).
typestring enumNobothOne of jockey | trainer | both.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/connections?names=value"
Example response
{
  "success": true,
  "data": {
    "jockeys": [
      {
        "name": "James McDonald",
        "slug": "james-mcdonald",
        "matched_slug": "james-mcdonald",
        "win_strike_rate": 24.6,
        "runs": 512,
        "wins": 126,
        "track_stats": [
          {
            "venue": "Randwick",
            "total_starts": 88,
            "wins": 24,
            "places": 41,
            "win_rate": 27.3,
            "place_rate": 46.6
          }
        ],
        "racing_com": {
          "code": "j4821",
          "career_wins": 1834,
          "career_starts": 8422,
          "win_percent": 21.8,
          "place_percent": 47.2,
          "recent_win_percent": 26.1,
          "current_wins": 92,
          "current_starts": 344,
          "current_seconds": 58,
          "current_thirds": 44,
          "current_vic_metro_starts": 12,
          "current_vic_metro_wins": 3,
          "current_vic_country_starts": 0,
          "current_vic_country_wins": 0,
          "current_sa_metro_starts": 0,
          "current_sa_metro_wins": 0,
          "current_sa_country_starts": 0,
          "current_sa_country_wins": 0,
          "victorian_ranking": null,
          "location": "Sydney, NSW",
          "apprentice": false,
          "weight_average": 57.2
        }
      }
    ],
    "trainers": []
  },
  "meta": {
    "count": 1,
    "matched": 1,
    "empty": false,
    "tier": "free",
    "type": "jockey",
    "names": [
      "James McDonald"
    ],
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/connections/combinationsFree tier

Win-strike-rate stats for jockey/trainer/horse combinations, computed from the last 90 days of racing_historical_results.

Computed in-process over up to 5000 racing_historical_results rows from the last 90 days (Supabase racing_historical_results, Firestore fallback) — not a precomputed table. Sorted by wins desc then runs desc.

Query parameters
NameTypeRequiredDefaultDescription
typestring enumNojockey_trainerOne of jockey_trainer | jockey_horse | trainer_horse — which combo pair to aggregate.
namesstring (comma-separated)NoFilter combos where either side's name contains one of these (case-insensitive); max 50 entries.
limitintegerNo50Max rows returned; capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/connections/combinations?type=jockey_trainer"
Example response
{
  "success": true,
  "data": [
    {
      "primary": "j mcdonald",
      "secondary": "c waller",
      "runs": 41,
      "wins": 11,
      "win_strike_rate": 26.83
    },
    {
      "primary": "j mcdonald",
      "secondary": "j cummings",
      "runs": 22,
      "wins": 5,
      "win_strike_rate": 22.73
    }
  ],
  "meta": {
    "count": 2,
    "empty": false,
    "tier": "free",
    "type": "jockey_trainer",
    "names": null,
    "window": "90 days",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/connections/season-statsFree tier

Per-season (Aug–Jul AU racing season) jockey/trainer stats broken down by venue and race type, from racing_results_flat.

503 'Season stats require Supabase backend' if DATA_BACKEND isn't Supabase for 'racing' — this endpoint has NO Firestore fallback. Queries racing_results_flat with .ilike on jockey/trainer column.

Query parameters
NameTypeRequiredDefaultDescription
namestringYesJockey or trainer name (case-insensitive).
typestring enumNojockeyOne of jockey | trainer.
seasonstringNocurrent seasonSeason label e.g. '2025/26', or a single year (interpreted as Aug prevYear–Jul year).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/connections/season-stats?name=value"
Example response
{
  "success": true,
  "data": {
    "name": "James McDonald",
    "type": "jockey",
    "season": "2025/26",
    "overall": {
      "starts": 344,
      "wins": 92,
      "places": 194,
      "win_rate": 26.74,
      "place_rate": 56.4
    },
    "by_venue": [
      {
        "venue": "randwick",
        "starts": 88,
        "wins": 24,
        "places": 48,
        "win_rate": 27.27,
        "place_rate": 54.55
      },
      {
        "venue": "rosehill-gardens",
        "starts": 61,
        "wins": 15,
        "places": 33,
        "win_rate": 24.59,
        "place_rate": 54.1
      }
    ],
    "by_type": [
      {
        "race_type": "T",
        "starts": 344,
        "wins": 92,
        "places": 194,
        "win_rate": 26.74,
        "place_rate": 56.4
      }
    ]
  },
  "meta": {
    "tier": "free",
    "name": "James McDonald",
    "type": "jockey",
    "season": "2025/26",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/futuresFree tier

Upcoming major-race futures (Melbourne Cup, Cox Plate, Golden Rose etc.) merged from PointsBet futures markets and Amused/BlackStream racing feeds.

Venue is GUESSED from race/competition name text via a hardcoded RACE_VENUE map — not authoritative. Only future dates (startTime >= today) are queried. Uses shared authorizeRacing() helper (../_shared.ts).

Query parameters
NameTypeRequiredDefaultDescription
venuestringNoSubstring match on inferred venue (mapped from race/competition name via a well-known-race lookup table).
race_typestring enumNoOne of T | H | G.
datestring (YYYY-MM-DD)NoFilter to a specific date. When omitted, returns all meetings across all dates.
litebooleanNofalseWhen '1' or 'true', skips heavy enrichments (form, stats, sectionals, videos) for faster responses.
limitintegerNo50Max rows returned; capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/futures?venue=Randwick"
Example response
{
  "success": true,
  "data": [
    {
      "date": "2026-11-03",
      "venue": "Flemington",
      "race_name": "Melbourne Cup",
      "race_type": "T",
      "runners": [
        {
          "name": "Vauban",
          "odds": 8.5,
          "bookmaker": "PointsBet"
        },
        {
          "name": "Absurde",
          "odds": 12,
          "bookmaker": "PointsBet"
        }
      ],
      "prize_money": null,
      "distance": null,
      "conditions": null,
      "bookmakers": [
        "PointsBet",
        "Amused"
      ]
    }
  ],
  "meta": {
    "count": 1,
    "sources": [
      "PointsBet",
      "Amused"
    ],
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "timestamp": "2026-09-03T04:00:00.000Z"
  }
}
GET/api/v1/racing/internationalAPI plan

UK/IE and other international racecards and results, from racing_intl_racecards / racing_intl_results (The Racing API sourced).

Built on the shared makeV1Route() factory (v1-collection-route.ts) with feature: 'historical' — this is the ONLY racing endpoint that is NOT free-tier (historical/bulk_export are the only two paid-only features). 204 status + credit refund on empty result set.

Query parameters
NameTypeRequiredDefaultDescription
typestring enumNoracecardsOne of racecards | results — which collection to query.
datestring (YYYY-MM-DD)NoExact-match filter on race date.
limitintegerNo100Max rows returned; capped at 50 (free) / 200 (api). Note: gated to api-tier anyway via the 'historical' feature.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/international?type=racecards"
Example response
{
  "success": true,
  "data": [
    {
      "date": "2026-09-03",
      "venue": "Ascot",
      "country": "GB",
      "race_name": "3:35 Ascot - Handicap",
      "race_class": "Class 4",
      "distance_f": 8,
      "runners": [
        {
          "name": "Northern Lad",
          "jockey": "R Moore",
          "trainer": "C Appleby",
          "draw": 3,
          "or_rating": 78
        }
      ]
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 100,
    "requested_limit": 100,
    "type": "racecards",
    "date": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 1998,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/meetingsFree tier

The full racing board \u2014 today's (or filtered) AU/NZ/UK-IE race meetings with races and runners, heavily enriched (TAB cards, Racing.com sectionals/videos, FormFav form/stats, results, Betfair Exchange snapshots, cross-book market movers).

By far the heaviest endpoint — up to 6 sequential/parallel enrichment joins per meeting (TAB, intl racecards, Racing.com sectionals/videos, form, stats, results/TABNZ/Betfair/PuntersEdge). Cache 120s free / 30s api (both below the 300s CLAUDE.md floor per v1Revalidate() clamp logic). Full runner object has ~80 optional keys — stripped of nulls by default; use ?full=1 to restore. UK/IE meetings synthesized as type 'R' but counted as 'T' unless H/G explicitly requested.

Query parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)NoFilter to a specific date. When omitted, returns all meetings across all dates.
race_typestring enumNoOne of T | H | G. Alias: `type`.
typestring enumNoAlias for race_type.
venuestringNoCase-insensitive substring match on venue name.
jurisdictionstringNoState/tote-jurisdiction code (e.g. NSW, VIC). Alias: `state`.
statestringNoAlias for jurisdiction.
limitintegerNo50Max meetings returned; capped at 50 (free) / 1000 (api).
fullboolean ("1")No0When '1', returns the explicit-null shape (all ~80 runner keys present even if null); default strips null/empty-array fields to shrink payload.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/meetings?date=2026-09-01"
Example response
{
  "success": true,
  "data": [
    {
      "id": "tab_1234567",
      "meeting_key": "2026-09-03_flemington",
      "venue": "Flemington",
      "state": "VIC",
      "country": null,
      "tote_jurisdiction": "VIC",
      "type": "T",
      "date": "2026-09-03",
      "race_count": 8,
      "next_jump": "2026-09-03T04:35:00.000Z",
      "weather": {
        "summary": "Partly cloudy",
        "temp_c": 16,
        "rain_24h_mm": 0,
        "wind_kmh": 14,
        "wind_dir": "SW"
      },
      "track_hint": "Good 4",
      "prize_money": 180000,
      "races": [
        {
          "id": "1234567_5",
          "race_key": "2026-09-03_flemington_5",
          "number": 5,
          "jump_time": "2026-09-03T04:35:00.000Z",
          "name": "Turnbull Stakes",
          "distance": 2000,
          "race_class": "Group 1",
          "status": "open",
          "track_condition": "Good 4",
          "runner_count": 10,
          "allowed_bet_types": [
            "WIN",
            "PLC",
            "EXA",
            "TRI"
          ],
          "runners": [
            {
              "id": "r1",
              "number": 3,
              "name": "Zaaki",
              "barrier": 6,
              "bib": null,
              "gear": null,
              "jockey": "J Bowman",
              "trainer": "A Freedman",
              "weight": 58.5,
              "is_scratched": false,
              "best_win": 3.2,
              "best_win_bookmaker": "Sportsbet",
              "odds": [
                {
                  "bookmaker": "Sportsbet",
                  "win": 3.2,
                  "place": 1.5,
                  "bet_link": "https://www.sportsbet.com.au/search?q=Zaaki"
                },
                {
                  "bookmaker": "Betfair",
                  "win": 3.4,
                  "matched": 42000,
                  "bet_link": "https://www.betfair.com.au/exchange/plus/search?q=Zaaki"
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/moversFree tier

Live odds steamers/drifters (significant price movements) across AU racing markets, read from racing_opportunities/movers.

Reads a single doc combining `movers` + `drifters` arrays. Falls back Firestore→Supabase if the Supabase doc is missing or both arrays are empty. Legacy inline auth (not shared _shared.ts helper).

Query parameters
NameTypeRequiredDefaultDescription
venuestringNoCase-insensitive substring match on meeting venue.
race_typestringNoExact match on race type code.
movement_typestring enumNoOne of steamer | drifter.
min_movementnumberNo0Minimum absolute movement percentage to include.
limitintegerNo50Max rows returned; capped at 100 (free) / 500 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/movers?venue=Randwick"
Example response
{
  "success": true,
  "data": [
    {
      "race_id": "1234567_5",
      "venue": "Caulfield",
      "race_number": 5,
      "race_name": "Memsie Stakes",
      "race_type": "T",
      "jump_time": "2026-09-03T05:10:00.000Z",
      "runner": "Alligator Blood",
      "runner_number": 4,
      "bookmaker": "Ladbrokes",
      "bet_link": "https://www.ladbrokes.com.au/search?q=Alligator%20Blood",
      "opening_odds": 6.5,
      "current_odds": 4.2,
      "movement_pct": -35.4,
      "movement_type": "steamer"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/odds-historyFree tier

Per-runner price-movement curves (open \u2192 every fluctuation \u2192 close/SP/BSP) per bookmaker for a meeting/race/runner, built from race card data, historical results and odds snapshots.

Tier detail: free (but >14 days back requires api tier)

Free tier is hard-clamped to the last 14 days regardless of the account's `historicalDays` window (extra check beyond the standard historical-clamp gate) — returns 402 with X-Krok-Feature: historical. Tote/SP book keys (tote, vrc, racenet, etc.) are excluded from bookFlucs. Falls back card→historical-results→live-snapshot-only in that order per meeting. Uses shared _shared.ts auth (feature racing_premium, cost 5).

Query parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)Notoday (Sydney meeting date)Meeting date to fetch.
venuestringNoCase-insensitive substring match on venue.
raceNumberintegerNoFilter to a single race number. Alias: `race`.
raceintegerNoAlias for raceNumber.
runner_slugstringNoFilter to a single runner by slugified name.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/odds-history?date=today%20(Sydney%20meeting%20date)"
Example response
{
  "success": true,
  "data": [
    {
      "id": "tab_1234567",
      "venue": "Flemington",
      "date": "2026-09-03",
      "races": [
        {
          "number": 5,
          "name": "Turnbull Stakes",
          "status": "closed",
          "start_time": "2026-09-03T04:35:00.000Z",
          "runners": [
            {
              "number": 3,
              "name": "Zaaki",
              "is_scratched": false,
              "open": 3.6,
              "sp": 3.2,
              "bsp": 3.15,
              "flucs": [
                {
                  "timestamp": "2026-09-03T02:00:00.000Z",
                  "odds": 3.6
                },
                {
                  "timestamp": "2026-09-03T04:30:00.000Z",
                  "odds": 3.2
                }
              ],
              "bookFlucs": {
                "sportsbet": [
                  {
                    "timestamp": "2026-09-03T02:00:00.000Z",
                    "odds": 3.7
                  },
                  {
                    "timestamp": "2026-09-03T04:30:00.000Z",
                    "odds": 3.3
                  }
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "meta": {
    "count": 1,
    "date": "2026-09-03",
    "venue": null,
    "race_number": 5,
    "runner_slug": null,
    "tier": "free",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    },
    "source": "card"
  }
}
GET/api/v1/racing/predictionsFree tier

AI-powered win/place probability predictions for AU/NZ races, from the racing predictions model.

Built on makeV1Route() factory. Sorted by model_rank asc then win_probability desc. Fetches limit*4 rows when venue/code/horse filters are set (JS post-filter), capped at the requested limit afterward.

Query parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)NotodayRace date to filter.
venuestringNoCase-insensitive substring match on venue.
codestring enumNoRacing code: thoroughbred | harness | greyhound.
horsestringNoCase-insensitive substring match on horse name.
limitintegerNo200Max rows returned; capped at 200 (free) / 1000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/predictions?date=today"
Example response
{
  "success": true,
  "data": [
    {
      "id": "bf_pred_20260903_flemington_5_3",
      "date": "2026-09-03",
      "venue": "Flemington",
      "race_no": 5,
      "horse_name": "Zaaki",
      "win_probability": 0.31,
      "place_probability": 0.58,
      "model_rank": 1,
      "code": "thoroughbred"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 200,
    "requested_limit": 200,
    "date": "2026-09-03",
    "venue": null,
    "code": null,
    "horse": null,
    "source": "Krok Odds Predictions",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/ratingsFree tier

KrokBot AI pick detail for a single race — per-runner win probability/rating, field analysis verdicts, and Betfair money-flow tip signals, from racing_ai_picks.

404 with meta.status='not_yet_populated' if no pick exists for the race yet. `model` field is deliberately null (internal, not exposed via public API). Betfair money-flow tip join is best-effort (venue+raceNumber+sport, disambiguated by jumpTime proximity) — failure degrades gracefully, doesn't fail the request.

Query parameters
NameTypeRequiredDefaultDescription
race_idstringYesDirect racing_ai_picks doc id lookup.
meeting_idstringYesMeeting id, used with race_number as a fallback query.
race_numberintegerYesRace number within the meeting, used with meeting_id.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/ratings?race_id=abc123&meeting_id=abc123&race_number=10"
Example response
{
  "success": true,
  "data": {
    "race_id": "1234567_5",
    "meeting_id": "1234567",
    "race_name": "Turnbull Stakes",
    "race_number": 5,
    "venue": "Flemington",
    "state": "VIC",
    "race_type": "T",
    "jump_time": "2026-09-03T04:35:00.000Z",
    "date": "2026-09-03",
    "tier": "feature",
    "score": 82.4,
    "confidence": "high",
    "top_pick": {
      "slug": "zaaki",
      "name": "Zaaki",
      "bet_link": "https://www.sportsbet.com.au/search?q=Zaaki"
    },
    "runners": [
      {
        "slug": "zaaki",
        "name": "Zaaki",
        "win_prob": 0.31,
        "fair_odds": 3.23,
        "confidence": "high",
        "rank": 1,
        "edge_pct": 4.2,
        "value": true,
        "money_share": 0.28,
        "money_delta": 0.03,
        "rating": 88.5
      }
    ],
    "risk": "low",
    "pick_rationale": "Strong recent form at set weights over 2000m.",
    "track_condition": "Good 4",
    "bestWin": 3.2,
    "bestWinBookmaker": "Sportsbet",
    "value_picks": [],
    "market_mover": null
  },
  "meta": {
    "count": 8,
    "empty": false,
    "tier": "free",
    "race_id": "1234567_5",
    "meeting_id": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/resultsFree tier

Archive query across settled race results (Betfair BSP-based, lighter fields than the date-specific endpoint) with filters by track, race type, runner and date range.

Tier detail: free (date-range filters `since`/`until` require api tier via the 'historical' feature)

Explicit CREDIT_COST=5 archive endpoint; refunds the full credit cost when the result set is empty. Setting `since`/`until` on a free-tier key returns a 402 tier-gate error for the 'historical' feature BEFORE the per-key clamp runs. `track`/`runner_slug`/`race_name` filters are applied client-side after an over-fetch (up to 2000 rows).

Query parameters
NameTypeRequiredDefaultDescription
race_typestring enumNoOne of T | H | G.
track_slugstringNoExact match on venue slug.
trackstringNoCase-insensitive substring match on track name.
race_namestringNoCase-insensitive substring match on race name.
runner_slugstringNoExact match on a runner's slug within the race.
sincestring (YYYY-MM-DD)NoResults on/after this date. Requires 'historical' feature (api tier); also clamped per-key by verification.historicalDays.
untilstring (YYYY-MM-DD)NoResults on/before this date. Requires 'historical' feature (api tier).
limitintegerNo50Max rows returned; capped at 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/results?race_type=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "2026-09-03_flemington_5",
      "date": "2026-09-03",
      "track": "Flemington",
      "track_slug": "flemington",
      "state": "VIC",
      "race_no": 5,
      "race_type": "T",
      "distance_m": 2000,
      "race_name": "Turnbull Stakes",
      "winning_time": 121.4,
      "mile_rate": null,
      "winner": {
        "tab_number": 3,
        "name": "Zaaki",
        "slug": "zaaki",
        "jockey": "J Bowman",
        "trainer": "A Freedman",
        "win_bsp": 3.15
      },
      "runners": [
        {
          "tab_number": 3,
          "name": "Zaaki",
          "slug": "zaaki",
          "finish_position": 1,
          "win_result": 1,
          "win_bsp": 3.15,
          "jockey": "J Bowman",
          "trainer": "A Freedman"
        }
      ],
      "going": "Good 4",
      "race_class": "Group 1",
      "ingested_at": "2026-09-03T05:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "race_type": null,
    "track_slug": null,
    "race_name": null,
    "runner_slug": null,
    "since": null,
    "until": null,
    "data_note": "This endpoint returns Betfair BSP-based results (limited fields). For richer results including margins, times and full connections, use the date-specific endpoint /api/v1/racing/results/{date}.",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 937,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/results/{date}Free tier

Full settled race results for every meeting on a single date — finishing order, margins, times, BSP, jockey/trainer — merged with TAB tote pools and dividends.

Tier detail: free (subject to per-key historicalDays clamp)

Same `{date}_{venueSlug}_{raceNo}` race_key/meeting_key join keys as /racing/meetings. Sorts results by raceNo in JS (NOT in SQL) to avoid a full seq-scan on the unindexed data->>raceNo JSONB path — deliberate perf workaround. Cache 300s (respects the CLAUDE.md floor).

Path parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)NoMeeting date to fetch results for; 400 if not a valid YYYY-MM-DD string.
Query parameters
NameTypeRequiredDefaultDescription
venuestringNoCase-insensitive substring match on track name/slug. Alias: `track`.
trackstringNoAlias for venue.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/results/2026-09-01?venue=Randwick"
Example response
{
  "success": true,
  "data": [
    {
      "id": "2026-09-03_flemington_5",
      "race_key": "2026-09-03_flemington_5",
      "meeting_key": "2026-09-03_flemington",
      "date": "2026-09-03",
      "track": "Flemington",
      "track_slug": "flemington",
      "state": "VIC",
      "race_no": 5,
      "race_type": "T",
      "distance_m": 2000,
      "race_name": "Turnbull Stakes",
      "going": "Good 4",
      "race_class": "Group 1",
      "status": "settled",
      "winner": {
        "tab_number": 3,
        "name": "Zaaki",
        "slug": "zaaki",
        "jockey": "J Bowman",
        "trainer": "A Freedman",
        "win_bsp": 3.15
      },
      "pools": [
        {
          "pool_type": "WIN",
          "total": 184200
        }
      ],
      "dividends": [
        {
          "pool_type": "WIN",
          "tab_number": 3,
          "dividend": 3.4
        }
      ],
      "runners": [
        {
          "tab_number": 3,
          "name": "Zaaki",
          "finish_position": 1,
          "margin": 0,
          "win_result": 1,
          "win_bsp": 3.15,
          "jockey": "J Bowman",
          "trainer": "A Freedman",
          "barrier": 6,
          "weight": "58.5kg"
        },
        {
          "tab_number": 7,
          "name": "Buffalo River",
          "finish_position": 2,
          "margin": 0.8,
          "win_result": 0,
          "win_bsp": 6.4,
          "jockey": "M Zahra",
          "trainer": "C Maher",
          "barrier": 2,
          "weight": "56kg"
        }
      ],
      "exotic_pools": [
        {
          "type": "QUINELLA",
          "total": 42100
        }
      ],
      "ingested_at": "2026-09-03T05:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "date": "2026-09-03",
    "venue": null,
    "tier": "free",
    "timestamp": "2026-09-03T05:30:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/runner-formFree tier

A single runner's recent race history (form) across thoroughbred/harness/greyhound codes, with joined Racing.com sectionals and a Brightcove replay-video fallback resolver.

Per memory (runner-form Supabase path): the Supabase branch (`runner_recent_races` table) is gated behind isSupabase('runner-form') and was reported BLOCKED/undeployed as of the last audit — may silently fall back to the Firestore `runner_historical_stats/{sport}__{slug}/recent_races` subcollection path. Sectionals join is a single best-effort query per request (ilike on horseName).

Query parameters
NameTypeRequiredDefaultDescription
runner_slugstringYesSlugified runner name to look up.
sport_keystring enumNoall three codesOne of racing_T | racing_H | racing_G. Alias: `sport`.
sportstring enumNoAlias for sport_key.
sincestring (YYYY-MM-DD)NoOnly races on/after this date; clamped by the key's historicalDays window.
untilstring (YYYY-MM-DD)NoOnly races on/before this date.
limitintegerNo50Max rows returned; capped at 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/runner-form?runner_slug=value"
Example response
{
  "success": true,
  "data": [
    {
      "race_id": "2026-08-20_randwick_4",
      "sport_key": "racing_T",
      "runner_slug": "zaaki",
      "date": "2026-08-20",
      "track": "Randwick",
      "race_no": 4,
      "race_name": "Warwick Stakes",
      "distance": 1400,
      "win_result": 1,
      "place_result": 1,
      "win_bsp": 2.8,
      "tab_number": 5,
      "jockey": "J Bowman",
      "trainer": "A Freedman",
      "barrier": 3,
      "finish_position": 1,
      "margin": 1.2,
      "replay_video": "https://videos.krokodds.com.au/replay/2026-08-20_randwick_4.m3u8",
      "sectionals": {
        "l600": 34.1,
        "l400": 22.4,
        "l200": 11.3,
        "speed": {
          "early": 58.2,
          "mid": 59.8,
          "late": 61.4,
          "overall": 59.9,
          "peak": 62.1
        },
        "closing_ratio": 1.04,
        "runs_sampled": 8
      }
    }
  ],
  "meta": {
    "count": 1,
    "empty": false,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "runner_slug": "zaaki",
    "sport_key": null,
    "since": null,
    "until": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/runner-statsFree tier

Aggregated career/split stats for a single runner — win/place rates by track, distance, going, track+distance combo, and prep-stage (first-up/second-up/in-prep), plus sectionals and career prize money.

404 with meta.status='not_yet_populated' if no historical record AND no sectionals row exist. When sport_key is omitted, probes racing_T → racing_H → racing_G in order and returns the first with a career record (falls further back to racing_com_sectionals-only if no fetchRunnerStats hit).

Query parameters
NameTypeRequiredDefaultDescription
runner_slugstringYesSlugified runner name to look up.
sport_keystring enumNoprobes all three in orderOne of racing_T | racing_H | racing_G. Alias: `sport`.
sportstring enumNoAlias for sport_key.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/runner-stats?runner_slug=value"
Example response
{
  "success": true,
  "data": {
    "runner_slug": "zaaki",
    "sport_key": "racing_T",
    "historical": {
      "name": "Zaaki",
      "starts": 34,
      "wins": 11,
      "places": 20,
      "win_rate": 32.35,
      "place_rate": 58.82,
      "avg_win_bsp": 4.1,
      "last_track": "Randwick",
      "last_date": "2026-08-20"
    },
    "career_prize_money": 8420000,
    "sectionals": {
      "avg_l600": 34.5,
      "avg_l400": 22.8,
      "avg_l200": 11.5,
      "closing_ratio": 1.03,
      "avg_speed_early": 57.9,
      "avg_speed_mid": 59.4,
      "avg_speed_late": 61,
      "avg_overall_speed": 59.5,
      "avg_peak_speed": 61.8
    },
    "recent_races": [
      {
        "date": "2026-08-20",
        "venue": "Randwick",
        "finish": 1,
        "position": 1,
        "distance": 1400,
        "speed": 61.2
      }
    ],
    "splits": {
      "by_track": [
        {
          "bucket": "Randwick",
          "starts": 9,
          "wins": 4,
          "places": 6,
          "win_rate": 44.4,
          "place_rate": 66.7,
          "avg_bsp": 3.6
        }
      ],
      "by_distance": [
        {
          "bucket": "1400m",
          "starts": 12,
          "wins": 5,
          "places": 8,
          "win_rate": 41.7,
          "place_rate": 66.7,
          "avg_bsp": 3.9
        }
      ],
      "by_going": [
        {
          "bucket": "Good",
          "starts": 20,
          "wins": 8,
          "places": 13,
          "win_rate": 40,
          "place_rate": 65,
          "avg_bsp": 3.8
        }
      ],
      "by_track_distance": [],
      "by_prep_stage": {
        "first_up": {
          "bucket": "first_up",
          "starts": 6,
          "wins": 2,
          "places": 4,
          "win_rate": 33.3,
          "place_rate": 66.7,
          "avg_bsp": 4.5
        },
        "second_up": {
          "bucket": "second_up",
          "starts": 6,
          "wins": 3,
          "places": 4,
          "win_rate": 50,
          "place_rate": 66.7,
          "avg_bsp": 3.2
        },
        "in_prep": {
          "bucket": "in_prep",
          "starts": 22,
          "wins": 6,
          "places": 12,
          "win_rate": 27.3,
          "place_rate": 54.5,
          "avg_bsp": 4.4
        }
      },
      "class_change": null,
      "avg_days_between_starts": 28,
      "spell_length": 84,
      "form_string": "1-2x14",
      "best_bsp": 2.1,
      "best_bsp_label": "2026-06-14 Rosehill",
      "total_analysed": 34
    }
  },
  "meta": {
    "tier": "free",
    "runner_slug": "zaaki",
    "sport_key": "racing_T",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/sectionals/{horseCode}Free tier

Racing.com sectional times (L600/L400/L200, speed map) and profile (condition splits, first/second/third-up stats, sire progeny) for a horse, keyed by Racing.com's own horse code.

404 if BOTH racing_com_sectionals and racing_com_profiles docs are missing for that code. Cache 600s — sectionals are backfilled post-race and slow-moving. Uses the shared _shared.ts authorizeRacing() with default feature 'racing'.

Path parameters
NameTypeRequiredDefaultDescription
horseCodestringNoRacing.com horse code (alphanumeric/underscore/hyphen, 1-64 chars); 400 if it fails that pattern.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/sectionals/H123456"
Example response
{
  "success": true,
  "data": {
    "horse_code": "h284719",
    "name": "Zaaki",
    "sectionals": {
      "run_count": 8,
      "l600": 34.1,
      "l400": 22.4,
      "l200": 11.3,
      "speed": {
        "early": 58.2,
        "mid": 59.8,
        "late": 61.4,
        "overall": 59.9,
        "peak": 62.1
      },
      "closing_ratio": 1.04
    },
    "profile": {
      "condition_splits": {
        "firm": "2:1-0-1",
        "good": "24:9-5-3",
        "soft": "6:1-2-1",
        "heavy": "2:0-0-1",
        "wet": "8:1-2-2"
      },
      "first_up": "6:2-0-1",
      "second_up": "6:3-1-0",
      "third_up": "5:1-1-1",
      "winning_range": "1400m-2000m",
      "days_since_last_win": 96,
      "career_stats": "34:11-9-6",
      "sire_progeny_dry": "412:78-65-59",
      "sire_progeny_wet": "88:14-11-9"
    }
  },
  "meta": {
    "tier": "free",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/racing/tote-poolsFree tier

Parimutuel (tote) pool totals, dividends and multi-leg exotic pools per race for a meeting day.

Cache is 60s (below the 300s CLAUDE.md floor, but v1Revalidate() enforces the floor at the framework level regardless of the declared constant). Dividends array is typically empty pre-race and only populates once TAB settles the pool.

Query parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)Notoday (Sydney meeting date)Meeting date to fetch tote pool data for.
venuestringNoCase-insensitive substring match on venue.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/tote-pools?date=today%20(Sydney%20meeting%20date)"
Example response
{
  "success": true,
  "data": [
    {
      "id": "tab_1234567",
      "venue": "Flemington",
      "state": "VIC",
      "type": "T",
      "date": "2026-09-03",
      "tote_jurisdiction": "VIC",
      "exotic_pools": [
        {
          "type": "QUADDIE",
          "legs": [
            4,
            5,
            6,
            7
          ],
          "total": 612400,
          "jackpot": false
        }
      ],
      "races": [
        {
          "number": 5,
          "name": "Turnbull Stakes",
          "distance": 2000,
          "status": "open",
          "start_time": "2026-09-03T04:35:00.000Z",
          "pools": [
            {
              "pool_type": "WIN",
              "total": 184200
            },
            {
              "pool_type": "PLACE",
              "total": 96700
            }
          ],
          "dividends": []
        }
      ]
    }
  ],
  "meta": {
    "count": 1,
    "date": "2026-09-03",
    "venue": null,
    "tier": "free",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01"
    }
  }
}

Odds Feed

14 endpoints

Raw per-bookmaker prices as scraped, before any analysis layer. `/scraped-odds/*` paths are aliases of the equivalent `/odds-feed/*` route.

GET/api/v1/odds-feed/bookmakersFree tier

Catalogue of bookmakers KrokOdds sources directly (pure registry read, no DB hit).

Zero-cost registry read — no database hit. Cache 300s. `feeds[].status` is 'pending' when the sync is written/merged but the Cloud Function is not yet deployed — surfaced rather than hidden. `aliases` = white-label brands accepted by `?bookmaker=` on other endpoints, served from the parent book's pricing backend. meta.redistributable is @deprecated in favour of meta.direct_scrape (removal 2027-02-01).

Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/bookmakers"
Example response
{
  "success": true,
  "data": [
    {
      "key": "sportsbet",
      "name": "Sportsbet",
      "type": "corporate",
      "feeds": [
        {
          "kind": "sports",
          "status": "live"
        },
        {
          "kind": "racing",
          "status": "live"
        }
      ],
      "racing": true,
      "sports": true,
      "aliases": []
    },
    {
      "key": "betmakers",
      "name": "BetMakers Nimbus",
      "type": "platform",
      "feeds": [
        {
          "kind": "sports",
          "status": "live"
        },
        {
          "kind": "racing",
          "status": "pending"
        }
      ],
      "racing": false,
      "sports": true,
      "aliases": [
        {
          "key": "unibet",
          "name": "Unibet"
        },
        {
          "key": "betright",
          "name": "BetRight"
        }
      ]
    }
  ],
  "meta": {
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary",
    "note": "All feeds listed here are sourced directly by KrokOdds. Display-permitted in your own product with attribution; not licensed for resale as a standalone feed — see Terms 6.1. Aggregator-licensed odds are never served from /v1/odds-feed."
  }
}
GET/api/v1/odds-feed/clvFree tier

Historical closing-line odds (last snapshot per bookmaker before commence_time) from the CLV archive.

Backed by `clv_archive` table; Supabase-first with a Firestore fallback on query failure. `bookmaker` filter is applied in-memory AFTER the cache read so all sport/date combos share one cache entry. Cache 300s. `from` is clamped via clampHistoricalFrom to the caller's plan window. 400 on malformed from/to (must be YYYY-MM-DD).

Query parameters
NameTypeRequiredDefaultDescription
sportstringNoFilter by sport key, e.g. 'basketball_nba', 'aussierules_afl'.
bookmakerstringNoFilter by bookmaker key, e.g. 'sportsbet', 'tab'.
fromstring (YYYY-MM-DD)NoStart of commence_time date range. Clamped to the caller's historicalDays window.
tostring (YYYY-MM-DD)NoEnd of commence_time date range.
limitintegerNo50 (free) / 500 (api)Max results; capped per tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/clv?sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "nrl-panthers-storm-20260903",
      "sport": "rugbyleague_nrl",
      "sport_title": "NRL",
      "home_team": "Penrith Panthers",
      "away_team": "Melbourne Storm",
      "commence_time": "2026-09-03T09:30:00Z",
      "archived_at": "2026-09-03T09:29:10Z",
      "bookmakers": [
        {
          "key": "sportsbet",
          "markets": [
            {
              "key": "h2h",
              "outcomes": [
                {
                  "name": "Penrith Panthers",
                  "bet_link": "https://www.sportsbet.com.au/..."
                },
                {
                  "name": "Melbourne Storm",
                  "bet_link": "https://www.sportsbet.com.au/..."
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "meta": {
    "sport": "rugbyleague_nrl",
    "bookmaker": null,
    "date_from": "2026-08-27",
    "date_to": null,
    "limit": 50,
    "requested_limit": 50,
    "total_matched": 1,
    "truncated": false,
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary"
  }
}
GET/api/v1/odds-feed/prediction-marketsFree tier

Prediction-market odds from Polymarket and Kalshi with de-vigged probabilities and CLOB order-book enrichment.

Reads `external_prediction_markets` (Supabase), synced every 30 min by `predictionMarketSync` Cloud Function. Cache 600s — matches the sync interval, fresher would be wasted reads. 400 on invalid source/type enum values.

Query parameters
NameTypeRequiredDefaultDescription
sourcestring enumNoboth'polymarket' or 'kalshi'.
sportstringNoKrokOdds sport key, e.g. 'basketball_nba'.
typestring enumNo'game' or 'futures'.
limitintegerNo200Max 1000.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/prediction-markets?source=both"
Example response
{
  "success": true,
  "data": [
    {
      "id": "pm_0x8f21a",
      "source": "polymarket",
      "type": "game",
      "marketTitle": "Will the Sydney Kings win vs Melbourne United?",
      "sportKey": "basketball_nbl",
      "eventKey": "nbl-kings-united-20260903",
      "matchConfidence": 0.94,
      "impliedProbs": {
        "yes": 0.57,
        "no": 0.43
      },
      "rawPrices": {
        "yes": 0.57,
        "no": 0.43
      },
      "volume": 18450.25,
      "liquidity": 6200.5,
      "bestBid": 0.56,
      "bestAsk": 0.58,
      "spread": 0.02,
      "lastTradePrice": 0.57,
      "oneDayPriceChange": 0.03,
      "updatedAt": "2026-09-03T08:15:00Z"
    }
  ],
  "meta": {
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary",
    "sources": [
      "polymarket"
    ],
    "filter": {
      "source": null,
      "sport": "basketball_nbl",
      "type": null
    },
    "note": "Prediction-market contract prices de-vigged and enriched with CLOB order-book data. Synced every 30 minutes."
  }
}
GET/api/v1/odds-feed/racingFree tier

Racing meetings -> races -> runners -> per-book fixed/tote odds, normalised across all proprietary racing feeds.

Gated on 'racing' not 'scraped_odds' by design — both are free today; kept separate so racing can move to paid independently. Per-collection scan cap is 600 (TAB alone runs ~420 meetings on a busy day). TAB stores thoroughbred as 'R' internally; API normalises to 'T' in both query and output. date before the caller's historicalDays window returns 403. Cache 300s.

Query parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)Notoday (Australia/Melbourne)Meeting date, AU-local.
bookmakerstringNoCanonical key or white-label alias (e.g. 'swiftbet' resolves to bluebet).
venuestringNoSubstring match on venue slug.
race_typestring enumNoT/R (thoroughbred), H (harness), G (greyhound).
limitintegerNo10 (free) / 500 (api)Capped per tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/racing?date=today%20(Australia%2FMelbourne)"
Example response
{
  "success": true,
  "data": [
    {
      "bookmaker_key": "tab",
      "venue_slug": "flemington",
      "race_type": "T",
      "races": [
        {
          "race_number": 4,
          "runners": [
            {
              "number": 7,
              "name": "Golden Streak",
              "win": 4.2,
              "place": 1.65,
              "bet_link": "https://tab.com.au/..."
            }
          ]
        }
      ]
    }
  ],
  "meta": {
    "date": "2026-09-03",
    "bookmaker": "tab",
    "brand": null,
    "limit": 10,
    "requested_limit": 10,
    "total_matched": 1,
    "truncated": false,
    "scan_capped_bookmakers": [],
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary"
  }
}
GET/api/v1/odds-feed/racing/historyAPI plan

Historical per-book, per-runner racing odds snapshots (~5-min granularity) from the BigQuery cold archive — the backtest feed.

Tier detail: api (paid)

PAID-ONLY endpoint: gated on `bulk_export`, which is NOT in FREE_TIER_FEATURES — requires the api (paid) plan. `from` is REQUIRED (400 if missing/malformed); unbounded scans are rejected. Max 31-day window (400 if exceeded). Cursor-based pagination — supports `?fields=` projection via parseFields/projectRows. 503 (not 500) if the BQ table doesn't exist yet (mirror/flatten not live). TAB's 'R' race_type is normalised to 'T' in output; both 'T' and 'R' accepted as query aliases.

Query parameters
NameTypeRequiredDefaultDescription
fromstring (YYYY-MM-DD)YesRequired. Start of date window; clamped to the caller's historicalDays.
tostring (YYYY-MM-DD)Nosame as fromEnd of date window. Must be >= from. Window capped at 31 days total.
bookmakerstring enumNoOne of tab, betfair, sportsbet, ladbrokes, neds, pointsbet, bluebet, palmerbet.
venuestringNoSubstring match on venue_slug.
race_typestring enumNoT/R (thoroughbred), H (harness), G (greyhound). T and R both map to stored 'T'.
limitintegerNo5000Capped at 5000 for both free and api internal tiers.
cursorstring (base64url)NoOpaque pagination cursor returned as meta.next_cursor from the previous page.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/racing/history?from=2026-09-01"
Example response
{
  "success": true,
  "data": [
    {
      "date": "2026-09-03",
      "snapshot_ts": "2026-09-03T05:05:00.000Z",
      "snapshot_ms": 1772773500000,
      "venue_slug": "flemington",
      "race_no": 4,
      "race_id": "flemington-20260903-r4",
      "meeting_id": "flemington-20260903",
      "race_type": "T",
      "runner_no": 7,
      "runner_name": "Golden Streak",
      "scratched": false,
      "book_key": "sportsbet",
      "bet_link": "https://www.sportsbet.com.au/...",
      "win": 4.2,
      "place": 1.65,
      "tote_win": 4.4,
      "tote_place": 1.7,
      "lay": 4.4
    }
  ],
  "meta": {
    "from": "2026-08-20",
    "to": "2026-09-03",
    "partition_days": 15,
    "bookmaker": "sportsbet",
    "venue": null,
    "race_type": "T",
    "limit": 5000,
    "requested_limit": 5000,
    "next_cursor": "eyJtcyI6MTc3Mjc3MzUwMDAwMCwicmFjZSI6ImZsZW1pbmd0b24tMjAyNjA5MDMtcjQiLCJydW5uZXIiOjcsImJvb2siOiJzcG9ydHNiZXQifQ",
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary"
  }
}
GET/api/v1/odds-feed/racing/movementsFree tier

Cross-bookmaker price movements (opening vs current) for today's/tomorrow's races, per runner.

Reads a single precomputed doc `racing_opportunities/racing_book_movements` (Supabase-first, Firestore fallback). Cache 60s. Returns empty array (not error) when the doc has no movements yet. Same tier gate as /api/v1/odds-feed/racing ('racing'), independent of 'scraped_odds'.

Query parameters
NameTypeRequiredDefaultDescription
venuestringNoSubstring match on venue name (case-insensitive).
race_typestring enumNoT/R (thoroughbred), H (harness), G (greyhound).
bookmakerstringNoFilter each runner's bookMovements down to one book.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/racing/movements?venue=Randwick"
Example response
{
  "success": true,
  "data": [
    {
      "raceId": "flemington-20260903-r4",
      "raceName": "Race 4",
      "venue": "Flemington",
      "raceNumber": 4,
      "raceType": "T",
      "jumpTime": "2026-09-03T05:10:00Z",
      "runners": [
        {
          "number": 7,
          "name": "Golden Streak",
          "bestMovementPct": -12.5,
          "bookMovements": [
            {
              "book": "sportsbet",
              "opening": 4.8,
              "current": 4.2,
              "pct": -12.5,
              "bet_link": "https://www.sportsbet.com.au/..."
            },
            {
              "book": "tab",
              "opening": 5,
              "current": 4.4,
              "pct": -12,
              "bet_link": "https://tab.com.au/..."
            }
          ]
        }
      ]
    }
  ],
  "meta": {
    "updatedAt": "2026-09-03T05:00:12Z"
  }
}
GET/api/v1/odds-feed/resultsFree tier

Settled race results (winner + full finishing order) from Krok Odds result feeds.

Backed by `race_results` collection (Supabase, single jsonb-path equality filter on date + limit). date before the caller's historicalDays window returns 403. Cache 300s, scan cap 1000 rows/day.

Query parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)Notoday (Australia/Melbourne)AU-local race date.
sportstring enumNoR (thoroughbred), H (harness), G (greyhound); T accepted as alias for R.
sourcestringNoFilter by result source.
venuestringNoSubstring match on venue slug.
limitintegerNo20 (free) / 800 (api)Capped per tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/results?date=today%20(Australia%2FMelbourne)"
Example response
{
  "success": true,
  "data": [
    {
      "date": "2026-09-03",
      "venue": "Flemington",
      "venueSlug": "flemington",
      "raceNumber": 4,
      "raceType": "R",
      "source": "tab",
      "status": "settled",
      "winner": {
        "number": 7,
        "name": "Golden Streak"
      },
      "placings": [
        {
          "position": 1,
          "number": 7,
          "name": "Golden Streak"
        },
        {
          "position": 2,
          "number": 3,
          "name": "Silver Line"
        }
      ]
    }
  ],
  "meta": {
    "date": "2026-09-03",
    "sport": null,
    "source": null,
    "limit": 20,
    "requested_limit": 20,
    "total_matched": 1,
    "truncated": false,
    "scan_capped": false,
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary"
  }
}
GET/api/v1/odds-feed/results/{date}Free tier

All settled race results for a specific AU-local date (path-param variant of /odds-feed/results).

Same source, tier gate and shape as /odds-feed/results (query-param version); date comes from the path instead of ?date=. 400 on malformed date; shares the `shared.ts` loader (`race_results` collection, cache 300s, scan cap 1000).

Path parameters
NameTypeRequiredDefaultDescription
datestring (YYYY-MM-DD)YesAU-local race date.
Query parameters
NameTypeRequiredDefaultDescription
sportstring enumNoR (thoroughbred), H (harness), G (greyhound). T accepted as alias for R.
sourcestringNoFilter by result source.
venuestringNoSubstring match on venue slug.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/results/2026-09-01?sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "date": "2026-09-03",
      "venue": "Flemington",
      "venueSlug": "flemington",
      "raceNumber": 4,
      "raceType": "R",
      "source": "tab",
      "status": "settled",
      "winner": {
        "number": 7,
        "name": "Golden Streak"
      },
      "placings": [
        {
          "position": 1,
          "number": 7,
          "name": "Golden Streak"
        },
        {
          "position": 2,
          "number": 3,
          "name": "Silver Line"
        }
      ],
      "scratchings": [
        5
      ],
      "resultAt": "2026-09-03T05:14:22Z"
    }
  ],
  "meta": {
    "date": "2026-09-03",
    "sport": "R",
    "source": null,
    "limit": 20,
    "total_matched": 1,
    "truncated": false,
    "scan_capped": false,
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary"
  }
}
GET/api/v1/odds-feed/sportsFree tier

Sports with live proprietary event coverage, with per-sport live event counts and supplying bookmakers.

Counts come from PostgREST head-only COUNT queries, never a full document scan (deliberate cost guard, see June 2026 cost incident). Cache 1800s — sport list moves slowly. If ALL count queries fail, the route throws (500) rather than caching an empty list for 1800s. Racing-only books (BlueBet, Palmerbet) return empty sports list — that's real coverage, not an error; surfaced via meta.bookmaker_sports_feed = 'pending'.

Query parameters
NameTypeRequiredDefaultDescription
bookmakerstringNoCanonical key or white-label alias; filters the (sport, book) counts in-memory over one cache entry.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/sports?bookmaker=sportsbet"
Example response
{
  "success": true,
  "data": [
    {
      "sport": "aussierules_afl",
      "event_count": 9,
      "bookmakers": [
        "betr",
        "ladbrokes",
        "pointsbet",
        "sportsbet",
        "tab"
      ],
      "href": "/api/v1/odds-feed/sports/aussierules_afl"
    },
    {
      "sport": "rugbyleague_nrl",
      "event_count": 8,
      "bookmakers": [
        "ladbrokes",
        "sportsbet",
        "tab"
      ],
      "href": "/api/v1/odds-feed/sports/rugbyleague_nrl"
    }
  ],
  "meta": {
    "bookmaker": null,
    "brand": null,
    "bookmaker_sports_feed": null,
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary"
  }
}
GET/api/v1/odds-feed/sports/{sport}Free tier

Events + full market/selection odds for one sport, fanned out across every book that scrapes it.

Per-collection scan cap 100, independent of the caller's `limit`, so an in-memory bookmaker filter still has a full candidate pool. 60s in-memory per-instance cache layered under the 300s unstable_cache to dedupe burst traffic. bet_link attached AFTER slicing to `limit`, outside the cache, so only served selections pay the link-build cost.

Path parameters
NameTypeRequiredDefaultDescription
sportstringYesSport slug, e.g. 'basketball_nba', 'soccer_epl'. Resolved via resolveSportSlug; 400 if unresolvable.
Query parameters
NameTypeRequiredDefaultDescription
bookmakerstringNoCanonical key or white-label alias.
leaguestringNoLeague-specific slug, e.g. 'soccer_epl'; takes precedence over `competition`.
competitionstringNoSubstring match on competition name (used only if `league` not set).
upcomingboolean ('true')NofalseOnly events with start_time in the future.
marketsboolean ('false' to disable)NotrueSet to 'false' to omit markets/selections from the response (event metadata only).
limitintegerNo25 (free) / 1000 (api)Capped per tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/sports/aussierules_afl?bookmaker=sportsbet"
Example response
{
  "success": true,
  "data": [
    {
      "bookmaker_key": "sportsbet",
      "sport_slug": "soccer_epl",
      "event": "Arsenal vs Chelsea",
      "start_time": "2026-09-03T19:30:00Z",
      "competition": "English Premier League",
      "league_slug": "soccer_epl",
      "markets": [
        {
          "key": "h2h",
          "selections": [
            {
              "name": "Arsenal",
              "price": 2.1,
              "bet_link": "https://www.sportsbet.com.au/..."
            },
            {
              "name": "Draw",
              "price": 3.4,
              "bet_link": "https://www.sportsbet.com.au/..."
            },
            {
              "name": "Chelsea",
              "price": 3.5,
              "bet_link": "https://www.sportsbet.com.au/..."
            }
          ]
        }
      ]
    }
  ],
  "meta": {
    "sport": "soccer_epl",
    "bookmaker": null,
    "brand": null,
    "limit": 25,
    "requested_limit": 25,
    "total_matched": 1,
    "truncated": false,
    "scan_capped_bookmakers": [],
    "direct_scrape": true,
    "redistributable": true,
    "license": "krokodds-proprietary"
  }
}
GET/api/v1/scraped-odds/bookmakers→ alias of /api/v1/odds-feed/bookmakersFree tier

Alias of /api/v1/odds-feed/bookmakers.

Directly re-exports { GET, OPTIONS } from ../../odds-feed/bookmakers/route — same handler, not a redirect. Historically was a 308 redirect but NextResponse.redirect() built the Location from internal request.url (localhost on Cloud Run/App Hosting), breaking external clients — now serves the handler directly.

Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/scraped-odds/bookmakers"
GET/api/v1/scraped-odds/racing→ alias of /api/v1/odds-feed/racingFree tier

Alias of /api/v1/odds-feed/racing.

Directly re-exports { GET, OPTIONS } from ../../odds-feed/racing/route — same handler, not a redirect.

Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/scraped-odds/racing"
GET/api/v1/scraped-odds/sports→ alias of /api/v1/odds-feed/sportsFree tier

Alias of /api/v1/odds-feed/sports.

Directly re-exports { GET, OPTIONS } from ../../odds-feed/sports/route — same handler, not a redirect.

Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/scraped-odds/sports"
GET/api/v1/scraped-odds/sports/{sport}→ alias of /api/v1/odds-feed/sports/{sport}Free tier

Alias of /api/v1/odds-feed/sports/{sport}.

Directly re-exports { GET, OPTIONS } from ../../../odds-feed/sports/[sport]/route — same handler, not a redirect.

Path parameters
NameTypeRequiredDefaultDescription
sportstringYesPasses through unchanged to the odds-feed handler.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/scraped-odds/sports/aussierules_afl"

Sports

7 endpoints

Sport reference data, team form guides, league standings, injury reports and weather-impact analysis.

GET/api/v1/sportsFree tier

List all supported sports/leagues with category and AU coverage level.

No feature-gate/tierAllowsFeature check at all — only API-key auth + rate limit, no credit-cost debit path (verifyApiKey called without a cost arg). Available to any valid tier. `category` filter is normalised (case/space/dash/underscore insensitive) before matching. `refresh_interval_seconds` is a static heuristic (600 for keys ending `_winner`/`_preseason`, else 60) — not measured.

Query parameters
NameTypeRequiredDefaultDescription
categorystringNoFilter by sport category, case/punctuation-insensitive (e.g. "AFL", "au_sports", "Soccer", "Tennis").
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sports?category=value"
Example response
{
  "success": true,
  "data": [
    {
      "key": "aussierules_afl",
      "label": "AFL",
      "category": "AU Sports",
      "au_coverage": "high",
      "refresh_interval_seconds": 60
    },
    {
      "key": "rugbyleague_nrl",
      "label": "NRL",
      "category": "AU Sports",
      "au_coverage": "high",
      "refresh_interval_seconds": 60
    },
    {
      "key": "basketball_nba",
      "label": "NBA",
      "category": "Basketball",
      "au_coverage": "high",
      "refresh_interval_seconds": 60
    },
    {
      "key": "tennis_atp_madrid_open",
      "label": "Madrid Open (ATP)",
      "category": "Tennis",
      "au_coverage": "medium",
      "refresh_interval_seconds": 60
    },
    {
      "key": "soccer_epl",
      "label": "Premier League",
      "category": "Soccer",
      "au_coverage": "high",
      "refresh_interval_seconds": 60
    }
  ],
  "meta": {
    "total": 5,
    "categories": [
      "AU Sports",
      "Basketball",
      "Tennis",
      "Soccer"
    ],
    "timestamp": "2026-09-03T04:00:00.000Z"
  }
}
GET/api/v1/sports/futuresFree tier

AFL futures markets — Brownlow Medal (12 markets) + Premiership Winner — aggregated across all AU bookmaker futures collections.

Reads from all external_*_futures collections (12 books). Filters for AFL-related futures using event/competition/sport name matching. Returns one row per (market, bookmaker) pair. Markets include: Brownlow Winner, Brownlow Without Daicos, Top 3/5/10/20, Leader After Round 6/10/15, Most 3-Vote Games, Most Poll Games, Most Votes Last 8 Rounds, and Premiership Winner. Cached 600s via unstable_cache + in-memory dedup.

Query parameters
NameTypeRequiredDefaultDescription
marketstringNoFilter by market key (e.g. 'brownlow_winner', 'premiership_winner').
categorystringNoFilter by category: 'brownlow' or 'premiership'.
bookmakerstringNoFilter to a single bookmaker (e.g. 'sportsbet', 'ladbrokes').
limitnumberNo200Max market entries returned. Clamped to 200 (free) / 1000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sports/futures?market=h2h"
Example response
{
  "success": true,
  "data": [
    {
      "market_key": "brownlow_winner",
      "market_label": "Brownlow Medal Winner",
      "category": "brownlow",
      "bookmaker": "Sportsbet",
      "bookmaker_key": "sportsbet",
      "selections": [
        {
          "name": "Nick Daicos",
          "odds": 3.5
        },
        {
          "name": "Marcus Bontempelli",
          "odds": 8
        }
      ],
      "captured_at": "2026-09-06T10:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "sport": "aussie-rules",
    "category": null,
    "market": null,
    "bookmaker": null,
    "limit": 200,
    "requested_limit": 200,
    "total_matched": 1,
    "truncated": false,
    "direct_scrape": true,
    "license": "krokodds-proprietary",
    "timestamp": "2026-09-06T10:00:00.000Z"
  }
}
GET/api/v1/sports/formFree tier

Form deep-dive combining recent results, head-to-head, venue form and last-5/last-10 trend stats for teams, players, and horses.

Fans out across 5 Supabase sources (player_historical_stats, racing_runner_stats, team_game_log, game_results, player_props_results) and dedupes by synthetic id. `player`/`venue` filters only apply to the racing (horse) branch; `team` narrows sport-scoped sources. Cached 600s via unstable_cache, floored by v1Revalidate (min 300s).

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoSport to analyse, e.g. "aussierules_afl", "rugbyleague_nrl", or "horse_racing". Case-insensitive.
teamstringNoTeam name filter, partial/substring match.
playerstringNoPlayer or horse runner name filter, partial match.
venuestringNoVenue filter, partial match — racing branch only.
limitintegerNo50Max results, clamped 1-200 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sports/form?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "team_game_log_aussierules_afl_geelong",
      "entity_type": "team",
      "entity_name": "geelong",
      "sport_key": "aussierules_afl",
      "recent_results": [
        "W",
        "W",
        "L",
        "W",
        "W"
      ],
      "win_rate": 0.8,
      "streak": "W2",
      "venue_form": null,
      "h2h_record": null,
      "last_5": {
        "games": 5,
        "wins": 4,
        "win_pct": 0.8,
        "points_for_avg": 92.4,
        "points_against_avg": 71.2,
        "margin_avg": 21.2
      },
      "last_10": {
        "games": 10,
        "wins": 7,
        "win_pct": 0.7,
        "points_for_avg": 89.1,
        "points_against_avg": 76.8,
        "margin_avg": 12.3
      },
      "notes": "Strong recent form. On a W2 winning streak",
      "updated_at": ""
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "filter": {
      "sport_key": "aussierules_afl",
      "team": "geelong",
      "player": null,
      "venue": null
    },
    "note": "Form deep-dive from game_results, player_props_results, and team logs — last 5/10 game averages, win rates, streaks, and score trends.",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/sports/injuriesFree tier

Cross-sport injury report — unified schema across all supported sports, sourced from injury_reports.

Reads from injury_reports (Supabase). Comment header says it 'extends /v1/injuries (racing-focused)' with multi-sport coverage and status_severity classification. `sport_key` also accepts a legacy `sport` alias param. Cached 300s.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport (AFL, NRL, NFL, NBA, NHL, MLB, etc). Alias: `sport`.
teamstringNoFilter by team name, partial match.
statusstringNoFilter by raw injury status string (e.g. "out", "doubtful", "questionable", "probable"), exact match case-insensitive.
limitintegerNo100Max results, clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sports/injuries?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "inj_nba_2026090301",
      "sport_key": "basketball_nba",
      "player_name": "Jaylen Brown",
      "player_slug": "jaylen-brown",
      "team": "Boston Celtics",
      "status": "Questionable",
      "status_severity": "questionable",
      "reason": "Ankle soreness",
      "body_part": "Ankle",
      "date": "2026-09-03",
      "season": 2026,
      "source": "automated",
      "updated_at": "2026-09-03T02:15:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "filter": {
      "sport_key": "basketball_nba",
      "team": null,
      "status": null
    },
    "sport_breakdown": {
      "basketball_nba": 1
    },
    "note": "Cross-sport injury aggregation from multiple sources.",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/sports/standingsFree tier

NHL and MLB league standings in a unified schema.

Covers NHL and MLB standings. Cached 3600s. `points` is null for MLB rows, `games_behind`/`elimination_number` null for NHL rows (fields are sport-specific but unified into one shape).

Query parameters
NameTypeRequiredDefaultDescription
sportstringNoboth (nhl+mlb)"nhl" or "mlb"; omit for both, lowercased.
limitintegerNo100Max rows, clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sports/standings?sport=both%20(nhl%2Bmlb)"
Example response
{
  "success": true,
  "data": [
    {
      "sport": "MLB",
      "team_id": "147",
      "team": "New York Yankees",
      "division": "AL East",
      "league": "American League",
      "group": null,
      "wins": 84,
      "losses": 58,
      "points": null,
      "win_pct": 0.592,
      "games_behind": "-",
      "streak": "W3",
      "home": "45-25",
      "away": "39-33",
      "last_ten": "7-3",
      "rank": 1,
      "runs_scored": 712,
      "runs_allowed": 601,
      "run_differential": 111,
      "clinched": false,
      "elimination_number": null,
      "updated_at": "2026-09-03T01:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "sport": "mlb",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/sports/weather-impactFree tier

Weather impact analysis for upcoming events — correlates Open-Meteo forecasts / racing-meeting weather with tagged impact notes (wind, rain, heat, cold, humidity, storm risk).

Two sources merged: external_openmeteo_forecasts (multi-sport outdoor events, skips retractable-roof venues) and racing_meetings (embedded BOM/Open-Meteo weather). `impact_tags` are rule-based thresholds (wind>30km/h, rain>5mm, temp>38C or <5C, humidity>80%, summary contains storm/thunder). Cached 300s.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport, e.g. "AFL", "NRL", "horse_racing".
venuestringNoVenue filter, partial match.
datestringNoEvent date filter, YYYY-MM-DD, exact match.
limitintegerNo50Max results, clamped 1-200 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sports/weather-impact?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "openmeteo_rugbyleague_nrl_accorstadium_20260906",
      "sport_key": "NRL",
      "venue": "Accor Stadium",
      "date": "2026-09-06",
      "race_type": null,
      "track_condition": null,
      "weather_summary": "Showers clearing, gusty southerly",
      "temp_c": 16,
      "humidity_pct": 78,
      "rain_24h_mm": 6.2,
      "wind_kmh": 34,
      "wind_dir": null,
      "impact_tags": [
        "high_wind",
        "heavy_rain"
      ],
      "impact_notes": "Strong wind (34 km/h) favours inside barriers and wind-assisted runners. Significant rain (6.2mm) expected — prefer on-pace runners and firm-track specialists",
      "updated_at": "2026-09-03T03:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "filter": {
      "sport_key": "NRL",
      "venue": null,
      "date": null
    },
    "impact_summary": {
      "high_wind": 1,
      "heavy_rain": 1
    },
    "note": "Weather impact analysis from Open-Meteo forecasts and BOM/racing meeting data — temperature, wind, rain, humidity correlated with event conditions.",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/standingsFree tier

Unified standings for ALL sports from match_standings table, plus NHL/MLB from dedicated standings tables.

Reads from match_standings (all sports) plus NHL and MLB standings tables. Results are merged into a unified shape. Cached 3600s.

Query parameters
NameTypeRequiredDefaultDescription
sportstringNoSport_key filter (e.g. "aussierules_afl", "rugbyleague_nrl"). Also accepts "nhl" or "mlb" for legacy tables.
limitnumberNo100Max rows, clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/standings?sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "sport_key": "aussierules_afl",
      "tournament_id": "1",
      "season_id": "2026",
      "team_name": "Collingwood",
      "team_id": "1",
      "position": 1,
      "played": 22,
      "won": 18,
      "drawn": 1,
      "lost": 3,
      "points_for": 1800,
      "points_against": 1200,
      "points_diff": 600,
      "points": 73,
      "updated_at": "2026-09-03T01:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "sport": "all"
  }
}

Tips & Predictions

4 endpoints

AI-generated betting tips with confidence scores and historical accuracy tracking.

GET/api/v1/predictions→ alias of /api/v1/tipsFree tier

Deprecated alias of /api/v1/tips.

QUIRK: the file's own doc-comment claims it "proxies to tips with a sport_key=soccer filter for backwards compatibility", but the actual code is a bare re-export (`export { GET, OPTIONS } from '../tips/route'`) — NO soccer filter is applied; it is byte-identical behaviour to /api/v1/tips, all sports included. Comment is stale/misleading. Marked deprecated 2026-08-06, "will be removed in a future version" (no removal date set as of 2026-09-03). Same params/response/tier as /api/v1/tips.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoSame as /api/v1/tips — filters by sport key despite the doc-comment implying a fixed soccer filter (that filter does not actually exist in code).
min_confidenceintegerNo1Same as /api/v1/tips.
limitintegerNo50Same as /api/v1/tips.
include_settledbooleanNofalseSame as /api/v1/tips.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/predictions?sport_key=aussierules_afl"
Example response
"identical shape to /api/v1/tips — see that entry"
GET/api/v1/predictions/confidenceFree tier

Model confidence breakdown across racing, sports, and player props, plus 30-day historical accuracy by confidence bucket.

Gates on the 'tips' feature (tierAllowsFeature(tier,'tips')) even though the path is under /predictions. `force-dynamic`/`revalidate=0` — NOT cached via unstable_cache (response header still claims `Cache-Control: public, max-age=300` which is misleading given no server cache). Sports and props predictions are ONLY loaded if `sport`/`event_id` params are respectively provided — with type=all and neither param, only racing predictions populate. `summary.breakdown` is always `{}` in the current code (allBreakdowns is declared but never populated — dead aggregation). historicalAccuracy tries Supabase settled_tips first, falls back to Firestore.

Query parameters
NameTypeRequiredDefaultDescription
typestringNoall"racing" | "sports" | "props" | "all". Lowercased.
sportstringNoSport key — required for the sports branch to run (ignored if type excludes sports).
datestringNotoday (AU)YYYY-MM-DD — racing branch only.
event_idstringNoSpecific event id — required for the props branch to run.
limitintegerNo50Max predictions per type-branch; capped 20 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/predictions/confidence?type=all"
Example response
{
  "success": true,
  "meta": {
    "type": "racing",
    "sport": null,
    "date": "2026-09-03",
    "eventId": null,
    "generatedAt": "2026-09-03T04:00:00.000Z",
    "cacheTtl": 300
  },
  "summary": {
    "overallConfidence": 0.74,
    "breakdown": {},
    "dataSources": [
      "racing_form",
      "sectionals",
      "barrier_trial",
      "weather"
    ],
    "historicalAccuracy": {
      "totalPredictions": 812,
      "correctPredictions": 471,
      "accuracyRate": 0.58,
      "byConfidenceBucket": {
        "high": {
          "total": 210,
          "correct": 148,
          "rate": 0.7
        },
        "medium": {
          "total": 380,
          "correct": 218,
          "rate": 0.57
        },
        "low": {
          "total": 222,
          "correct": 105,
          "rate": 0.47
        }
      }
    }
  },
  "predictions": [
    {
      "id": "racing-Written By-R6",
      "type": "racing",
      "confidence": 0.81,
      "dataSources": [
        "racing_form",
        "sectionals",
        "barrier_trial"
      ],
      "breakdown": {
        "weather": 0.6,
        "barrier": 0.75,
        "sectionals": 0.88,
        "pedigree": 0.55,
        "ensemble": 0.81
      },
      "reasoning": "Strong recent sectionals and barrier draw at Flemington R6."
    }
  ]
}
GET/api/v1/tipsFree tier

In-house model tips/predictions across all covered sports (active by default, or settled history with include_settled=true).

Uses the OLDER hand-rolled auth preamble (not guardV1Request/makeV1Route) — no explicit credit-cost debit in this file, verifyApiKey called with default cost. Merges canonical tips_rounds engine picks over legacy gameday_tips (canonical wins on conflicting fixtures; legacy only fills 7 leagues the engine doesn't cover) — active-tips path only, not for include_settled=true which reads settled_tips directly. Free tier cache 120s, api tier 30s (both floored to 300s by v1Revalidate). Free tier capped at 50 rows/request, api tier 1000.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key. Alias: `sport`. Lowercased.
min_confidenceintegerNo1Minimum confidence 1-5, clamped.
limitintegerNo50Max rows; capped 50 (free) / 1000 (api) by getMaxLimit().
include_settledbooleanNofalse"true" to return settled_tips history (with resolution/actual_winner) instead of pending active tips.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tips?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "aussierules_afl-geelong-carlton-20260906t0740",
      "event_id": "e_afl_20260906_gee_car",
      "sport_key": "aussierules_afl",
      "sport": "AFL",
      "home_team": "Geelong Cats",
      "away_team": "Carlton Blues",
      "commence_time": "2026-09-06T07:40:00.000Z",
      "pick": "Geelong Cats -12.5",
      "pick_side": "home",
      "confidence": 4,
      "confidence_label": "High",
      "consensus_prob_home": 0.71,
      "consensus_prob_away": 0.29,
      "predicted_margin": 18.4,
      "predicted_margin_side": "home",
      "resolution": "pending",
      "actual_winner": null
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "include_settled": false,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/tips/accuracyFree tier

Historical hit-rate / track-record for the tips model, overall and broken down by confidence bucket, optionally scoped to one sport.

Reads model_track_record. Per-sport lookup filters on the `sport_key` column (indexed); the unscoped (all-sport) query has no column for `total` (lives in JSONB) so it sorts in memory after fetching up to `scanSize` (min(limit,100)) rows — an all-sports call is NOT a true global ranking beyond that scan window. Same free/api cache tiers as /tips (600s/120s, floored to 300s).

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter to one sport. Alias: `sport`. Lowercased.
limitintegerNo50Max rows; capped 50 (free) / 1000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tips/accuracy?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "sport_key": "rugbyleague_nrl",
      "wins": 142,
      "losses": 88,
      "pushes": 3,
      "total": 233,
      "decided": 230,
      "hit_rate": 0.6174,
      "by_confidence": [
        {
          "confidence": "3",
          "wins": 51,
          "losses": 44,
          "pushes": 1,
          "total": 96,
          "hit_rate": 0.5368
        },
        {
          "confidence": "4",
          "wins": 63,
          "losses": 32,
          "pushes": 1,
          "total": 96,
          "hit_rate": 0.6632
        },
        {
          "confidence": "5",
          "wins": 28,
          "losses": 12,
          "pushes": 1,
          "total": 41,
          "hit_rate": 0.7
        }
      ],
      "last_settled_at": "2026-09-02T22:30:00.000Z",
      "updated_at": "2026-09-02T22:31:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": "rugbyleague_nrl",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}

Tennis

5 endpoints

ATP/WTA rankings, upcoming fixtures, match results, Elo ratings and surface-specific stats.

GET/api/v1/tennis/eloFree tier

Overall or surface-specific Elo ratings for ATP/WTA players (UTS/tennisabstract-derived).

Two very differently-shaped response rows depending on `surface`: with `surface` set it reads external_utstat_surface and returns {id, player_name, player_id, surface, elo}; without it, reads external_utstat_elo and returns the richer {rank, player_name, country_code, elo_rating, best_rank, best_rating, points_diff} shape — field names differ (elo vs elo_rating) between the two modes. `player` filter fetches up to limit*6 rows first then filters in memory (partial match).

Query parameters
NameTypeRequiredDefaultDescription
playerstringNoPlayer name filter, partial/substring, case-insensitive.
surfacestringNo"hard" | "clay" | "grass" — switches to the surface-Elo table/shape. Any other value is ignored (falls back to overall Elo).
limitintegerNo100Max rows, clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tennis/elo?player=lebron-james"
Example response
{
  "success": true,
  "data": [
    {
      "id": "atp_sinner_j",
      "rank": 1,
      "player_name": "Jannik Sinner",
      "player_id": "s0ag",
      "country_code": "ITA",
      "elo_rating": 2312,
      "best_rank": 1,
      "best_rating": 2340,
      "points_diff": 45
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "player": null,
    "surface": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/tennis/fixturesAPI plan

Upcoming/scheduled tennis fixtures by tour.

Tier detail: api (paid) — 'historical' is NOT in FREE_TIER_FEATURES

Gated on the 'historical' feature despite being a forward-looking fixtures list (not a historical archive) — free-tier keys get a 402 tier-gate response here. creditCost=2 (archive-weighted, per the makeV1Route cfg comment: standard 1 / archive 5 / bulk 25 — this is priced above standard but not full archive). Built via makeV1Route/tennis_fixtures collection.

Query parameters
NameTypeRequiredDefaultDescription
tourstringNoTour filter, e.g. "ATP" or "WTA" — exact match on the `tour` JSONB field.
limitintegerNo100Max rows; capped 50 (free) / 200 (api) per limitByTier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tennis/fixtures?tour=value"
Example response
{
  "success": true,
  "data": [
    {
      "tour": "ATP",
      "tournament": "US Open",
      "round": "QF",
      "player1": "Carlos Alcaraz",
      "player2": "Alexander Zverev",
      "date": "2026-09-04",
      "surface": "hard",
      "venue": "USTA Billie Jean King National Tennis Center"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 100,
    "requested_limit": 100,
    "tour": "ATP",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 998,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/tennis/matchesFree tier

Historical ATP/WTA singles match results (Jeff Sackmann dataset), filterable by season/tour/surface.

Reads external_sackmann_tennis_matches. Only `season` is an indexed equality filter pushed to Supabase; `tour`/`surface` are applied in-memory after over-fetching (limit*6) when set — per the file's own query-design comment this keeps it on an indexed column. Throws V1BadRequest (400) if `season` is non-numeric.

Query parameters
NameTypeRequiredDefaultDescription
seasonintegerNoSeason year, e.g. 2026. Must parse as a number or 400 V1BadRequest is returned.
tourstringNo"atp" or "wta", case-insensitive, in-memory filter.
surfacestringNo"hard" | "clay" | "grass", case-insensitive, in-memory filter.
limitintegerNo100Max rows; capped 100 (free) / 500 (api) per limitByTier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tennis/matches?season=10"
Example response
{
  "success": true,
  "data": [
    {
      "season": 2026,
      "tour": "atp",
      "surface": "hard",
      "tournament": "US Open",
      "round": "SF",
      "winner_name": "Jannik Sinner",
      "loser_name": "Novak Djokovic",
      "score": "6-4 3-6 7-6(5) 6-2",
      "date": "2026-09-05"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "season": 2026,
    "tour": "atp",
    "surface": "hard",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/tennis/rankingsAPI plan

Current ATP/WTA player rankings.

Tier detail: api (paid) — 'historical' is NOT in FREE_TIER_FEATURES

Gated on 'historical' despite being a current-state rankings list, not an archive — free-tier keys get 402. No filter params at all beyond `limit` (no tour/player filter exposed). creditCost=2 like /tennis/fixtures.

Query parameters
NameTypeRequiredDefaultDescription
limitintegerNo100Max rows; capped 100 (free) / 500 (api) per limitByTier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tennis/rankings?limit=100"
Example response
{
  "success": true,
  "data": [
    {
      "ranking": 1,
      "tour": "ATP",
      "player_name": "Jannik Sinner",
      "country": "ITA",
      "points": 11330,
      "movement": 0
    },
    {
      "ranking": 1,
      "tour": "WTA",
      "player_name": "Iga Swiatek",
      "country": "POL",
      "points": 9945,
      "movement": 1
    }
  ],
  "meta": {
    "count": 2,
    "tier": "api",
    "limit": 100,
    "requested_limit": 100,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 998,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/tennis/surface-statsFree tier

Tennis surface-specific Elo/win-rate stats (hard/clay/grass) for ATP/WTA players.

Reads the SAME table (external_utstat_surface) and returns the SAME shape ({id, player_name, player_id, surface, elo}) as /api/v1/tennis/elo?surface=X — effectively a duplicate/alias of that mode with a dedicated path. No win-rate field despite the doc-comment and summary saying "win rates and Elo by surface" — only `elo` is actually returned, no win_pct/matches_played field exists in code.

Query parameters
NameTypeRequiredDefaultDescription
playerstringNoPlayer name filter, partial/substring, case-insensitive.
surfacestringNo"hard" | "clay" | "grass" — invalid values are silently ignored (no filter applied).
limitintegerNo100Max rows, clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tennis/surface-stats?player=lebron-james"
Example response
{
  "success": true,
  "data": [
    {
      "id": "atp_alcaraz_c_clay",
      "player_name": "Carlos Alcaraz",
      "player_id": "a0e2",
      "surface": "clay",
      "elo": 2280
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "player": null,
    "surface": "clay",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 949,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}

Historical

6 endpoints

Deep archives — player and team game logs, shot charts, AFL/NRL historical data. Paid API plan only.

GET/api/v1/historical/aflAPI plan

AFL historical archive — player game logs (2010+) or match results (2018+ via Akareen, pre-2018 via AFL Tables backfill).

Tier detail: api (paid) — 'historical' is NOT in FREE_TIER_FEATURES

Cache: free=3600s, api=1800s. kind=match with year<2018 silently swaps collection to the historical matches collection (identically shaped). Uses shared makeV1Route wrapper (historical-window clamp on since/from, empty-result credit refund, 204 on empty).

Query parameters
NameTypeRequiredDefaultDescription
kindstringNoplayer'player' or 'match'. 400 if any other value.
yearintegerNoSeason year filter. Values <2018 with kind=match route to the AFL Tables backfill collection instead of Akareen.
roundstringNoRound filter, only applied when kind=player (post-fetch filter, over-fetches limit*4 rows).
limitintegerNo100Row cap. Tier-capped: free=100, api=500.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/historical/afl?apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "afl_2026_r22_j_daicos_collingwood",
      "player": "Josh Daicos",
      "team": "Collingwood",
      "year": "2026",
      "round": "22",
      "disposals": 28,
      "goals": 1,
      "kicks": 19,
      "handballs": 9,
      "marks": 6,
      "tackles": 4,
      "fixtureId": 923841
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 500,
    "requested_limit": 100,
    "kind": "player",
    "year": 2026,
    "round": "22",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 8421,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/historical/nrlAPI plan

NRL historical archive — player/match data from 2010 onwards.

Tier detail: api (paid)

Cache: free=3600s, api=1800s. Three distinct underlying Supabase collections keyed off 'kind', each with its own season field name (competitionYear vs season).

Query parameters
NameTypeRequiredDefaultDescription
kindstringNomodern'player' (2010-2023 archive), 'match' (2010-2023 archive), or 'modern' (2024+ current data). 400 on any other value.
season / yearintegerNoSeason filter; 'season' checked first, falls back to 'year'.
limitintegerNo100Tier-capped: free=100, api=500.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/historical/nrl?apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "nrl_2026_r24_n_hynes_sharks",
      "player": "Nicho Hynes",
      "team": "Cronulla Sharks",
      "season": "2026",
      "tries": 1,
      "tackles": 22,
      "runMetres": 118,
      "tackleBreaks": 3,
      "fixtureId": 448120
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 500,
    "requested_limit": 100,
    "kind": "modern",
    "season": 2026,
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 8420,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/historical/player-game-logAPI plan

Full accumulated game-by-game history for a player+stat combo (not a fixed recent-form window — depth varies per player).

Tier detail: api (paid)

Does NOT use makeV1Route — hand-rolled handler, no explicit creditCost field/mechanism (unlike the shared wrapper's credit-cost headers). recentGames written append-only via Firestore arrayUnion so a doc holds FULL history, not a 15-game window; history_from/history_to expose real span. Cache: free=600s, api=120s. Over-fetches limit*4 (capped 2000) then filters player/team in memory.

Query parameters
NameTypeRequiredDefaultDescription
sport_key / sportstringYese.g. basketball_nba, americanfootball_nfl. 400 if missing.
playerstringNoCase-insensitive substring match, filtered post-fetch.
teamstringNoCase-insensitive substring match, filtered post-fetch.
stat_key / statstringNoExact-match stat key filter (e.g. 'points', 'disposals').
sincestring (YYYY-MM-DD)NoFilters recentGames[] to dates >= since. Malformed values ignored. Clamped forward by clampHistoricalFrom() to the key's historicalDays window.
limitintegerNo100Tier-capped: free=50, api=2000.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/historical/player-game-log?sport_key / sport=aussierules_afl&apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "basketball_nba__nikola_jokic__points",
      "sport_key": "basketball_nba",
      "player": "Nikola Jokic",
      "team": "Denver Nuggets",
      "stat_key": "points",
      "games_count": 3,
      "history_to": "2026-04-12",
      "history_from": "2025-10-22",
      "recent_games": [
        {
          "date": "2026-04-12",
          "opponent": "Los Angeles Lakers",
          "is_home": true,
          "stat_value": 31,
          "fixture_id": 501233
        },
        {
          "date": "2026-04-09",
          "opponent": "Phoenix Suns",
          "is_home": false,
          "stat_value": 27,
          "fixture_id": 501198
        },
        {
          "date": "2026-04-06",
          "opponent": "Golden State Warriors",
          "is_home": true,
          "stat_value": 24,
          "fixture_id": 501167
        }
      ],
      "updated_at": "2026-04-13T02:11:04.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 2000,
    "requested_limit": 100,
    "sport_key": "basketball_nba",
    "stat_key": "points",
    "since": "2025-10-22",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 8419,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/historical/player-statsAPI plan

Season averages, splits, and market lines for a player from the historical player-stats archive; single-slug lookup or sport-wide/team-filtered listing.

Tier detail: api (paid)

Cache: free=3600s, api=1800s. Has BOTH loadSupabase and loadBq (BQ fallback only fires if BQ_ENABLED and Supabase throws). Slug lookup bypasses the limit param entirely.

Query parameters
NameTypeRequiredDefaultDescription
sport_key / sportstringYes400 'sport_key parameter required' if missing.
slugstringNoCanonical player slug. If present, does a direct id lookup on `${sportKey}__${slug}` (limit ignored, returns 0 or 1 row).
teamstringNoCase-insensitive exact match on team, filtered post-fetch (only applies when slug absent).
limitintegerNo100Tier-capped: free=100, api=500.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/historical/player-stats?sport_key / sport=aussierules_afl&apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "basketball_nba__jayson_tatum",
      "name": "Jayson Tatum",
      "slug": "jayson_tatum",
      "sport_key": "basketball_nba",
      "team": "Boston Celtics",
      "position": "SF",
      "active": true,
      "coverage": {
        "seasons": [
          "2023",
          "2024",
          "2025",
          "2026"
        ]
      },
      "season_averages": {
        "points": 26.8,
        "rebounds": 8.1,
        "assists": 4.6
      },
      "splits": {
        "home": {
          "points": 27.9
        },
        "away": {
          "points": 25.7
        }
      },
      "market_lines": {
        "points": 26.5,
        "rebounds": 8
      },
      "updated_at": "2026-09-02T22:04:11.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 500,
    "requested_limit": 100,
    "sport_key": "basketball_nba",
    "slug": "jayson_tatum",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 8418,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/historical/shot-chartAPI plan

NBA shot chart — half-court shot coordinates for a player, optionally filtered to one season.

Tier detail: api (paid)

Cache: free=3600s, api=1800s. One doc per (season, player) in player_shot_charts; data.shots is the shot-point array. NBA-only (Wave 3 feature).

Query parameters
NameTypeRequiredDefaultDescription
slug / playerstringYesCanonical player slug (spaces normalized to underscores, lowercased). 400 'slug is required' if missing.
seasonstringNoExact-match season filter, applied in-memory over an over-fetched (limit*4) window.
limitintegerNo100Tier-capped: free=20, api=60 (much lower cap than other historical routes).
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/historical/shot-chart?slug / player=lebron-james&apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "2026__nikola_jokic",
      "playerSlug": "nikola_jokic",
      "season": "2026",
      "team": "Denver Nuggets",
      "shots": [
        {
          "x": 12.4,
          "y": 8.1,
          "made": true,
          "value": 2,
          "distance_ft": 14,
          "date": "2026-04-12"
        },
        {
          "x": -21,
          "y": 22.6,
          "made": false,
          "value": 3,
          "distance_ft": 27,
          "date": "2026-04-12"
        }
      ],
      "updated_at": "2026-04-13T01:50:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 60,
    "requested_limit": 100,
    "slug": "nikola_jokic",
    "season": "2026",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 60,
      "remaining": 8417,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/historical/team-game-logAPI plan

Historical team-level game results (scores, periods, status) with date-range filtering.

Tier detail: api (paid)

Does NOT use makeV1Route (hand-rolled handler, mirrors historical/player-game-log structure) — no explicit per-request creditCost field. Cache: free=600s, api=120s. Ordered DESC by date server-side (unlike most sibling historical routes which have no explicit order).

Query parameters
NameTypeRequiredDefaultDescription
sport_key / sportstringYes400 if missing.
teamstringNoCase-insensitive substring match against home OR away team, filtered post-fetch.
sincestring (YYYY-MM-DD)NoRange start. Clamped forward by clampHistoricalFrom() to the key's historicalDays window; malformed values ignored.
untilstring (YYYY-MM-DD)NoRange end, validated YYYY-MM-DD only (not clamped).
limitintegerNo100Tier-capped: free=100, api=5000 (highest cap of all historical routes).
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/historical/team-game-log?sport_key / sport=aussierules_afl&apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "americanfootball_nfl_2026_wk1_kc_bal",
      "sport_key": "americanfootball_nfl",
      "fixture_id": 700421,
      "date": "2026-09-05",
      "home_team": "Baltimore Ravens",
      "away_team": "Kansas City Chiefs",
      "home_score": 24,
      "away_score": 27,
      "periods": {
        "q1": 7,
        "q2": 3,
        "q3": 7,
        "q4": 7
      },
      "league": "NFL",
      "league_id": 1,
      "country": "USA",
      "status": "final",
      "updated_at": "2026-09-06T03:10:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 5000,
    "requested_limit": 100,
    "sport_key": "americanfootball_nfl",
    "since": "2025-09-01",
    "until": null,
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 8416,
      "reset": "2026-10-01"
    }
  }
}

Reference

6 endpoints

Static lookup data for building your own UI — players, venues, headshots, team logos and sport/league metadata.

GET/api/v1/reference/headshotsFree tier

Static player headshot photo URLs by sport, optionally a single player by slug.

Tier detail: free — 'reference' IS in FREE_TIER_FEATURES

Cache: free=86400s, api=43200s (24h/12h, longest TTLs of any v1 route — static reference data). No explicit creditCost set in config → defaults to 1 (makeV1Route default).

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringYes400 'sport_key parameter required' if missing.
slugstringNoSingle-player lookup via doc id `${sportKey}_${slug}` (bypasses limit).
limitintegerNo100Tier-capped: free=200, api=1000.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/reference/headshots?sport_key=aussierules_afl&apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "basketball_nba_jayson_tatum",
      "name": "Jayson Tatum",
      "slug": "jayson_tatum",
      "sport_key": "basketball_nba",
      "photo_url": "https://a.espncdn.com/i/headshots/nba/players/full/4066261.png"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 200,
    "requested_limit": 100,
    "sport_key": "basketball_nba",
    "slug": "jayson_tatum",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 998,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/reference/metadataAPI plan

Team metadata (name, league, venue, etc.) from the team_metadata collection, optionally filtered by sport.

Tier detail: api (paid) — QUIRK: despite living under /reference/, this route gates on the 'historical' feature, NOT 'reference', so it is paid-tier-only unlike its sibling reference/* endpoints

Cache: free=86400s, api=43200s. Results ordered ASC by $.name. Row shape is `r.data ?? r` — raw stored doc data passed through largely unmapped (no field renaming like other routes).

Query parameters
NameTypeRequiredDefaultDescription
sportstringNoExact-match sport filter.
limitintegerNo100Tier-capped: free=50, api=200.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/reference/metadata?apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "name": "Boston Celtics",
      "sport": "basketball_nba",
      "league": "NBA",
      "conference": "Eastern",
      "division": "Atlantic",
      "venue": "TD Garden",
      "city": "Boston",
      "country": "USA",
      "abbreviation": "BOS"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 200,
    "requested_limit": 100,
    "sport": "basketball_nba",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 8415,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/reference/playersFree tier

Player metadata lookup by name and/or sport from the player_metadata collection.

Cache: free=86400s, api=43200s. Ordered DESC by $.fetchedAt (most recently updated first). Row shape passthrough (`r.data ?? r`).

Query parameters
NameTypeRequiredDefaultDescription
namestringNoExact-match player name filter.
sportstringNoExact-match sport filter.
limitintegerNo100Tier-capped: free=50, api=200.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/reference/players?apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "name": "Jayson Tatum",
      "sport": "basketball_nba",
      "team": "Boston Celtics",
      "position": "SF",
      "jersey_number": 0,
      "height": "6'8\"",
      "weight": "210 lbs",
      "birth_date": "1998-03-03",
      "fetchedAt": "2026-09-01T10:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 100,
    "name": "Jayson Tatum",
    "sport": "basketball_nba",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 997,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/reference/team-logosFree tier

Static team logo URLs and name aliases by sport, backfilled from team_metadata when the primary logo source is sparse.

Cache: free=86400s, api=43200s. When primary external_team_logos rows < limit, best-effort fills gaps from team_metadata (logoUrl/badgeUrl fields), deduping by team name; fallback failures are swallowed silently. No explicit creditCost → defaults to 1.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringYes400 'sport_key parameter required' if missing.
limitintegerNo100Tier-capped: free=200, api=1000.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/reference/team-logos?sport_key=aussierules_afl&apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "basketball_nba_bos",
      "names": [
        "Boston Celtics",
        "Celtics",
        "BOS"
      ],
      "logo": "https://a.espncdn.com/i/teamlogos/nba/500/bos.png",
      "sport_key": "basketball_nba",
      "external_id": "bos"
    },
    {
      "id": "team_metadata_gsw",
      "names": [
        "Golden State Warriors"
      ],
      "logo": "https://cdn.krokodds.com.au/logos/nba/gsw.png",
      "sport_key": "basketball_nba",
      "external_id": null,
      "source": "team_metadata"
    }
  ],
  "meta": {
    "count": 2,
    "tier": "free",
    "limit": 200,
    "requested_limit": 100,
    "sport_key": "basketball_nba",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 996,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/reference/venuesFree tier

Venue metadata (location, capacity, surface, etc.) lookup by name and/or sport.

Cache: free=86400s, api=43200s. Ordered DESC by $.fetchedAt. Row shape passthrough (`r.data ?? r`).

Query parameters
NameTypeRequiredDefaultDescription
namestringNoExact-match venue name filter.
sportstringNoExact-match sport filter.
limitintegerNo100Tier-capped: free=50, api=200.
apikey / api_keystringYesAPI key, alternatively via X-API-Key header.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/reference/venues?apikey / api_key=value"
Example response
{
  "success": true,
  "data": [
    {
      "name": "Marvel Stadium",
      "sport": "australianfootball_afl",
      "city": "Melbourne",
      "state": "VIC",
      "country": "Australia",
      "capacity": 53359,
      "surface": "grass",
      "roof": "retractable",
      "fetchedAt": "2026-08-15T06:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 100,
    "name": "Marvel Stadium",
    "sport": "australianfootball_afl",
    "timestamp": "2026-09-03T04:12:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 995,
      "reset": "2026-10-01"
    }
  }
}
GET/api/v1/rosterFree tier

Team roster/squad data from team_roster table. Currently supports AFL teams; extensible to all sports.

Reads from team_roster table (Supabase). team_id and sport are Supabase-level filters; team name is a post-filter. Sorted by jersey number ascending. Cached 86400s (24h).

Query parameters
NameTypeRequiredDefaultDescription
team_idnumberNoSofaScore team ID (e.g. 4452 for Fremantle).
teamstringNoTeam name filter (partial match, case-insensitive).
sportstringNoSport_key filter (e.g. "aussierules_afl").
limitnumberNo100Max rows, clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/roster?team_id=abc123"
Example response
{
  "success": true,
  "data": [
    {
      "sport_key": "aussierules_afl",
      "team_id": 4452,
      "team_name": "Fremantle",
      "season_id": null,
      "player_id": "p001",
      "player_name": "Andrew Brayshaw",
      "position": "Midfielder",
      "jersey_number": 8,
      "nationality": "AUS",
      "age": 25,
      "updated_at": "2026-09-01T00:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "sport": "aussierules_afl"
  }
}

Prediction Markets

3 endpoints

Polymarket and Kalshi sentiment plus a universal prediction-market catalogue and resolutions feed.

GET/api/v1/prediction-markets/sentimentFree tier

Directional sentiment (bullish/bearish/neutral) derived from Polymarket + Kalshi consensus pricing, plus a heuristic market-disagreement flag.

Tier detail: free — 'prediction_markets' IS in FREE_TIER_FEATURES

Cache: 300s flat (no per-tier split). Only status='open' rows queried. direction: bullish if consensus>0.6, bearish if <0.4, else neutral. disagreement: bid-ask spread>0.15 OR (2+ outcome prices, top two within 0.1 of each other) — reads as inverted/odd logic, worth flagging in docs. Uses guardV1Request/v1Success helper, not makeV1Route.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoILIKE substring match against the market question text (not a structured sport field), e.g. 'NFL', 'AFL', 'EPL'.
categorystringNoExact match on platform_category.
sourcestringNoboth'polymarket' or 'kalshi'. 400 on any other value.
limitintegerNo50Clamped 1-200 (MAX_LIMIT=200).
offsetintegerNo0Pagination offset.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/prediction-markets/sentiment?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "pm_will-chiefs-win-super-bowl-lx",
      "source": "polymarket",
      "question": "Will the Kansas City Chiefs win Super Bowl LX?",
      "category": "sports",
      "consensus_probability": 0.183,
      "volume": 842000,
      "liquidity": 61500,
      "price_direction": "bearish",
      "market_disagreement": false,
      "status": "open",
      "end_date": "2027-02-14",
      "fetched_at": "2026-09-03T03:45:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "license": "krokodds-derived",
    "filter": {
      "sport_key": "NFL",
      "category": null,
      "source": null
    },
    "sentiment_summary": {
      "bearish": 1
    },
    "note": "Prediction market sentiment derived from Polymarket + Kalshi pricing. Direction is heuristic from consensus probability.",
    "limit": 50
  }
}
GET/api/v1/prediction-markets/universalFree tier

Full-catalog Polymarket + Kalshi markets across EVERY category (weather, politics, crypto, economics, sports) — not just sports-matched markets. Synced every 30 min by universalMarketSync Cloud Function.

Cache: 300s flat. Distinct from /api/v1/odds-feed/prediction-markets, which serves the sports-matched subset (external_prediction_markets table) — this route is the raw full catalog (universal_prediction_markets table), not sport-filtered server-side (only via question-text search).

Query parameters
NameTypeRequiredDefaultDescription
sourcestringNo'polymarket' or 'kalshi'. 400 on invalid value.
categorystringNoExact match on platform_category.
statusstringNoopenOne of open|closed|settled|halted. 400 on invalid value.
limitintegerNo50Clamped 1-200.
offsetintegerNo0Pagination offset.
searchstringNoCase-insensitive ILIKE match on question text.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/prediction-markets/universal?source=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "pm_us-fed-rate-cut-september-2026",
      "source": "kalshi",
      "platform_category": "economics",
      "market_id": "FED-RATE-SEP26",
      "question": "Will the Fed cut rates at the September 2026 FOMC meeting?",
      "description": "Resolves YES if the FOMC announces a rate cut at its September 2026 meeting.",
      "outcomes": [
        "Yes",
        "No"
      ],
      "outcome_prices": [
        0.71,
        0.29
      ],
      "status": "open",
      "result": null,
      "volume": 1250000,
      "liquidity": 98000,
      "best_bid": 0.7,
      "best_ask": 0.72,
      "end_date": "2026-09-18",
      "created_at": "2026-07-01T00:00:00.000Z",
      "fetched_at": "2026-09-03T03:30:00.000Z",
      "resolved_at": null
    }
  ],
  "meta": {
    "count": 1,
    "license": "krokodds-proprietary",
    "filter": {
      "source": null,
      "category": "economics",
      "status": "open",
      "search": null
    },
    "pagination": {
      "limit": 50,
      "offset": 0
    },
    "note": "Full-catalog Polymarket + Kalshi markets across every category. Synced every 30 minutes.",
    "limit": 50
  }
}
GET/api/v1/prediction-markets/universal/resolutionsFree tier

Append-only archive of RESOLVED Polymarket + Kalshi markets (final outcome + prices at resolution), with a day-based lookback window.

Cache: 300s flat. Separate `market_resolutions` table (distinct from universal_prediction_markets) — append-only, never mutated after resolution. No explicit historical-days tier clamp applied to `days` param (unlike historical/* routes' since/from clamp) — same 30/365-day window available to free and api tiers alike.

Query parameters
NameTypeRequiredDefaultDescription
sourcestringNo'polymarket' or 'kalshi'. 400 on invalid value.
categorystringNoExact match on platform_category.
daysintegerNo30Lookback window in days for resolved_at, clamped 1-365.
limitintegerNo50Clamped 1-200.
offsetintegerNo0Pagination offset.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/prediction-markets/universal/resolutions?source=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "pm_us-fed-rate-cut-july-2026",
      "source": "kalshi",
      "market_id": "FED-RATE-JUL26",
      "question": "Will the Fed cut rates at the July 2026 FOMC meeting?",
      "platform_category": "economics",
      "outcomes": [
        "Yes",
        "No"
      ],
      "result": "No",
      "final_prices": [
        0.05,
        0.95
      ],
      "volume_at_resolution": 980000,
      "resolved_at": "2026-07-30T18:00:00.000Z",
      "captured_at": "2026-07-30T18:05:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "license": "krokodds-proprietary",
    "filter": {
      "source": "kalshi",
      "category": "economics",
      "days": 30
    },
    "pagination": {
      "limit": 50,
      "offset": 0
    },
    "note": "Resolved Polymarket + Kalshi markets, append-only resolution archive.",
    "limit": 50
  }
}

Other Sports

20 endpoints

Cricket, cycling, esports, Formula 1, golf, MMA, boxing, NFL and soccer coverage beyond the AU staples.

GET/api/v1/boxing/fight-resultsFree tier

Boxer career fight-results ledger (record, KO count, full fight list) by boxer slug or the most recently updated boxers.

Gated on the 'gameday' feature (not 'multisport') even though it's a static career record, not a live gameday signal — likely a copy/paste from another handler. No BigQuery fallback; hand-rolled auth preamble (predates makeV1Route helper) so it has no explicit creditCost/refund-on-empty logic.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key. Also accepted as ?apikey= or ?api_key=.
boxer_slugstringNoExact boxer slug; when set, does a direct single-document lookup instead of a listing query and ignores since/limit ordering.
sincestring (ISO date)NoOnly return boxers updated on/after this date. Clamped to the key's historical-window entitlement.
limitintegerNo50Max rows. Capped at 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/boxing/fight-results?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "boxer_slug": "marcus-oyelaran",
      "name": "Marcus Oyelaran",
      "total_fights": 24,
      "wins": 21,
      "losses": 2,
      "draws": 1,
      "no_contests": 0,
      "ko_wins": 15,
      "fights": [
        {
          "opponent": "Diego Salazar",
          "date": "2026-08-15",
          "result": "W",
          "method": "KO",
          "round": 6
        },
        {
          "opponent": "Kwame Boateng",
          "date": "2026-05-02",
          "result": "W",
          "method": "UD",
          "round": 12
        }
      ],
      "updated_at": "2026-09-02T11:14:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "boxer_slug": "marcus-oyelaran",
    "since": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 942,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/cricket/inningsFree tier

Cricsheet ball-by-ball derived per-player batting/bowling innings lines.

Returns 400 'sport_key, match_type, or season parameter required' if none supplied. When only match_type or season is given, over-fetches limit*6 rows and filters client-side, so results can be sparse relative to the requested limit on a narrow slice.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
sport_keystringYesFilter by internal sport/competition key. If set, this is the single indexed equality filter; match_type/season are applied in-memory afterward.
match_typestringYese.g. 'T20', 'ODI', 'Test'. Used as the indexed filter only when sport_key is absent.
seasonstringYesSeason label, e.g. '2026'. Stored/compared as a string, not a number.
limitintegerNo100Max rows. Capped at 100 (free) / 500 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/cricket/innings?X-API-Key=value&sport_key=aussierules_afl&match_type=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "bbl2026-m34-innings-rt-smith",
      "sportKey": "cricket_big_bash",
      "matchType": "T20",
      "season": "2026",
      "matchId": "bbl2026-m34",
      "player": "R. Thomson-Smith",
      "team": "Perth Scorchers",
      "battingRuns": 62,
      "battingBalls": 41,
      "fours": 6,
      "sixes": 3,
      "wickets": 0,
      "oversBowled": 0,
      "runsConceded": 0,
      "fetchedAt": "2026-09-02T13:05:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "sport_key": "cricket_big_bash",
    "match_type": null,
    "season": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 3,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/cricket/liveAPI plan

Recent/live cricket match snapshots (from the general cricket_matches archive, most-recently-fetched first).

Named 'live' but gated on 'historical' (paid-only) and reads a plain archive table sorted by fetchedAt DESC — no is-live/status filter server-side, so callers must filter status client-side. Free tier cannot access this endpoint at all.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
sportKeystringNoFilter by sport/competition key.
limitintegerNo100Max rows. Capped at 20 (free) / 50 (api) — note the config cap is far below the parse default of 100.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/cricket/live?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "cm-2026-09-03-ind-vs-eng",
      "sportKey": "cricket_test_match",
      "status": "in_progress",
      "homeTeam": "India",
      "awayTeam": "England",
      "venue": "Wankhede Stadium, Mumbai",
      "session": "Day 3, Session 2",
      "score": {
        "home": "312/6",
        "away": "not yet batted"
      },
      "fetchedAt": "2026-09-03T03:55:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "sportKey": "cricket_test_match",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1998,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/cycling/resultsAPI plan

Historical cycling race results (stage/GC placings).

Paid-only ('historical' feature). No date-range param — only exact race-name match; free tier gets a 402 regardless of params.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
racestringNoExact race name filter.
limitintegerNo100Max rows. Capped at 20 (free) / 50 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/cycling/results?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "race": "Vuelta a Espana",
      "stage": 18,
      "date": "2026-09-02",
      "rider": "Tobias Halvorsen",
      "team": "Visma | Lease a Bike",
      "position": 1,
      "time": "4:12:08",
      "gc_position": 2,
      "fetchedAt": "2026-09-02T18:40:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 20,
    "requested_limit": 100,
    "race": "Vuelta a Espana",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 20,
      "remaining": 1997,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/cycling/ridersAPI plan

Cycling rider profiles/roster records.

Both name and team filters are exact-match equality, no partial/fuzzy search.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
namestringNoExact rider name filter.
teamstringNoExact team name filter.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/cycling/riders?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "name": "Tobias Halvorsen",
      "team": "Visma | Lease a Bike",
      "nationality": "Norway",
      "dob": "1999-04-11",
      "specialty": "Climber",
      "fetchedAt": "2026-08-30T09:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "name": null,
    "team": "Visma | Lease a Bike",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1996,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/cycling/startlistsAPI plan

Cycling race startlists by race and date.

Both filters exact-match; no range query for date.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
racestringNoExact race name filter.
datestring (YYYY-MM-DD)NoExact date filter.
limitintegerNo100Max rows. Capped at 20 (free) / 50 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/cycling/startlists?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "race": "Vuelta a Espana",
      "date": "2026-09-03",
      "stage": 19,
      "riders": [
        {
          "name": "Tobias Halvorsen",
          "team": "Visma | Lease a Bike",
          "bib": 21
        },
        {
          "name": "Elia Fontaine",
          "team": "UAE Team Emirates",
          "bib": 1
        }
      ],
      "fetchedAt": "2026-09-03T00:30:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 20,
    "requested_limit": 100,
    "race": "Vuelta a Espana",
    "date": "2026-09-03",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 20,
      "remaining": 1995,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/esports/leaguesAPI plan

Esports league/competition metadata.

Simple single-filter listing, no search-by-name.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
sportKeystringNoFilter by sport/game key, e.g. 'esports_lol'.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/esports/leagues?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "sportKey": "esports_lol",
      "leagueId": "lck-2026-summer",
      "name": "LCK 2026 Summer",
      "region": "Korea",
      "game": "League of Legends",
      "season": "2026 Summer",
      "fetchedAt": "2026-08-28T05:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "sportKey": "esports_lol",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1994,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/esports/matchesAPI plan

Esports match schedule/results, ordered by start time ascending.

Ordered by $.beginAt ASC (upcoming-first), unlike most sibling endpoints which order DESC by fetchedAt.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
sportKeystringNoFilter by sport/game key.
statusstringNoMatch status filter, e.g. 'not_started', 'running', 'finished'.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/esports/matches?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "sportKey": "esports_csgo",
      "matchId": "blast-premier-fall-2026-m12",
      "status": "not_started",
      "league": "BLAST Premier Fall Groups 2026",
      "teamA": "Team Vitality",
      "teamB": "Natus Vincere",
      "beginAt": "2026-09-04T10:00:00.000Z",
      "bestOf": 3,
      "fetchedAt": "2026-09-03T02:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "sportKey": "esports_csgo",
    "status": "not_started",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1993,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/esports/teamsAPI plan

Esports team profiles/roster metadata.

Same skeleton as esports/leagues, single equality filter on sportKey.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
sportKeystringNoFilter by sport/game key.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/esports/teams?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "sportKey": "esports_lol",
      "teamId": "t1",
      "name": "T1",
      "region": "Korea",
      "roster": [
        "Zeus",
        "Oner",
        "Faker",
        "Gumayusi",
        "Keria"
      ],
      "fetchedAt": "2026-08-25T06:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "sportKey": "esports_lol",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1992,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/f1/racesFree tier

Formula 1 race schedule/status (API-Sports feed) — season, circuit, weather, fastest lap.

Code comment flags a past bug fix: this was previously gated on 'racing' (AU racing tier) which wrongly locked F1 out; now correctly gated on 'multisport'. Distinct dataset from /v1/f1/results (this is the API-Sports schedule, not Ergast/Jolpica results).

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
race_idstringNoExact race id; does a direct single-document lookup, ignoring season/since/limit.
seasonintegerNoSeason year, e.g. 2026.
sincestring (ISO date)NoOnly races updated on/after this date. Clamped to the key's historical-window entitlement.
limitintegerNo50Max rows. Capped at 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/f1/races?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "race_id": "f1-2026-r16-monza",
      "season": 2026,
      "competition": "Formula 1",
      "circuit": "Autodromo Nazionale Monza",
      "date": "2026-09-06T13:00:00.000Z",
      "type": "Race",
      "status": "Scheduled",
      "laps": 53,
      "distance": "306.72km",
      "timezone": "Europe/Rome",
      "weather": "Sunny, 26C",
      "fastest_lap": null,
      "updated_at": "2026-09-03T01:20:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "race_id": null,
    "season": 2026,
    "since": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 940,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/f1/resultsFree tier

Formula 1 driver race results per season/round (Ergast/Jolpica feed).

Separate source/collection from /v1/f1/races — do not conflate the two in docs. When 'round' is passed without narrowing further, over-fetches limit*6 rows to filter client-side, so results can undercount near the tail of large seasons.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
seasonintegerNoSeason year. Returns 400 if non-numeric.
roundintegerNoRace round number within the season. Returns 400 if non-numeric. Filtered in-memory after an over-fetch.
limitintegerNo100Max rows. Capped at 100 (free) / 500 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/f1/results?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "season": "2026",
      "round": 16,
      "raceName": "Italian Grand Prix",
      "driver": "Lando Norris",
      "constructor": "McLaren",
      "grid": 1,
      "position": 1,
      "points": 25,
      "status": "Finished",
      "fastestLapTime": "1:21.046"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "season": 2026,
    "round": 16,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 939,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/golf/rankingsAPI plan

Golf world/tour rankings by player.

No tour/date param — single collection covers all rankings snapshots, latest by fetchedAt.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
playerstringNoExact player name filter.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/golf/rankings?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "player": "Ludvig Karlberg",
      "rank": 4,
      "points": 8.42,
      "events_played": 19,
      "tour": "PGA Tour",
      "fetchedAt": "2026-09-01T12:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "player": "Ludvig Karlberg",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1991,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/golf/skillsAPI plan

Golf strokes-gained / approach-shot skill ratings by player.

`source=approach` switches to a different underlying collection (golf_approach_skills) with an approach-shot-specific shape — document both variants.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
sourcestringNoskillsWhich dataset: 'skills' (golf_player_skills) or 'approach' (golf_approach_skills). 400 if any other value.
playerstringNoExact player name filter.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/golf/skills?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "player": "Ludvig Karlberg",
      "sg_total": 2.14,
      "sg_off_tee": 0.61,
      "sg_approach": 0.88,
      "sg_around_green": 0.34,
      "sg_putting": 0.31,
      "fetchedAt": "2026-08-30T09:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "source": "skills",
    "player": "Ludvig Karlberg",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1990,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/golf/statsAPI plan

Golf player statistical projections (most recent snapshot).

No query filters at all besides limit — collection is 'golf_projections' despite the route name 'stats'; there is no player/event filter param.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/golf/stats?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "player": "Ludvig Karlberg",
      "event": "BMW Championship",
      "projected_finish": 6,
      "win_prob": 0.041,
      "top10_prob": 0.32,
      "made_cut_prob": 0.91,
      "fetchedAt": "2026-08-29T15:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1989,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/leaderboards/soccerFree tier

Soccer league top-scorers or top-assists leaderboard for a league+season.

Data shape differs from every other endpoint here: `data` is a single object (not an array) containing a `players` array. Backing docs are chunked (>1MB Firestore doc limit split into `{docId}_c{i}` chunk docs) and reassembled server-side; Supabase path falls back to Firestore on any read error. If the doc doesn't exist, returns success:true with data:null and count:0 (not a 404).

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
kindstringNotopscorers'topscorers' or 'topassists'. Any other value returns 400.
league_idstringYesLeague id, e.g. '39' (Premier League). Required — 400 if missing.
seasonstringYesSeason year, e.g. '2026'. Required — 400 if missing.
limitintegerNo100Max players returned. Capped at 100 (free) / 5000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/leaderboards/soccer?X-API-Key=value&league_id=abc123&season=value"
Example response
{
  "success": true,
  "data": {
    "league_id": "39",
    "league": "Premier League",
    "season": "2026",
    "kind": "topscorers",
    "players": [
      {
        "rank": 1,
        "player": "Erling Haaland",
        "team": "Manchester City",
        "goals": 9,
        "appearances": 4
      },
      {
        "rank": 2,
        "player": "Alexander Isak",
        "team": "Liverpool",
        "goals": 7,
        "appearances": 4
      }
    ],
    "count": 2,
    "updated_at": "2026-09-02T22:00:00.000Z"
  },
  "meta": {
    "count": 2,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "kind": "topscorers",
    "league_id": "39",
    "season": "2026",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 946,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/mma/fightsFree tier

MMA fight card entries/results by fight id or recently-updated window.

Same hand-rolled auth preamble pattern as boxing/fight-results and gated on 'gameday' rather than 'multisport'.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
fight_idstringNoExact fight id; does a direct single-document lookup, ignoring since/limit.
sincestring (ISO date)NoOnly fights updated on/after this date. Clamped to the key's historical-window entitlement.
limitintegerNo50Max rows. Capped at 50 (free) / 2000 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/mma/fights?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "fight_id": "ufc-306-fontaine-vs-reyes",
      "date": "2026-09-06",
      "time": "22:00",
      "slug": "ufc-306",
      "status": "scheduled",
      "is_main": true,
      "category": "Welterweight",
      "fighters": [
        {
          "name": "Diego Fontaine",
          "record": "19-3-0"
        },
        {
          "name": "Marcus Reyes",
          "record": "17-2-0"
        }
      ],
      "league": "UFC",
      "country": "USA",
      "updated_at": "2026-09-02T20:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "fight_id": null,
    "since": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 938,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/nfl/scheduleFree tier

NFL full-season schedule with teams, venues, kickoff times, and starting QBs.

The only endpoint in this batch built on the newer guardV1Request/v1Success helper (adds X-Krok-Data-Source: derived header and license:'krokodds-derived' in meta). Explicit creditCost=1 passed to guardV1Request. Sorted by gameday then gametime.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
seasonintegerNocurrent calendar yearSeason year. 400 if non-numeric when provided.
weekintegerNoWeek number filter. 400 if non-numeric when provided.
teamstringNoCase-insensitive team name substring OR exact abbreviation match, checked against both home and away.
limitintegerNo100Max rows, 1-500.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/nfl/schedule?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "2026-w1-kc-bal",
      "game_id": "2026090400",
      "season": 2026,
      "season_type": "REG",
      "week": 1,
      "gameday": "2026-09-04",
      "weekday": "Thursday",
      "gametime": "20:20",
      "away_team": "Kansas City Chiefs",
      "away_abbr": "KC",
      "home_team": "Baltimore Ravens",
      "home_abbr": "BAL",
      "stadium": "M&T Bank Stadium",
      "location": "Baltimore, MD",
      "result": null,
      "overtime": false,
      "away_qb": "Patrick Mahomes",
      "home_qb": "Lamar Jackson"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "season": 2026,
    "week": 1,
    "team": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 950,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/soccer/bundesligaAPI plan

OpenLigaDB Bundesliga / 2.Bundesliga / DFB-Pokal match results — fallback settlement source for German-football props.

creditCost=5 (archive-tier weight) despite the endpoint label being a plain fixtures list. `league` is mandatory. season/matchday applied in-memory after an 8x over-fetch when combined with league.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
leaguestringYesOne of 'bl1' (Bundesliga), 'bl2' (2. Bundesliga), 'dfb' (DFB-Pokal). Required — 400 if missing or invalid.
seasonintegerNoStart-year of the season, e.g. 2025 for 2025/26.
matchdayintegerNoMatchday/round number within the season.
limitintegerNo100Max rows. Capped at 100 (free) / 500 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/soccer/bundesliga?X-API-Key=value&league=value"
Example response
{
  "success": true,
  "data": [
    {
      "matchId": 68321,
      "date": "2026-08-30T13:30:00.000Z",
      "matchday": "3. Spieltag",
      "homeTeam": "Bayer 04 Leverkusen",
      "awayTeam": "Borussia Dortmund",
      "homeGoals": 2,
      "awayGoals": 2,
      "homeGoalsHT": 1,
      "awayGoalsHT": 1,
      "finished": true,
      "location": "BayArena, Leverkusen"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 100,
    "requested_limit": 100,
    "league": "bl1",
    "season": 2025,
    "matchday": 3,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 1985,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/soccer/closing-oddsFree tier

football-data.co.uk settled soccer matches with Bet365 + Pinnacle opening AND closing prices (Asian handicap, O/U 2.5) — the CLV reference dataset.

creditCost=5 despite being on the free tier ('closing_lines' is a FREE_TIER_FEATURES member) — free-tier callers still burn 5 credits per call. 400 if neither league/league_code nor season is provided. from/to are plain string comparisons against a YYYY-MM-DD date field, not true date-range queries at the DB layer.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
leaguestringYesLeague code (e.g. 'E0' for EPL). Also accepted as league_code.
league_codestringYesAlias for league.
seasonstringYesSeason label, e.g. '2025-2026'.
fromstring (YYYY-MM-DD)NoInclusive start date filter (in-memory, string comparison on date).
tostring (YYYY-MM-DD)NoInclusive end date filter (in-memory, string comparison on date).
limitintegerNo100Max rows. Capped at 100 (free) / 500 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/soccer/closing-odds?X-API-Key=value&league=value&league_code=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "fdcouk-e0-2025-2026-020",
      "date": "2026-08-30",
      "time": "17:30",
      "league_code": "E0",
      "season": "2025-2026",
      "home_team": "Arsenal",
      "away_team": "Tottenham",
      "ftr": "H",
      "htr": "D",
      "ft_home_goals": 2,
      "ft_away_goals": 1,
      "b365_open": {
        "home": 1.65,
        "draw": 4,
        "away": 5.5
      },
      "b365_close": {
        "home": 1.58,
        "draw": 4.1,
        "away": 6
      },
      "pinnacle_open": {
        "home": 1.68,
        "draw": 3.95,
        "away": 5.3
      },
      "pinnacle_close": {
        "home": 1.6,
        "draw": 4.05,
        "away": 5.8
      },
      "asian_handicap": {
        "line": -0.75,
        "home_close": 1.95,
        "away_close": 1.97
      },
      "over_under_25": {
        "over_close": 1.85,
        "under_close": 2
      }
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "league_code": "E0",
    "season": null,
    "from": "2026-08-01",
    "to": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 944,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/soccer/fixturesAPI plan

Soccer fixtures/results by sport key and status, ordered by kickoff time ascending.

Ordered by $.utcDate ASC (upcoming-first), like esports/matches, unlike the DESC-by-fetchedAt convention used by most other collection routes.

Query parameters
NameTypeRequiredDefaultDescription
X-API-KeystringYesAPI key.
sportKeystringNoFilter by sport/competition key, e.g. 'soccer_epl'.
statusstringNoMatch status filter, e.g. 'SCHEDULED', 'IN_PLAY', 'FINISHED'.
limitintegerNo100Max rows. Capped at 50 (free) / 200 (api).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/soccer/fixtures?X-API-Key=value"
Example response
{
  "success": true,
  "data": [
    {
      "sportKey": "soccer_epl",
      "matchId": "epl-2026-gw4-avl-che",
      "status": "SCHEDULED",
      "utcDate": "2026-09-06T14:00:00.000Z",
      "homeTeam": "Aston Villa",
      "awayTeam": "Chelsea",
      "venue": "Villa Park",
      "matchday": 4
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 100,
    "sportKey": "soccer_epl",
    "status": "SCHEDULED",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 1980,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}

Infrastructure

14 endpoints

Account and billing endpoints, bulk export, SSE streaming, webhook management and API health.

GET/api/v1/advanced-statsFree tier

Multiplexed advanced/derived stat archives across 14 sources (MLB Statcast, NHL/MoneyPuck skaters+goalies, NCAAB, soccer xG/FBref, NCAAF, NFL NGS/snaps/depth, NRL, AFL, tennis closing odds), selected via ?source=.

Tier detail: free (advanced_stats is in FREE_TIER_FEATURES; both free and api tier keys can call it)

Built on the shared makeV1Route factory. Cache TTL: free=3600s, api=1800s. Falls back to BigQuery mirror only if loadSupabase throws AND a mirror exists; the soccer advanced stats source has no BQ mirror, so it reads from Supabase. Empty result set returns HTTP 204 and refunds the credit.

Query parameters
NameTypeRequiredDefaultDescription
sourcestringYesOne of: mlb_advanced, nhl_skaters, ncaab_schools, soccer_xg, ncaaf_player_games, nfl_ngs, nfl_snaps, nfl_depth, soccer_fbref, nrl_player_games, afl_matches, tennis_closing_odds, nhl_moneypuck_skaters, nhl_moneypuck_goalies. 400 V1BadRequest if not in this list.
seasonintegerNoSeason/year filter (field name varies per source: 'season' or 'year'). Non-numeric value returns 400.
team | playerType | league | sportKey | opponent | flavor | round | teamNickname | playerSlug | team1 | team2 | venue | tour | surface | position | situation | compId | squadstringNoPer-source in-memory filter, only the subset listed in that source's `extra` array is honored (e.g. mlb_advanced accepts team/playerType; nhl_moneypuck_skaters accepts team/position/playerSlug/situation, defaulting situation to 'all' when omitted).
limitintegerNo100Row cap. Free tier max 100, api tier max 500.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/advanced-stats?source=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "abc123",
      "season": 2026,
      "team": "NYY",
      "playerType": "batter",
      "xwOBA": 0.361,
      "barrelPct": 9.8
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "source": "mlb_advanced",
    "season": 2026,
    "filters": {
      "team": "nyy"
    },
    "timestamp": "2026-09-03T00:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GETPOST/api/v1/bets/trackFree tier

GET returns a user's tracked-bet history plus a computed P&L summary (by sport, by bookmaker, streaks, monthly breakdown); POST logs a new bet.

Tier detail: free (bet_tracking is in FREE_TIER_FEATURES)

Requires a valid API key with the bet_tracking feature (via guardV1Request) PLUS a Firebase-session-derived `x-user-id` request header — the API key alone is not sufficient, so this is a hybrid api_key+session auth route despite being under v1. Reads/writes the `bets` Supabase table directly (no Firestore fallback in this file). POST returns 201 with the created row and its plaintext id (no secret hidden). GET response cached 60s via unstable_cache, keyed per user+filters.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter bets by sport.
statusstringNoFilter by bet status: pending | won | lost | void.
fromstring (YYYY-MM-DD)NoStart date filter (created_at >= from).
tostring (YYYY-MM-DD)NoEnd date filter (created_at <= to 23:59:59).
limitintegerNo50Max results, clamped 1-200.
offsetintegerNo0Pagination offset.
Body (JSON)
NameTypeRequiredDefaultDescription
event_idstringYesEvent identifier.
sport_keystringYesSport key.
marketstringNo""Market type, e.g. h2h, spread, total.
selectionstringNoWhat was backed.
oddsnumberNoDecimal odds.
stakenumberNoStake amount (AUD). potential_return is auto-computed as odds*stake when both are present.
statusstringNopendingpending | won | lost | void.
commence_timestring (ISO)NoEvent start time; passed through into the stored `data` jsonb blob (not a dedicated column).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_id":"evt_abc123","sport_key":"aussierules_afl","market":"\"\"","selection":"value","odds":"10","stake":"10","status":"pending","commence_time":"value"}' \
  "https://krokodds.com.au/api/v1/api/v1/bets/track?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "u1_1735689600_ab12cd",
      "user_id": "u1",
      "event_id": "evt123",
      "sport_key": "basketball_nba",
      "market": "h2h",
      "selection": "Lakers",
      "odds": 1.85,
      "stake": 50,
      "potential_return": 92.5,
      "profit_loss": null,
      "status": "pending",
      "created_at": "2026-09-01T00:00:00.000Z"
    }
  ],
  "meta": {
    "license": "krokodds-derived",
    "summary": {
      "total_bets": 12,
      "pending": 2,
      "won": 7,
      "lost": 3,
      "void": 0,
      "total_stake": 600,
      "total_return": 740,
      "net_profit": 140,
      "roi_pct": 23.33,
      "win_rate": 70,
      "by_sport": {
        "basketball_nba": {
          "bets": 5,
          "stake": 250,
          "profit": 60,
          "roi": 24,
          "win_rate": 66.67
        }
      },
      "by_bookmaker": {
        "sportsbet": {
          "bets": 5,
          "stake": 250,
          "profit": 60,
          "roi": 24,
          "win_rate": 66.67
        }
      },
      "streak": {
        "current": 2,
        "type": "win",
        "best_win": 4,
        "best_loss": 2
      },
      "monthly": {
        "2026-08": {
          "bets": 6,
          "stake": 300,
          "profit": 70,
          "roi": 23.33
        }
      }
    },
    "pagination": {
      "limit": 50,
      "offset": 0
    }
  }
}
GET/api/v1/bookmakersFree tier

Static-ish catalog of AU-facing bookmakers KrokOdds supports (id, name, provider, feed_group, is_aggregator, is_exchange, supported flag).

Tier detail: free (no tierAllowsFeature gate at all — any valid key of any tier can call it)

24h Cache-Control (private, max-age=86400, stale-while-revalidate=172800) — deliberately 'private' not 'public' since a shared/CDN cache would leak the auth-gated response to keyless callers (CDN keys on URL only, ignores X-API-Key). X-Krok-Version: 2 (higher than most other v1 routes, which are version 1).

Query parameters
NameTypeRequiredDefaultDescription
regionstringNoauRegion filter; only 'au' returns rows, anything else (non-empty) returns an empty array — forward-compat hook, AU is the only region currently supported.
providerstringNoFilter by provider metadata (matches provider field or provider === 'both').
include_aggregatorsboolean ("true"/"false" string)NofalseWhen true, also includes AGGREGATOR_FEEDS (deduped clone lines, no arb value) marked supported:false.
fieldsstring (comma-separated)NoField projection via parseFields/projectRows.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/bookmakers?region=au"
Example response
{
  "success": true,
  "data": [
    {
      "id": "sportsbet",
      "name": "Sportsbet",
      "region": "au",
      "odds_api_key": "sportsbet",
      "supported": true,
      "provider": "krok-odds",
      "feed_group": "sportsbet",
      "is_aggregator": false,
      "is_exchange": false
    }
  ],
  "meta": {
    "count": 42,
    "tier": "free",
    "region": "au",
    "provider": "all",
    "include_aggregators": false,
    "timestamp": "2026-09-03T00:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 999,
      "reset": ""
    }
  }
}
GET/api/v1/bulkAPI plan

Enterprise cursor-paginated JSON bulk export of the historical archive across 13 datasets (team/player game logs, prop results, game results, MLB/soccer/NCAAF/NFL/NHL/NRL advanced stats, Betfair BSP/results). No ?dataset= returns the dataset manifest.

Tier detail: api (bulk_export is paid-only, not in FREE_TIER_FEATURES)

creditCost: 25 (enterprise-weighted, set explicitly in makeV1Route config). Cache TTL 3600s both tiers (archive is immutable so cached hard). Cursor pagination via supabasePageById (`.order(id).gt(id, cursor).limit(n)`) — index-cheap and resumable. Companion CSV/JSON export at /api/v1/export shares the same bulk_export gate and 25-credit cost.

Query parameters
NameTypeRequiredDefaultDescription
datasetstringNoOne of: team_game_logs, player_game_logs, player_history, prop_results, game_results, mlb_advanced, soccer_xg, ncaaf_player_games, nfl_tracking, nhl_skaters, nrl_player_games, exchange_bsp, exchange_results. Omitted → returns manifest (list of {dataset, description}) with no DB read.
sport_key | sportstringNoOnly supported for player_game_logs and nrl_player_games datasets; 400 otherwise (message points caller to /v1/historical/player-game-log).
cursorstringNoOpaque id cursor for resumable pagination — pass back `next_cursor` from the previous response until it's null.
limitintegerNo100Row cap. Free tier max 100, api tier max 500 (though the feature itself is api-tier gated).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/bulk?dataset=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "0001",
      "sportKey": "americanfootball_nfl",
      "team": "KC",
      "gameDate": "2026-08-30"
    }
  ],
  "meta": {
    "count": 100,
    "tier": "api",
    "limit": 100,
    "requested_limit": 100,
    "dataset": "team_game_logs",
    "collection": "team_game_log",
    "next_cursor": "0100",
    "has_more": true,
    "timestamp": "2026-09-03T00:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 49975,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/closing-linesFree tier

Cross-event aggregator of opening vs closing H2H odds (for CLV computation at scale) — flips the single-event odds-history ladder inside out into many events with opens/close/movement.

Tier detail: free (closing_lines is in FREE_TIER_FEATURES)

Hand-rolled route (not makeV1Route) with its own dual-source fallback: primary `odds_history` table (has opening_odds), falls back to legacy `gameday_odds_history` shape if odds_history returns 0 rows or throws — the two sources use different field casing (commence_time vs commenceTime) which the route normalizes. Cache 300s free / 60s api. Distinct from the single-event ladder at /v1/odds-history?event_id=.

Query parameters
NameTypeRequiredDefaultDescription
sport_key | sportstringNoFilter by sport.
sincestring (date/ISO)NoClamped by clampHistoricalFrom() against the key's historicalDays window.
untilstring (date/ISO)NoUpper bound on commence_time.
only_closedboolean ("true"/"false")NotrueWhen true, only events whose commence_time has already passed are returned.
limitintegerNo50Free tier max 50, api tier max 2000.
cursorstring (opaque)NoKeyset cursor encoding {v: commence_time ISO, id}.
fieldsstring (comma-separated)NoField projection.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/closing-lines?sport_key | sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "evt-1",
      "event_id": "evt-1",
      "sport_key": "basketball_nba",
      "home_team": "Lakers",
      "away_team": "Celtics",
      "commence_time": "2026-09-02T00:00:00.000Z",
      "is_closed": true,
      "opens": {
        "home": 1.9,
        "away": 1.95,
        "opened_at_ms": null,
        "home_implied_prob": 0.5263,
        "away_implied_prob": 0.5128
      },
      "close": {
        "home": 1.83,
        "away": 2.05,
        "home_book": "sportsbet",
        "away_book": "sportsbet",
        "captured_at_ms": null,
        "home_implied_prob": 0.5464,
        "away_implied_prob": 0.4878
      },
      "movement": {
        "home_delta": -0.07,
        "away_delta": 0.1,
        "home_direction": "shorten",
        "away_direction": "drift"
      },
      "snapshot_count": 0,
      "updated_at": "2026-09-02T05:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": "basketball_nba",
    "since": null,
    "until": null,
    "only_closed": true,
    "next_cursor": null,
    "timestamp": "2026-09-03T00:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 999,
      "reset": ""
    }
  }
}
GET/api/v1/clv-archive/{eventId}Free tier

Fetch the archived closing odds snapshot for a single event by path parameter.

Tier detail: any signed-in user (no tier/feature gate — this is NOT an API-key route despite living under /api/v1/)

This route authenticates with the dashboard's browser session, not an API key – it isn't callable from third-party integrations.

IMPORTANT DISCREPANCY: this route uses Firebase session auth (getServerUser()), returning 401 Unauthorized if no signed-in user — it does NOT use verifyApiKey / X-API-Key at all, unlike almost every other /v1/* route. Reads Supabase `clv_archive` table by doc id first (isSupabase('clv-archive') gate), falls back to Firestore `clv_archive` collection on Supabase error. 404 if the event isn't archived.

Path parameters
NameTypeRequiredDefaultDescription
eventIdstring (path param)YesDynamic route segment [eventId]; trimmed, 400 if empty after trim.
Example request
curl \
  # dashboard-only: uses your browser session, not an API key
  "https://krokodds.com.au/api/v1/api/v1/clv-archive/evt_abc123"
Example response
{
  "success": true,
  "data": {
    "event_id": "evt-1",
    "sport": "basketball_nba",
    "sport_title": "NBA",
    "home_team": "Lakers",
    "away_team": "Celtics",
    "commence_time": "2026-09-02T00:00:00.000Z",
    "bookmakers": [],
    "archived_at": "2026-09-02T05:00:00.000Z",
    "expire_at": null
  }
}
GET/api/v1/ev-hit-ratesFree tier

Historical hit-rate / calibration stats for +EV bets, aggregated by market category (total_bets, hit_count, hit_rate, avg_clv, avg_ev).

Tier detail: free (positive_ev is in FREE_TIER_FEATURES)

Hand-rolled route with a THREE-tier fallback chain: Supabase (ev_hit_rates table) -> point-get on Firestore evHitRates if category set -> BigQuery mirror -> full Firestore collection scan (capped at min(limit,500)). Cache TTL 3600s free / 600s api. Per memory note 'ev-records-empty' class issues have hit similar tables historically — verify live data before citing exact numbers in docs.

Query parameters
NameTypeRequiredDefaultDescription
categorystringNoMarket category id — when set, does a point-get by doc id (category) instead of a scan.
limitintegerNo100Free tier max 100, api tier max 5000.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/ev-hit-rates?category=value"
Example response
{
  "success": true,
  "data": [
    {
      "category": "h2h",
      "total_bets": 542,
      "hit_count": 301,
      "hit_rate": 0.5554,
      "avg_clv": 1.8,
      "avg_ev": 4.2,
      "updated_at": "2026-09-01T00:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "category": null,
    "timestamp": "2026-09-03T00:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 999,
      "reset": ""
    }
  }
}
GET/api/v1/exchangeFree tier

KrokOdds Exchange (Betfair) pricing suite multiplexed by ?source=: settled racing BSP, race results, sports match-odds, scorer props, and live pre-jump snapshots.

Tier detail: free (betfair_exchange is in FREE_TIER_FEATURES — filterExchangeForHobby in api-tier-gate.ts is now a no-op, exchange pricing is available on all tiers)

Built on makeV1Route, Supabase-only (no loadBq). Cache TTL 600s free / 300s api. Underlying writers are functions/src/external/betfair/*. Per CLAUDE.md, this exchange endpoint is distinct from the max-fidelity odds_archive/racing_odds_archive firehose tables (migration 056) which are internal-only, not exposed via this API.

Query parameters
NameTypeRequiredDefaultDescription
sourcestringYesOne of: bsp, results, match_odds, scorer_props, snapshots. 400 if not in this list.
raceKeystringNoRacing filter for bsp/results, keyed `${venueSlug}_${raceNumber}`.
venueSlug | marketTypestringNoAdditional bsp filters (in-memory, post-fetch).
sportstringNoEquality filter (indexed path) for match_odds/scorer_props/snapshots sources.
eventIdstringNoIn-memory filter for match_odds/scorer_props/snapshots.
limitintegerNo100Free tier max 100, api tier max 500.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/exchange?source=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "flem_r5",
      "raceKey": "flemington_5",
      "venueSlug": "flemington",
      "marketType": "WIN",
      "bsp": 4.6,
      "runnerName": "Fast Horse"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "source": "bsp",
    "filters": {
      "raceKey": "flemington_5"
    },
    "timestamp": "2026-09-03T00:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 999,
      "reset": ""
    }
  }
}
GET/api/v1/exportAPI plan

Enterprise bulk data export as downloadable CSV (or JSON) from the BigQuery archive mirror — companion to the JSON-only /v1/bulk endpoint.

Tier detail: api (bulk_export is paid-only)

Uses guardV1Request(request, 'bulk_export', 25) — same 25-credit weighting as /v1/bulk. 503 if BQ_ENABLED is false or the dataset's BQ mirror doesn't exist yet ('Dataset not yet available'). CSV response sets filename `krok-{datasetKey}-{YYYY-MM-DD}.csv` and header X-Krok-Export-Rows with the actual row count. Cache 1800s for the json format variant.

Query parameters
NameTypeRequiredDefaultDescription
datasetstringYesOne of: game_results, player_props_results, model_track_record, gameday_odds_history, clv_archive, ev_hit_rates. 400 if missing/unknown.
limitintegerNo5000Capped at 5000 for both free and api tier (LIMIT_BY_TIER).
formatstringNocsvcsv (file download, Content-Disposition attachment) or json (standard v1 envelope via v1Success).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/export?dataset=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "g1",
      "sport_key": "americanfootball_nfl",
      "home_score": 24,
      "away_score": 17,
      "completed": true
    }
  ],
  "meta": {
    "dataset": "game_results",
    "label": "Settled game results",
    "limit": 5000,
    "requested_limit": 5000
  }
}
GET/api/v1/extended-marketsFree tier

Derivative/extended odds markets excluded from the core h2h/spreads/totals feed — half/period/inning splits, soccer corners+cards, AFL/NRL winning_margin_bands, team_winning_margin, alt-totals — from our proprietary collection of 140+ AU bookmakers. Key market keys: winning_margin_bands (NRL 1-12/13+), team_winning_margin (Soccer Win By 2+/3+/4+), total_points_odd_even, ht_ft, highest_scoring_half.

Tier detail: free (advanced_stats is in FREE_TIER_FEATURES, so free keys reach this data too — the licence boundary here is Terms 6.1, not the tier gate)

Every row carries `source` ('scraped') and `redistributable` (bool) — licensed aggregator rows are FORBIDDEN_RESALE tier (b) under Terms 6.1, internal-use-only, never redistributable; KrokOdds' own scrape is redistributable. The three core markets (h2h, spreads, totals) are deliberately excluded — those live at /v1/odds. `meta.provider` field is @deprecated in favor of per-event `source`. Cache 300s free / 60s api; the two source tiers are fetched via Promise.allSettled so one failing doesn't take the other down.

Query parameters
NameTypeRequiredDefaultDescription
sport_key | sportstringNoSport filter (used when event_id absent).
event_idstringNoPoint-read a single event across both tiers instead of a windowed scan.
marketstringNoPost-merge market-key filter, applied after both tiers are combined.
sourcestringNoallall | scraped. 'scraped' returns only the redistributable KrokOdds-owned tier.
limitintegerNo50Free tier max 50, api tier max 2000; applied per-tier before merge.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/extended-markets?sport_key | sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "evt-1",
      "event_key": null,
      "sport_key": "soccer_epl",
      "sport_title": "EPL",
      "home_team": "Arsenal",
      "away_team": "Chelsea",
      "commence_time": "2026-09-05T00:00:00.000Z",
      "bookmakers": [
        {
          "key": "sportsbet",
          "title": "Sportsbet",
          "last_update": "2026-09-03T00:00:00.000Z",
          "markets": [
            {
              "key": "corners_over_under",
              "single_sided": false,
              "outcomes": [
                {
                  "name": "Over",
                  "description": "9.5",
                  "price": 1.9,
                  "point": 9.5,
                  "bet_link": "https://..."
                }
              ]
            }
          ]
        }
      ],
      "source": "scraped",
      "redistributable": true
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": "soccer_epl",
    "event_id": null,
    "market": null,
    "source": "all",
    "provider": "scraped",
    "providers": [
      "scraped"
    ],
    "timestamp": "2026-09-03T00:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 999,
      "reset": ""
    }
  }
}
GET/api/v1/meFree tier

API key introspection — returns the caller's tier, monthly usage/limit/remaining, per-request rate limit window, reset date, and a masked key preview.

Tier detail: free (no feature/tier gate at all — just requires a valid, active key)

CORRECTION vs. task brief: this route is API-KEY auth (X-API-Key header / apikey / api_key query param -> verifyApiKey), NOT Firebase session auth (getServerUser) — it does not import server-auth at all. Only webhooks/route.ts and clv-archive/[eventId]/route.ts use session auth in this batch of 14. PER_TIER_REQUEST_LIMITS here: free=100, api=10000 (note: api's 10000 differs from the 500-ish caps seen on most other free/api routes).

Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/me"
Example response
{
  "success": true,
  "data": {
    "tier": "free",
    "monthly_limit": 1000,
    "current_usage": 42,
    "remaining": 958,
    "usage_pct": 4.2,
    "reset_date": "2026-10-01T00:00:00.000Z",
    "per_request_limit": 100,
    "rate_limit_window": {
      "limit": 60,
      "remaining": 55,
      "reset": "2026-09-03T00:05:00.000Z"
    },
    "api_key_preview": "krok...ab12"
  },
  "meta": {
    "timestamp": "2026-09-03T00:00:00.000Z"
  }
}
GET/api/v1/statusFree tier

Public health/sync-freshness check — reports whether the arbs/snipes opportunity sync is healthy based on how recently the newest doc was written.

Tier detail: none — the only public, unauthenticated /v1 endpoint

force-dynamic route, no API key check at all. Rate-limited as tier 'free' under identifier 'anonymous' (shared bucket across all anonymous callers). 'degraded' means the newest arbs/snipes row is >5 minutes old. Reads Supabase supabaseMaxUpdatedAt('arbs'/'snipes') (indexed updated_at column) when isSupabase('opportunities'), else falls back to Firestore orderBy('detectedAt','desc').limit(1) — per an in-code historical note, an earlier version ordered by a field ('timestamp') the writer never set, which permanently reported 'degraded' (false alarm); see CLAUDE.md memory 'health-check-must-read-supabase'. Cached 30s via getCached().

Example request
curl \
  "https://krokodds.com.au/api/v1/api/v1/status"
Example response
{
  "success": true,
  "data": {
    "status": "operational",
    "sync": {
      "healthy": true,
      "last_sync_seconds_ago": 42
    },
    "timestamp": "2026-09-03T00:00:00.000Z"
  },
  "meta": {}
}
GET/api/v1/stream/opportunitiesFree tier

Server-Sent Events (SSE) stream of live opportunity deltas (arbs/snipes/middles/low_holds) — initial full snapshot per type, then add/remove/update deltas on a 5s poll, with 15s keepalive heartbeats.

Tier detail: free (streaming is in FREE_TIER_FEATURES)

SSE, not a normal JSON envelope. Hard-caps the connection at 10 minutes wall-clock (MAX_STREAM_MS) then sends a 'bye' event and closes — clients should reconnect via EventSource's built-in retry. limit is fixed per tier (free=100, api=500), not client-adjustable. Uses polling (not Firestore onSnapshot) deliberately: Admin onSnapshot listeners can't be safely torn down across cold-start boundaries in the App Router serverless runtime. Rows older than 12h past commence_time (STALE_CUTOFF_MS) are filtered out of every snapshot/delta. Reads Supabase (supabaseLatestDocs) when isSupabase('opportunities'), else Firestore with server-side orderBy/where.

Query parameters
NameTypeRequiredDefaultDescription
typesstring (comma-separated)Noarbs,snipes,middles,low_holdsSubset of arbs|snipes|middles|low_holds to stream. Invalid/empty result -> 400.
sport_keystringNoFilter events by sport.
min_valuenumberNo0Minimum edge value filter, ignored for the low_holds type (which sorts by holdPct instead).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/stream/opportunities?types=arbs%2Csnipes%2Cmiddles%2Clow_holds"
Example response
{
  "note": "This is an SSE stream (Content-Type: text/event-stream), not a single JSON response. Example frames:",
  "frames": [
    "event: ready\ndata: {\"types\":[\"arbs\",\"snipes\",\"middles\",\"low_holds\"],\"sport_key\":null,\"min_value\":0,\"poll_ms\":5000}\n\n",
    "event: snapshot\ndata: {\"type\":\"arbs\",\"rows\":[{\"id\":\"a1\",\"value\":3.2,\"sport_key\":\"basketball_nba\"}]}\n\n",
    "event: delta\ndata: {\"type\":\"arbs\",\"added\":[],\"removed\":[\"a2\"],\"updated\":[{\"id\":\"a1\",\"value\":3.5}]}\n\n",
    ": keepalive 1735689600000\n\n",
    "event: bye\ndata: {\"reason\":\"max_stream_duration\"}\n\n"
  ]
}
GETPOSTDELETE/api/v1/webhooksFree tier

CRUD for outbound webhook subscriptions: GET lists the caller's webhooks, POST registers a new one (returns a one-time-visible secret), DELETE soft-deletes (deactivates) one by id.

Tier detail: free (webhooks is in FREE_TIER_FEATURES) — but the tier is looked up from the user's own active API key, not from a key presented on this request

This route authenticates with the dashboard's browser session, not an API key – it isn't callable from third-party integrations.

Auth is Firebase session (getServerUser()) via getServerUser — NOT X-API-Key. Tier gate: GET reads tier from ANY active API key belonging to the user (getUserTier -> listUserApiKeys); POST requires the user to have at least one active API key (403 'No active API key' otherwise) and uses that key's tier. Writes are Supabase-only (supabaseUpsert on POST, supabaseMerge soft-delete on DELETE) with a Firestore-read fallback only on GET's list query and on ownership lookup in DELETE. GET response always strips the `secret` field (set to undefined) except at creation time in the POST response, which is the only time the secret is ever returned. Response envelope here does NOT use the standard {success,data,meta} shape on GET (bare {webhooks:[...]}) or POST (bare fields) — inconsistent with the rest of the v1 API.

Body (JSON)
NameTypeRequiredDefaultDescription
url (POST)stringYesDestination HTTPS URL. Passed through checkWebhookUrl() SSRF guard (rejects internal/private-network targets) before being accepted.
events (POST)string[]No["arb","ev","middle"]Event types to subscribe to.
minValue (POST)numberNo0Minimum edge value threshold to trigger delivery.
id (DELETE)stringYesWebhook id to deactivate; 404 if the caller doesn't own it.
Example request
curl \
  # dashboard-only: uses your browser session, not an API key
  -H "Content-Type: application/json" \
  -d '{"url (POST)":"value","events (POST)":"[\"arb\",\"ev\",\"middle\"]","minValue (POST)":"0","id (DELETE)":"abc123"}' \
  "https://krokodds.com.au/api/v1/api/v1/webhooks"
Example response
{
  "GET": {
    "webhooks": [
      {
        "id": "wh_1",
        "userId": "u1",
        "url": "https://example.com/hook",
        "events": [
          "arb",
          "ev",
          "middle"
        ],
        "minValue": 0,
        "active": true,
        "createdAt": "2026-09-01T00:00:00.000Z",
        "deliveryCount": 12,
        "failureCount": 0,
        "secret": null
      }
    ]
  },
  "POST": {
    "id": "wh_2",
    "url": "https://example.com/hook2",
    "events": [
      "arb",
      "ev",
      "middle"
    ],
    "secret": "whsec_ab12...",
    "message": "Webhook registered. Save your secret — it will not be shown again."
  },
  "DELETE": {
    "success": true
  }
}

Additional Data

14 endpoints

Odds history, steam-move detection, player prop stats/results, injuries, weather and other supporting feeds.

GET/api/v1/injuriesFree tier

Latest injury reports across sports/racing, filterable by sport and team. Reads from injury_reports.

NOT an alias of /api/v1/sports/injuries. Both read the same injury_reports table, but sports/injuries adds status_severity classification (out/doubtful/questionable/probable), body_part, and a cross-sport sport_breakdown summary in meta — its doc comment explicitly says it 'extends' this racing/general-purpose endpoint with multi-sport coverage. Treat as two distinct, overlapping endpoints, not a redirect/alias pair. Response cached 600s free / 120s api via unstable_cache.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (lowercased), e.g. "aussierules_afl".
teamstringNoCase-insensitive substring match on team name.
limitintegerNo100Max rows returned. Capped at 100 on free tier, 5000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/injuries?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "afl_2026_c-daniel-rioli",
      "sport_key": "aussierules_afl",
      "player_name": "Daniel Rioli",
      "player_slug": "daniel-rioli",
      "team": "Richmond",
      "status": "Out",
      "reason": "Hamstring strain",
      "date": "2026-09-01",
      "season": 2026,
      "source": "official-team-report",
      "updated_at": "2026-09-02T21:14:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 998,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/odds-historyAPI plan

Per-event opening vs. closing H2H odds with movement direction, deduped across re-keyed event ids.

Requires the paid 'api' tier (feature 'historical'). Two backing shapes depending on data source: primary reads the permanent `odds_history` table (snake_case fields, full bookmaker+opening_odds arrays, response includes them raw); fallback (if odds_history empty/errors) reads the legacy `gameday_odds_history` mirror (camelCase opens/latest/snapshots) then the BigQuery archive. Dedup: same fixture can carry multiple event_ids after upstream re-keying — stale rows are filtered out by default via staleFixtureIds() unless include_duplicates=true.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoFilter by sport key (lowercased).
event_idstringNoFilter to a single event.
include_snapshotsbooleanNofalseSet "true" to include the full time-series snapshot array (legacy gameday_odds_history source only).
include_duplicatesbooleanNofalseSet "true" to include stale rows for fixtures that were re-keyed with a new event_id upstream (flagged duplicate: true). Default is freshest-row-only.
limitintegerNo25Max rows. Capped at 25 on free tier, 1000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-history?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "e8f2a1-nrl-2026-09-04",
      "event_id": "e8f2a1",
      "sport_key": "rugbyleague_nrl",
      "home_team": "Penrith Panthers",
      "away_team": "Melbourne Storm",
      "commence_time": "2026-09-04T09:30:00Z",
      "opens": {
        "home": 1.65,
        "away": 2.25,
        "opened_at_ms": null,
        "home_implied_prob": 0.6061,
        "away_implied_prob": 0.4444
      },
      "latest": {
        "home": 1.58,
        "away": 2.45,
        "home_book": "Sportsbet",
        "away_book": "Sportsbet",
        "updated_at_ms": null,
        "home_implied_prob": 0.6329,
        "away_implied_prob": 0.4082
      },
      "movement": {
        "home_delta": -0.07,
        "away_delta": 0.2,
        "home_direction": "shorten",
        "away_direction": "drift"
      },
      "opening_odds": [],
      "bookmakers": [],
      "snapshot_count": 0,
      "updated_at": "2026-09-03T02:11:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 25,
    "requested_limit": 25,
    "include_snapshots": false,
    "include_duplicates": false,
    "duplicates_found": 0,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 4990,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/odds/movementFree tier

Historical odds movement for one event/market from the append-only odds_archive firehose, with steam-move and line-drive detection.

Reads the `odds_archive` table (per CLAUDE.md: max-fidelity, Supabase-only, append-only, month-partitioned, never pruned). Rows grouped by (market, bookmaker) then by selection; steam_move = >=5% implied-probability shift across snapshots (n>=3), line_drive = >=60% of consecutive moves in the same direction (n>=2). Uses guardV1Request/v1Success helper (newer pattern) rather than the older per-route boilerplate.

Query parameters
NameTypeRequiredDefaultDescription
event_idstringYesEvent identifier. 400 error if missing.
marketstringNoMarket filter, e.g. "h2h", "spreads", "totals".
bookmakerstringNoFilter to a single bookmaker.
fromstring (YYYY-MM-DD)NoStart date filter on captured_at.
tostring (YYYY-MM-DD)NoEnd date filter on captured_at (queried as to+'T23:59:59').
limitintegerNo100Max snapshots per outcome group, clamped 1-500.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds/movement?event_id=evt_abc123"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "e8f2a1",
      "sport_key": "rugbyleague_nrl",
      "home_team": "Penrith Panthers",
      "away_team": "Melbourne Storm",
      "market": "h2h",
      "outcomes": [
        {
          "selection": "Penrith Panthers",
          "snapshots": [
            {
              "timestamp": "2026-09-01T00:00:00Z",
              "odds": 1.65,
              "line": null,
              "implied_prob": 0.6061,
              "juice": null
            },
            {
              "timestamp": "2026-09-03T02:00:00Z",
              "odds": 1.58,
              "line": null,
              "implied_prob": 0.6329,
              "juice": null
            }
          ],
          "steam_move": false,
          "line_drive": true,
          "open_odds": 1.65,
          "close_odds": 1.58,
          "move_pct": -4.24
        }
      ],
      "detected_at": "2026-09-01T00:00:00Z",
      "bookmaker": "sportsbet"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "event_id": "e8f2a1",
    "market": null,
    "bookmaker": null,
    "from": null,
    "to": null,
    "limit": 100,
    "steam_moves_detected": 0,
    "line_drives_detected": 1,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/opportunity-historyAPI plan

Archived arb/+EV opportunities that have since expired (cleaned up after 24h).

Archive-tier endpoint (CREDIT_COST=5, comment: 'archive endpoint (historical opportunities)'). Feature 'historical' gates it to the paid 'api' tier only. Reads Supabase `opportunity_history` collection ordered by archivedAt desc.

Query parameters
NameTypeRequiredDefaultDescription
typestringNo"arb" or "ev" only; any other value returns 400.
sport_keystringNoFilter by sport key (lowercased).
sincestring (ISO date)NoClamped by the key's historicalDays entitlement via clampHistoricalFrom().
limitintegerNo50Max rows. Capped at 50 on free tier, 2000 on api tier (though feature itself is api-only).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunity-history?type=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "arb-2026-09-02-x91k",
      "type": "arb",
      "sport_key": "basketball_nba",
      "event_id": "b7c3d9",
      "event_name": "Boston Celtics vs Denver Nuggets",
      "commence_time": "2026-09-02T00:10:00Z",
      "market_key": "h2h",
      "profit_pct": 2.14,
      "ev_pct": null,
      "stake": 100,
      "legs": [
        {
          "bookmaker": "Ladbrokes",
          "outcome": "Boston Celtics",
          "odds": 2.05
        },
        {
          "bookmaker": "TAB",
          "outcome": "Denver Nuggets",
          "odds": 2.15
        }
      ],
      "archived_at": "2026-09-02T00:12:00.000Z",
      "created_at": "2026-09-01T23:40:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 50,
    "requested_limit": 50,
    "type": null,
    "sport_key": null,
    "since": null,
    "note": "Records cleaned up after 24 hours",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 200,
      "remaining": 4995,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/player-props-resultsFree tier

Settled player-prop outcomes (hit/miss/actual value) per event.

Truth source is `player_props_results.outcomes[]` per repo memory (prop-backtest-data-model). event_id path is a single doc fetch keyed on eventId (slashes replaced with underscores, truncated to 1500 chars); without event_id it's a settled_at-ordered scan.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoPost-filtered in memory (sportKey has no extracted column).
event_idstringNoIf set, does a direct doc lookup by id instead of a scan.
player_namestringNoCase-insensitive partial match, filters within each event's outcomes array.
marketstringNoExact match (lowercased) on outcome market_key.
sincestring (ISO date)NoClamped by clampHistoricalFrom(); filters/orders on the indexed settled_at column.
limitintegerNo50Capped at 50 on free tier, 2000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/player-props-results?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "event_id": "b7c3d9",
      "sport_key": "basketball_nba",
      "event_name": "Boston Celtics vs Denver Nuggets",
      "commence_time": "2026-09-02T00:10:00Z",
      "completed_at": "2026-09-02T02:45:00Z",
      "outcomes": [
        {
          "market_key": "player_points",
          "player_name": "Jayson Tatum",
          "line": 27.5,
          "side": "over",
          "hit": true,
          "actual_value": 31
        }
      ],
      "outcome_count": 1,
      "updated_at": "2026-09-02T03:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": null,
    "event_id": null,
    "player_name": null,
    "market": null,
    "since": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 998,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/player-props-statsFree tier

Aggregate hit/miss/push counts and hit rate per player/market/line/side.

Backed by Supabase collection `player_props_stats` (same table used by /api/v1/players/compare for its per-player prop breakdown). hit_rate = hits / (hits+misses), pushes excluded from denominator.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNo
player_slugstringNoExact match, post-filtered in memory after fetch.
player_canonicalstringNo
market_keystringNo
sidestringNoMust be "over" or "under" or it is ignored.
limitintegerNo50Capped at 50 on free tier, 2000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/player-props-stats?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "player_props_stats-jayson-tatum-player_points-27.5-over",
      "sport_key": "basketball_nba",
      "market_key": "player_points",
      "player_name": "Jayson Tatum",
      "player_slug": "jayson-tatum",
      "canonical_key": "nba_jayson-tatum",
      "line": 27.5,
      "side": "over",
      "hits": 34,
      "misses": 21,
      "pushes": 2,
      "total": 57,
      "decided": 55,
      "hit_rate": 0.6182,
      "last_result_at": "2026-09-02T03:00:00.000Z",
      "updated_at": "2026-09-02T03:05:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "sport_key": null,
    "player_slug": null,
    "player_canonical": null,
    "market_key": null,
    "side": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 998,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/player-props/projectionsFree tier

AFL player projections branded as Krok Odds Projections, sourced from an advanced statistical model feed.

Route comment mandates branding this 'Krok Odds Projections' — never the underlying source name. Backed by Supabase collection `external_fryzigg_player_stats` (AFL only). `confidence` is a derived field = min(1, |Rating|/10), not a true model confidence. Cache TTL fixed at 3600s regardless of tier (not tier-scaled like most other v1 routes).

Query parameters
NameTypeRequiredDefaultDescription
seasonintegerNocurrent yearSeason year filter; 400 if non-numeric.
roundintegerNoRound number filter; 400 if non-numeric.
teamstringNoExact match, case-insensitive, on team.
playerstringNoCase-insensitive partial match on player name.
limitintegerNo100Clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/player-props/projections?season=current%20year"
Example response
{
  "success": true,
  "data": [
    {
      "id": "fryzigg_2026_r23_marcus-bontempelli",
      "player_name": "Marcus Bontempelli",
      "team": "Western Bulldogs",
      "season": 2026,
      "round": 23,
      "game": "WB_vs_GEE_R23",
      "date": "2026-08-30",
      "opposition": "Geelong",
      "kicks": 18,
      "marks": 6,
      "handballs": 12,
      "tackles": 5,
      "goals": 1,
      "behinds": 0,
      "hit_outs": 0,
      "inside_50s": 4,
      "clearances": 7,
      "clangers": 2,
      "rebound_50s": 1,
      "frees_for": 2,
      "frees_against": 1,
      "time_on_ground_pct": 88,
      "rating": 132.4,
      "disposal_rating": 27.1,
      "confidence": 1
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "season": 2026,
    "round": null,
    "team": null,
    "player": null,
    "source": "Krok Odds Projections",
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/players/compareFree tier

Compare 2-5 players' prop hit rates and season stats side by side; adds tennis H2H record and Elo when both players are ATP/WTA.

H2H + Elo enrichment only fires when sport_key contains 'tennis' AND exactly 2 players given; Elo is looked up only for the first player's slug in the current code (second player's Elo (_eloB) is fetched but discarded/unused). Elo source table is `tennis_players`, tried under id `atp_{slug}` then `wta_{slug}`. Uses guardV1Request/v1Success pattern.

Query parameters
NameTypeRequiredDefaultDescription
playersstring (comma-separated slugs)YesMin 2, max 5 slugs/canonical keys; 400 if <2 or >5.
sport_keystringNo
seasonstringNoAccepted but currently unused inside loadPlayerStats (param name is _season).
metricsstring (comma-separated)NoDocumented in the file header comment but not read/implemented in the current GET handler.
limitintegerNo20Max prop rows fetched per player, clamped 1-100.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/players/compare?players=lebron-james"
Example response
{
  "success": true,
  "data": [
    {
      "slug": "novak-djokovic",
      "name": "Novak Djokovic",
      "sport_key": "tennis_atp",
      "props": [
        {
          "market_key": "total_games_over_under",
          "line": 21.5,
          "side": "over",
          "hit_rate": 62.5,
          "sample": 16,
          "hits": 10,
          "misses": 6
        }
      ],
      "season_stats": {
        "wins": 38,
        "losses": 6
      },
      "h2h": {
        "opponent_slug": "carlos-alcaraz",
        "opponent_name": "Carlos Alcaraz",
        "wins": 6,
        "losses": 5,
        "meetings": [
          {
            "date": "2026-07-14",
            "winner": "Carlos Alcaraz",
            "loser": "Novak Djokovic",
            "score": "6-4 6-7 7-6",
            "surface": "grass"
          }
        ],
        "surface_split": {
          "hard": {
            "a": 3,
            "b": 3
          },
          "clay": {
            "a": 2,
            "b": 1
          },
          "grass": {
            "a": 1,
            "b": 1
          }
        },
        "elo": {
          "overall": 2231,
          "hard": 2240,
          "clay": 2180,
          "grass": 2260
        }
      }
    },
    {
      "slug": "carlos-alcaraz",
      "name": "Carlos Alcaraz",
      "sport_key": "tennis_atp",
      "props": [
        {
          "market_key": "total_games_over_under",
          "line": 21.5,
          "side": "over",
          "hit_rate": 58.3,
          "sample": 12,
          "hits": 7,
          "misses": 5
        }
      ],
      "season_stats": {
        "wins": 41,
        "losses": 5
      }
    }
  ],
  "meta": {
    "count": 2,
    "tier": "free",
    "players": [
      "novak-djokovic",
      "carlos-alcaraz"
    ],
    "sport_key": "tennis_atp",
    "season": null,
    "limit": 20,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 20,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/resultsAPI plan

Completed game results (scores, winner) from cold-storage archive.

Archive tier (CREDIT_COST=5, comment: 'archive endpoint (game results / cold storage)'). Empty results are refunded in full via refundApiKeyCredits and logged as HTTP-status 204 in the internal request log (though the actual response status returned is still 200). Rows with an empty resolved sport_key are silently dropped — flagged in-code as an upstream data-quality issue.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoMatched via an OR filter across sport_key / data->>sportKey / data->>sport_key to cover inconsistent writer casing.
sincestring (ISO date)NoClamped by clampHistoricalFrom(); filters commence_time >= since.
limitintegerNo100Capped at 100 on free tier, 5000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/results?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "b7c3d9",
      "game_id": "b7c3d9",
      "sport_key": "basketball_nba",
      "home_team": "Boston Celtics",
      "away_team": "Denver Nuggets",
      "home_score": 112,
      "away_score": 104,
      "winner": "home",
      "commence_time": "2026-09-02T00:10:00Z",
      "completed": true,
      "completed_at": "2026-09-02T02:45:00Z",
      "resolved": true,
      "settled_at": "2026-09-02T02:50:00.000Z",
      "resolution_source": "krok-odds",
      "ingested_at": "2026-09-02T02:46:00.000Z",
      "first_published_at": "2026-09-02T02:46:00.000Z",
      "status": "final"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "api",
    "limit": 100,
    "requested_limit": 100,
    "since": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 4995,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/sport-activityFree tier

Reference list of active/inactive sport keys and groupings (which sports currently have live coverage).

No tierAllowsFeature() call in this route at all — any valid API key on any tier can hit it, it's not gated behind a V1Feature. Reads Supabase `sport_activity_global`. Response uses `private, max-age` Cache-Control (not the 'no-store' pattern most other v1 routes use) with an explicit comment warning against ever making this response public/CDN-cached since it's auth-gated per key.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNoIf set, does a direct single-doc lookup instead of a scan.
groupstringNoCase-insensitive exact match on group, post-filtered in memory.
activebooleanNofalse"true" restricts the scan to active=true rows.
limitintegerNo100Capped at 100 on free tier, 5000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sport-activity?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "key": "aussierules_afl",
      "group": "Australian Rules",
      "title": "AFL",
      "description": "Australian Football League",
      "active": true,
      "has_outrights": true,
      "updated_at": "2026-09-03T01:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "sport_key": null,
    "group": null,
    "active": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/steam-movesFree tier

Recently detected steam moves (sudden multi-book odds shortening/drifting) across sports.

Reads Supabase `steam_moves` collection (distinct from the raw-archive-derived /api/v1/odds/movement, which computes steam detection on the fly from odds_archive). This route consumes pre-computed steam-move docs instead.

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNo
event_idstringNo
directionstringNoMust be "shortening" or "drifting"; anything else is ignored (treated as null).
min_move_pctnumberNo0Filters rows whose movePct is below this threshold.
bookmakerstringNoMatched defensively against bookmaker/book/bookmakers/books fields, none of which are reliably populated per in-code comment.
limitintegerNo50Capped at 50 on free tier, 1000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/steam-moves?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "steam-2026-09-03-nrl-001",
      "event_id": "e8f2a1",
      "event": "Penrith Panthers vs Melbourne Storm",
      "sport_key": "rugbyleague_nrl",
      "sport_title": "NRL",
      "outcome": "Penrith Panthers",
      "direction": "shortening",
      "old_odds": 1.75,
      "new_odds": 1.55,
      "move_pct": -11.43,
      "old_implied_prob": 0.5714,
      "new_implied_prob": 0.6452,
      "implied_prob_delta": 0.0738,
      "bookmaker_count": 6,
      "commence_time": "2026-09-04T09:30:00Z",
      "detected_at": "2026-09-03T03:40:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "direction": null,
    "min_move_pct": null,
    "filters": {
      "sport_key": null,
      "event_id": null,
      "direction": null,
      "min_move_pct": null,
      "bookmaker": null
    },
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 50,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/teams/canonicalFree tier

Canonical team id/slug/full-name mapping used to join team names across bookmaker feeds.

No tierAllowsFeature() gate in this route — reachable by any valid key/tier. Reads Supabase `team_canonical`. `external_id` is only populated when the doc id does NOT contain '__' (ids containing '__' are treated as composite/internal, not an external system id).

Query parameters
NameTypeRequiredDefaultDescription
sport_keystringNo
slugstringNoExact match.
namestringNoCase-insensitive substring match on fullName, post-filtered in memory.
limitintegerNo100Capped at 100 on free tier, 5000 on api tier.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/teams/canonical?sport_key=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "afl__brisbane-lions",
      "sport_key": "aussierules_afl",
      "external_id": null,
      "participant_id": "afl__brisbane-lions",
      "full_name": "Brisbane Lions",
      "slug": "brisbane-lions",
      "updated_at": "2026-08-20T00:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 100,
    "requested_limit": 100,
    "sport_key": null,
    "slug": null,
    "name_contains": null,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "rate_limit": {
      "limit": 100,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/weatherFree tier

Raw observed + forecast weather for upcoming AU/NZ racing meetings.

NOT an alias of /api/v1/sports/weather-impact. This route only returns raw observed/forecast weather embedded on `racing_meetings/{id}` docs (BOM/Open-Meteo via racingWeatherSync), racing-only, per its own file-header comment. /api/v1/sports/weather-impact is a separate, cross-sport route that additionally reads `external_openmeteo_forecasts`, adds heuristic impact_tags/impact_notes (high_wind, heavy_rain, extreme_heat, cold_conditions, high_humidity, storm_risk), and covers AFL/NRL/NFL/MLB/EPL/NCAAF venues too, not just racing. Also uses `getAdminDb()`/Firestore as a fallback path if the Supabase read fails or isSupabase('weather') is false \u2014 the only one of these 13 routes with a live Firestore fallback.

Query parameters
NameTypeRequiredDefaultDescription
race_typestringNoMust be T (thoroughbred), H (harness) or G (greyhound); other values ignored.
venuestringNoCase-insensitive substring match.
statestringNoExact match, uppercased, e.g. NSW/VIC/QLD.
datestring (YYYY-MM-DD)NoExact match; also checked against the key's historicalDays window — 403 if too far in the past.
limitintegerNo50Capped at 50 on free tier, 1000 on api tier.
fieldsstring (comma-separated)NoField-projection allowlist parsed via parseFields()/projectRows() to trim the response shape.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/weather?race_type=value"
Example response
{
  "success": true,
  "data": [
    {
      "id": "randwick-2026-09-06",
      "venue": "Royal Randwick",
      "state": "NSW",
      "race_type": "T",
      "date": "2026-09-06",
      "track_hint": "Good 4",
      "weather_updated_at": "2026-09-03T01:00:00.000Z",
      "observed": {
        "summary": "Partly cloudy",
        "temp_c": 17,
        "rain_24h_mm": 0,
        "wind_kmh": 14,
        "wind_dir": "SE",
        "station": "Sydney Airport",
        "observed_at": "2026-09-03T00:00:00.000Z"
      },
      "forecast": {
        "date": "2026-09-06",
        "min_c": 11,
        "max_c": 19,
        "precis": "Shower or two",
        "rain_chance_pct": 60,
        "rain_range_mm": "1-5",
        "state": "NSW"
      }
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "limit": 50,
    "requested_limit": 50,
    "timestamp": "2026-09-03T04:00:00.000Z",
    "coverage_note": "Racing meetings only (AU/NZ). Stadium/event-level weather not yet ingested.",
    "rate_limit": {
      "limit": 50,
      "remaining": 999,
      "reset": "2026-10-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/match-scheduleFree tier

Season schedule / fixture list for all sports backed by the match_schedule table.

Reads from match_schedule table (Supabase). Covers all sports. season_year and sport are Supabase-level filters; date/team/week/completed are post-filters. Cached 3600s.

Query parameters
NameTypeRequiredDefaultDescription
sportstringNoFilter by sport_key (e.g. "americanfootball_nfl").
datestringNoFilter by match date (YYYY-MM-DD, starts-with match).
teamstringNoFilter by team name (partial match, case-insensitive).
season_yearnumberNoFilter by season year (integer, e.g. 2026).
weeknumberNoFilter by week/round number.
completedstringNo"true" or "false" to filter by completion status.
limitnumberNo100Max results, clamped 1-500 (MAX_LIMIT).
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/match-schedule?sport=aussierules_afl"
Example response
{
  "success": true,
  "data": [
    {
      "id": "sched_afl_2026_r23",
      "sport_key": "aussierules_afl",
      "event_id": "afl_20260906_abc",
      "event_name": "Collingwood vs Carlton",
      "short_name": "COL vs CAR",
      "date": "2026-09-06",
      "season_year": 2026,
      "week": 23,
      "week_text": "Round 23",
      "venue": "MCG",
      "venue_city": "Melbourne",
      "home_team_id": "1",
      "home_team_name": "Collingwood",
      "home_score": null,
      "away_team_id": "2",
      "away_team_name": "Carlton",
      "away_score": null,
      "completed": false,
      "status": "scheduled"
    }
  ],
  "meta": {
    "count": 1,
    "tier": "free",
    "license": "krokodds-derived",
    "filter": {
      "sport": "aussierules_afl",
      "date": null,
      "team": null,
      "season_year": null,
      "week": null,
      "completed": null
    },
    "sport_breakdown": {
      "aussierules_afl": 1
    }
  }
}

MCP Server

Krok Odds ships a Model Context Protocol server (mcp-server/ in the repo) that exposes the most useful data as MCP tools an AI agent can call directly – no manual curl-and-paste. It talks Streamable HTTP and authenticates with a bearer token.

Status: self-hosted, not yet on a public URL. The server queries Supabase directly (not through the public v1 API), so it currently runs with Krok Odds' own database credentials – you run your own instance rather than pointing at a hosted one. A managed, multi-tenant version that authenticates with your regular API key is on the roadmap.

Run it locally

cd mcp-server
npm install
export SUPABASE_URL=...
export SUPABASE_SERVICE_ROLE_KEY=...
export MCP_API_KEY=choose-a-bearer-token
export MCP_PORT=3100   # optional, defaults to 3100
npm run dev             # tsx watch, live-reloads on save

Connect from Cursor

{
  "mcpServers": {
    "krok-odds": {
      "url": "https://mcp.krokodds.com.au/mcp"
    }
  }
}

Add to .cursor/mcp.json or Settings → MCP. Claude Desktop and VS Code need a stdio bridge – see full docs below.

Available tools (9)

ToolDescriptionParameters
get_sportsList sports currently covered, with a count of open +EV opportunities per sport.(none)
get_injuriesCurrent injury reports, optionally filtered by sport.sport?, limit? (default 25, max 100)
get_upcoming_eventsUpcoming events for a sport, with AI tip and consensus data.sport?, limit? (default 20, max 100)
get_event_oddsAll tracked bookmaker odds/markets for an event, matched by team names.event, sport?, limit? (default 30, max 100)
get_value_betsFind positive expected value (+EV) betting opportunities.sport?, min_ev? (default 0), limit? (default 25, max 100)
get_sgm_picksAI-generated same-game multi (SGM) suggestions for upcoming events.sport?, tier? (safe|value|longshot), limit? (default 15, max 50)
get_player_propsPositive EV player prop opportunities.sport?, min_ev? (default 0), limit? (default 25, max 100)
get_racing_picksToday's AI racing win/place predictions.date? (default today), venue?, limit? (default 30, max 100)
get_racing_meetingsToday's race meetings with race cards and runner lists.date? (default today), state?, limit? (default 15, max 50)

Examples

One real call per category – swap in your own key and filters.

Gameday
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/gameday/alt-lines?sport_key=aussierules_afl"
Opportunities
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/opportunities?type=all"
Racing
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/racing/arbs?venue=Randwick"
Odds Feed
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/odds-feed/bookmakers"
Sports
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/sports?category=value"
Tips & Predictions
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/predictions/confidence?type=all"
Tennis
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/tennis/elo?player=lebron-james"
Historical
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/historical/afl?apikey / api_key=value"
Reference
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/reference/headshots?sport_key=aussierules_afl&apikey / api_key=value"
Prediction Markets
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/prediction-markets/sentiment?sport_key=aussierules_afl"
Other Sports
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/boxing/fight-results?X-API-Key=value"
Infrastructure
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/advanced-stats?source=value"
Additional Data
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/api/v1/injuries?sport_key=aussierules_afl"

Rate Limits & Headers

Every successful response carries the full set below so you can build your own backoff/metering without guessing.

HeaderMeaning
X-RateLimit-TierYour key’s tier – free or api
X-RateLimit-LimitPer-request row cap for this tier/endpoint
X-RateLimit-RemainingMonthly credits remaining after this request
X-RateLimit-ResetISO date-time the monthly counter resets
X-Credits-CostCredits debited for this request (0 if refunded for an empty result)
X-Credits-RemainingCredits remaining this month, post-request
X-Cache-AgeEnforced cache TTL in seconds for this response (floors at 300s free / 15s paid)
X-Krok-VersionAPI version – currently "1"
X-Krok-Data-SourceProvenance of the data: proprietary, licensed, or derived
Retry-AfterSeconds to wait before retrying (429 responses only)

Changelog

Recent API-facing changes, most recent first.

  • 2026-09-03Published this OpenAPI 3.1 spec and rebuilt this reference from a full read of every v1 route.
  • 2026-09-02positive-ev: min_odds filter now actually applied (was parsed but silently ignored).
  • 2026-09-0225 more v1 routes migrated from Firestore to Supabase-only reads.
  • 2026-08-29MLB player-prop grading fixed for night games crossing UTC midnight.
  • 2026-08-21SGM void window extended 48h → 7 days; recalibrated Platt scaling on same-game multis.

API FAQ

Auth, rate limits, credits, coverage and historical depth.

How do I authenticate with the Krok Odds API?

Every request carries your API key in the x-api-key header. Keys are issued from the API dashboard after signup and are scoped to a single plan – there is no OAuth flow and no separate token exchange. Keep the key server-side; it is a bearer credential and anyone holding it can spend your credits.

What are the rate limits?

Limits are enforced per key on a rolling window and vary by plan, with the free tier deliberately tight enough to test integration rather than run production traffic. Every response returns the remaining allowance in its headers, so poll those rather than hardcoding a cadence. A 429 means back off and retry with exponential delay.

How does the credit system work?

Requests spend credits rather than being counted as flat calls, because a single racing meeting response is far heavier than one head-to-head market. The free tier ships every live endpoint at 50 credits a day with no credit card, which is enough to verify response shapes against your model. The A$49/month plan covers full production access.

Which sports and leagues does the API cover?

181 sport and league keys, including AFL, NRL, NBA, NFL, MLB, NHL, EPL, A-League, La Liga, Serie A, Bundesliga, Champions League, BBL and international cricket, ATP/WTA tennis, UFC and boxing, Formula 1, golf, darts, esports, plus Australian thoroughbred, harness and greyhound racing. Query the sports endpoint for the live list rather than hardcoding keys.

How many bookmakers are in the odds responses?

140+ Australian bookmaker brands plus Betfair Exchange – every major corporate (Sportsbet, TAB, Ladbrokes, Neds, Bet365, PointsBet, BlueBet, BetRight, Betr, Unibet, Palmerbet, TABtouch, Dabble, BoomBet) and the white-label brands riding the major aggregator platforms. Prices are directly collected rather than resold, so the bookmaker list in a response is the live list, not a marketing figure.

How far back does the historical odds data go?

The odds archive is append-only and runs from January 2024, with 232K+ historical records and one row per bookmaker price per capture cycle rather than a single consensus line. That matters for backtesting: a consensus-only archive cannot reconstruct which book was the outlier, which is exactly the question an arbitrage or +EV backtest is asking.

Does the API include Australian racing?

Yes, and it is the main thing international odds feeds do not carry. Racing endpoints cover thoroughbred, harness and greyhound meetings across every Australian jurisdiction – fixed win and place across all books, Betfair Exchange back/lay and SP, market movers, scratchings, form, sectionals and runner metadata.

What is the difference between the odds endpoints and the opportunity endpoints?

The odds endpoints return raw prices and leave the analysis to you. The opportunity endpoints – arbitrage, positive EV, middles and low-holds – return the results of our own scanners over the same data, already devigged against a blended sharp baseline. Use the former if you have your own model; use the latter if you want our signals as JSON instead of as a dashboard.

API Reference – Australian Odds API Docs, Endpoints & Auth | Krok Odds