Krok Odds

Infrastructure API endpoints

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

10 of 15 endpoints · all categories · get a free API key

GET/api/v1/advanced-statsFree tier

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

Tier detail: free (advanced_stats is in the free-tier feature set; both free and api tier keys can call it)

Built on the shared the standard v1 route factory factory. Cache TTL: free=3600s, api=1800s. Falls back to cold archive mirror only if the primary-store loader throws AND a mirror exists; the soccer advanced stats source has no cold-archive mirror, so it reads from the primary store. Empty result set returns HTTP 200 + `data: []` and refunds the credit.

Query parameters
NameTypeRequiredDefaultDescription
sourcestringYes–One 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 a 400 bad-request error if not in this list.
seasonintegerNo–Season/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 | squadstringNo–Per-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/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 the free-tier feature set)

Requires a valid API key with the bet_tracking feature (via the standard auth guard) PLUS a dashboard-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` the primary store table directly (no secondary 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_keystringNo–Filter bets by sport.
statusstringNo–Filter by bet status: pending | won | lost | void.
fromstring (YYYY-MM-DD)No–Start date filter (created_at >= from).
tostring (YYYY-MM-DD)No–End date filter (created_at <= to 23:59:59).
limitintegerNo50Max results, clamped 1-200.
offsetintegerNo0Pagination offset.
Body (JSON)
NameTypeRequiredDefaultDescription
event_idstringYes–Event identifier.
sport_keystringYes–Sport key.
marketstringNo""Market type, e.g. h2h, spread, total.
selectionstringNo–What was backed.
oddsnumberNo–Decimal odds.
stakenumberNo–Stake amount (AUD). potential_return is auto-computed as odds*stake when both are present.
statusstringNopendingpending | won | lost | void.
commence_timestring (ISO)No–Event 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/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": 500,
      "total_return": 740,
      "net_profit": 240,
      "roi_pct": 48,
      "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

Catalog of the AU-facing bookmakers KrokOdds supports — id, display name, region, supported flag, aggregator/exchange flags and bookmaker_type (fixed, tote, exchange or group). Group labels add a brands array naming the individual books inside the group. Prediction-market venues (Kalshi, Polymarket) are catalogued separately under the Prediction Markets group.

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

The catalog is complete in every response: the roster is never truncated, so meta.truncated is always false and there is no limit parameter — region and provider only narrow what was asked for. Every row carries id, name, region, supported, is_aggregator, is_exchange and bookmaker_type; rows with bookmaker_type "group" additionally carry brands (the individual books inside the group), and those group rows are supported:false. The response exposes only these public roster fields — the internal provider and keying fields used to build the catalog are not returned to callers. 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 (a CDN keys on URL only and ignores X-API-Key). X-Krok-Version: 2 (higher than most other v1 routes, which are version 1). Prediction-market venues (Kalshi, Polymarket) are not bookmakers, so they are excluded from this catalog — see the Prediction Markets group for their contract and resolution feeds. Example captured 2026-10-22 with an api-tier key, so meta.tier and rate_limit.limit reflect api limits (free tier reports limit 100).

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).
providerstringNo–Narrows the catalog to books whose pricing provider matches, plus books shared by 'both'. The provider value itself is not part of the public row shape.
include_aggregatorsboolean ("true"/"false" string)NofalseWhen true, also lists the aggregator feeds (deduped clone lines, no arb value) as additional rows marked supported:false — 28 rows instead of 21 on 2026-10-22.
fieldsstring (comma-separated)No–Field projection — returns only the named fields of each row.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/api/v1/bookmakers?region=au"
Example response
{
  "success": true,
  "data": [
    {
      "id": "sportsbet",
      "name": "Sportsbet",
      "region": "au",
      "supported": true,
      "is_aggregator": false,
      "is_exchange": false,
      "bookmaker_type": "fixed"
    },
    {
      "id": "tab",
      "name": "TAB",
      "region": "au",
      "supported": true,
      "is_aggregator": false,
      "is_exchange": false,
      "bookmaker_type": "tote"
    },
    {
      "id": "betfair_ex_au",
      "name": "Betfair",
      "region": "au",
      "supported": true,
      "is_aggregator": false,
      "is_exchange": true,
      "bookmaker_type": "exchange"
    }
  ],
  "meta": {
    "count": 21,
    "truncated": false,
    "tier": "api",
    "region": "au",
    "provider": "all",
    "include_aggregators": false,
    "timestamp": "2026-10-22T04:10:50.862Z",
    "rate_limit": {
      "limit": 500,
      "remaining": 439828,
      "reset": "2026-11-01T00:00:00.000Z"
    }
  }
}
GET/api/v1/closing-linesAPI plan

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: api (paid) — 'closing_lines' is NOT in the free-tier feature set

Hand-rolled route (not the standard v1 route factory) with its own three-link fallback chain: primary `odds_history` table (has opening_odds), then the pre-computed closing-line analysis (pre-computed, 30K+ rows), then the legacy `gameday_odds_history` shape. The sources use different field casing (commence_time vs commenceTime) which the route normalizes. Cache 150s free / 60s api. Distinct from the single-event ladder at /api/v1/odds-history?event_id=. A failed link is never served as a quiet board: each source whose read threw is named in `meta.failed_sources`; a partial failure serves the rows it has and flags itself `degraded`; every link in the chain failing (nothing readable at all) answers 503 + `Retry-After: 30` with `Cache-Control: no-store` instead of an empty 200, and a degraded read is never written to the cache, so it is retried on the next request rather than held for the TTL.

Query parameters
NameTypeRequiredDefaultDescription
sport_key | sportstringNo–Filter by sport.
sincestring (date/ISO)No–Clamped by clampHistoricalFrom() against the key's historicalDays window.
untilstring (date/ISO)No–Upper 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)No–Keyset cursor encoding {v: commence_time ISO, id}.
fieldsstring (comma-separated)No–Field projection.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/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}API plan

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

Tier detail: api (paid) — 'closing_lines' is NOT in the free-tier feature set, the same gate as /api/v1/odds-feed/clv

API-key authenticated (X-API-Key header, apikey/api_key query param) on the 'closing_lines' feature with the credit cost 5 — the same gate as /api/v1/odds-feed/clv and /api/v1/closing-lines. It USED to call dashboard-session auth() (dashboard session cookie), so every API key got 401: measured 2026-10-15 in the documented-route sweep, this was the only route that 401'd on a valid key. Reads the primary store `clv_archive` table by doc id (a doc read); 404 if the event isn't archived, 400 if eventId is empty after trim. There is no secondary fallback in the current code.

Path parameters
NameTypeRequiredDefaultDescription
eventIdstring (path param)Yes–Dynamic route segment [eventId]. Accepts the bare hex event id (e467621918b702d29d87d82e2c464628) and the sport-key-prefixed form (basketball_nbl_e467621918b702d29d87d82e2c464628); a known <sport_key>_ prefix is stripped before the clv_archive lookup, so both forms resolve the same document. 400 if empty after trim.
Example request
curl \
  -H "X-API-Key: YOUR_KEY" \
  "https://krokodds.com.au/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 the free-tier feature set)

Hand-rolled route with a THREE-tier fallback chain: the primary store (ev_hit_rates table) -> point-get on the secondary store the hit-rate store if category set -> cold archive mirror -> full full secondary-store 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
categorystringNo–Market 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/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 pricing suite multiplexed by ?source=: settled racing BSP, race results, sports match-odds, scorer props, and live pre-jump snapshots.

Tier detail: free (the exchange feature is in the free-tier feature set — the exchange filter in api-tier-gate.ts is now a no-op, exchange pricing is available on all tiers)

Built on the standard v1 route factory, primary store only (no cold archive). Cache TTL 600s free / 300s api. Underlying writers are the exchange-feed writers. Per the engineering notes, this exchange endpoint is distinct from the max-fidelity the odds archive/the racing odds archive firehose tables (migration 056) which are internal-only, not exposed via this API.

Query parameters
NameTypeRequiredDefaultDescription
sourcestringYes–One of: bsp, results, match_odds, scorer_props, snapshots. 400 if not in this list.
raceKeystringNo–Racing filter for bsp/results, keyed `${venueSlug}_${raceNumber}`.
venueSlug | marketTypestringNo–Additional bsp filters (in-memory, post-fetch).
sportstringNo–Equality filter (indexed path) for match_odds/scorer_props/snapshots sources.
eventIdstringNo–In-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/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/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 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 the free-tier feature set, 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 /api/v1/odds. `meta.provider` field is @deprecated in favor of per-event `source`. Cache 120s 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 | sportstringNo–Sport filter (used when event_id absent).
event_idstringNo–Point-read a single event across both tiers instead of a windowed scan.
marketstringNo–Post-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/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/statusFree tier

Public health/sync-freshness check — reports whether the odds-sync writer is alive (its own heartbeat) plus the newest-row age per racing table.

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 odds-sync WRITER has not stamped its heartbeat for 15 minutes: the probe reads stats/global_stats.data.lastUpdated, which /api/odds/sync rewrites every cycle (measured 478 fires/24 h, worst gap 6.2 min, so 15 min is 2.4× the worst observed gap). If the heartbeat cannot be read the route falls back to the newest arbs/snipes row with a 35-minute threshold — those are OUTCOME tables that only move when an arb changes (median gap 15 min over 72 h), so a 5-minute threshold reported 'degraded' on a healthy platform about two-thirds of the time. 'data_freshness' reports the newest-row age per racing table (odds-store / the race-card store / tote); each probe is bounded (24× that table's threshold, which keeps the partitioned odds-archive probe index-ordered instead of sorting every partition) and individually guarded: a failing probe reports status 'error' with a normalised code in the error field ('timeout' or 'query_failed'), never 'unavailable' (a probe we could not read is not a table with nothing in it), and it never takes the route down. A table inside its own threshold reads 'green'; one older than its threshold reads 'stale', keeping the real age in last_write_age_seconds; one that has never been written reads 'unavailable'. The three sync probes (the writer heartbeat, then the arbs/snipes outcome fallback) are guarded the same way: when none of them answers, data.status is 'unknown' and data.sync carries probe_failed: true with last_sync_seconds_ago null, instead of a 500 or a 'degraded' claim about a writer that could not be read. status is therefore one of 'operational', 'degraded' or 'unknown'. A failure to build the payload at all answers 503 with Retry-After, not 500. Cached 30s via getCached().

Example request
curl \
  "https://krokodds.com.au/api/v1/status"
Example response
{
  "success": true,
  "data": {
    "status": "operational",
    "sync": {
      "healthy": true,
      "last_sync_seconds_ago": 42,
      "probe_failed": false
    },
    "data_freshness": {
      "as_of": "2026-10-15T03:50:20.000Z",
      "tables": {
        "racing_odds": {
          "last_write_age_seconds": 51,
          "status": "green"
        },
        "racing_cards": {
          "last_write_age_seconds": 6,
          "status": "green"
        },
        "tote": {
          "last_write_age_seconds": 209,
          "status": "green"
        }
      }
    },
    "timestamp": "2026-10-15T03:50:20.000Z"
  },
  "meta": {
    "truncated": false
  }
}
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 the free-tier feature set)

SSE, not a normal JSON envelope. Hard-caps the connection at 10 minutes wall-clock (the 10-minute stream cap) 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 the secondary store’s live listeners) 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 (COMMENCE_CUTOFF_MS) are filtered out of every snapshot/delta, as are rows whose `price_observed_at` (price-read time) is older than 90 minutes, rows with no price-read stamp at all, or rows whose quoted odds are out of sane bounds (the price-freshness check/the odds sanity check — the same gate the /api/v1/opportunities/* REST routes apply). Reads the primary store (a primary-store doc read) when the primary-store flag, else the secondary store with server-side orderBy/where. A read that fails is never served as an empty board: if every requested type fails its first read the stream answers 503 + Retry-After (meta.failed_sources names them) instead of a 200 that streams nothing; a partial failure streams the healthy types and names the missing ones in a 'degraded' frame, with no empty snapshot frame for them; a mid-stream read failure emits 'degraded' rather than a delta that removes everything, and 'recovered' follows when the reads succeed again.

Query parameters
NameTypeRequiredDefaultDescription
typesstring (comma-separated)Noarbs,snipes,middles,low_holdsSubset of arbs|snipes|middles|low_holds to stream. Invalid/empty result -> 400.
sport_keystringNo–Filter 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/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",
    "event: degraded\ndata: {\"failed_sources\":[\"snipes\"],\"meta\":{\"degraded\":true,\"failed_sources\":[\"snipes\"]}}\n\n",
    "event: recovered\ndata: {\"failed_sources\":[],\"meta\":{}}\n\n",
    ": keepalive 1735689600000\n\n",
    "event: bye\ndata: {\"reason\":\"max_stream_duration\"}\n\n"
  ]
}

Other categories

Gameday8 endpoints

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

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

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

Odds Feed16 endpoints

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

Sports8 endpoints

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

Tips & Predictions4 endpoints

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

Insights1 endpoint

Ranked leaderboards built on top of the stat feeds — per-sport player leaders for a chosen stat, season and view.

Tennis5 endpoints

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

Historical7 endpoints

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

Reference7 endpoints

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

Other Sports22 endpoints

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

Additional Data19 endpoints

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

Prediction Markets3 endpoints

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