Base URL
Authentication
Every sports endpoint on this page is public and requires no credentials. Valid API-key or session credentials are accepted when supplied, but they are optional. Rate limits./sports/tournament-bracket is 10 requests/minute (the heavy group) and /sports/game-markets is 60/minute (the sports group). Every other route on this page has no explicit decorator and falls under the service-wide default of 100/minute. Anonymous limits are keyed by client IP; authenticated requests can use the credential identity. Group defaults are adjustable at runtime by an operator.
Live events
public, max-age=10, s-maxage=10.
Predict.fun fixtures are included only when the provider indexing gate lists predictfun as enabled. The gate is re-checked on every request (including cache hits). If the gate lookup fails, Predict.fun is excluded (fail closed) — Polymarket/Kalshi live rows still return.
Request
sportFamily, the canonical category id from /sports/catalog
(for example, cricket). The existing sport field remains the source league code
(for example, cricbbl).
Falls back to a live provider lookup when the per-game market cache is cold, and, when no matched entry carries a Kalshi side, synthesizes one from a team-name lookup.
Response
The response shape is described by the field table below rather than by a JSON example.
Every event row also carries
gameId, sport, sportFamily, homeTeam, awayTeam, homeScore, awayScore, status, period, elapsed, live, ended, slug, matchingSlug, and marketsCount.
events is ordered by urgencyScore descending, then by ts descending.
Soccer and NFL fixture timing
GET /txodds/fixtures/{fixture_id}/timing returns timing evidence for NFL
regulation and second-half EPL / senior World Cup fixtures. The
OpenAPI reference defines the response fields.
usableForLateGame indicates whether the observation supports its named
lateGameBasis; it does not indicate that a game is already late. Soccer’s
periodRemainingSeconds describes the nominal half clock, excluding unknown
added time. NFL also exposes regulationRemainingSeconds.
gameRemainingSeconds remains null: neither predicts the final whistle.
Stale, missing, or unsupported evidence returns usableForLateGame=false with
an unavailableReason. Consumers must check observation age and verify the
fixture’s market mapping and settlement scope before using timing.
Timing and late-game strategies
Each event includes atiming object. elapsed remains an opaque, nullable
upstream display string. Do not subtract it from a regulation duration or use
it as seconds elapsed. A value such as 05:12 does not by itself establish
clock direction, period length, or whether the clock is running.
Explicit suspended/delayed/cancelled/final statuses take precedence over the
broad
live flag. Unrecognized statuses stay unknown. FT OT and F/OT
indicate completion after overtime, not active overtime. No countdown is
extrapolated during breaks, suspension, or silence. urgencyScore remains a
candidate-ranking heuristic, not permission to trigger a strategy.
ts records when the game state was observed. Depending on the available
feed data, it can represent the source observation or the time Kairos received
it. Freshness therefore measures the age of our observation, not a guarantee that the upstream score is current.
The response can also be cached; neither reading it again nor receiving a
WebSocket heartbeat proves that a particular game’s clock has advanced.
Predict.fun and cricket schedule rows are inferred from scheduled start/end
windows, not a live scoreboard. Their legacy live field can be true, but
timing.status is unknown, stale is true, and unavailableReason is
no_live_score_feed.
The upstream sports documentation
lists periods and lifecycle statuses, but does not establish the complete
sport-specific elapsed contract needed to enable these remaining-time fields.
Gotcha:
matchingMarkets entries are backfilled to {"providers": []} for any event that resolved a matching slug but has no cross-provider data. A present key does not imply a match was found — check providers. When the Predict.fun gate is off, predictfun entries are stripped from every providers list.Event markets
game_id or by slug (which is first resolved to a gameId).
Auth: none — public. Rate limit: 100/minute. Cache-Control: public, max-age=10, s-maxage=10.
Request
* At least one should be supplied — both are optional, unvalidated strings at the request layer.
200 with {"gameId": "<value or \"unknown\">", "markets": []} when nothing resolves — it never 404s.
Response
Array of normalized markets, same shape asevents[].primaryMarket on /sports/live-events (id, question, conditionId, tokenId, outcomePrices, outcomes, volumeNum, liquidityNum, acceptingOrders, sportsMarketType, provider, …).
Game markets
public, max-age=30.
Request
Supply exactly one identifier. Sending neither or both returns
400 {"error":"exactly one of game_id or slug is required"}. Use the game-level event slug, not an individual market ID. A valid identifier that does not resolve returns 200: team and sport metadata are null, and sections with no matched supplemental markets are empty.
Response
Gotcha: using the YES position ID for a
no leg buys the opposite outcome. Always pick the position ID that matches legs[].side.comboEligible and both position-ID fields can be absent when the combo catalog is unavailable. Absence means eligibility is unknown, not false. Supplemental venue legs explicitly return comboEligible: false because atomic combos — single positions built from several legs — are Polymarket-only.
Supplemental venue enrichment is best-effort: unavailable supplemental data does not remove the Polymarket catalog.
Gotcha: an open market without a usable quote remains in the response with
price: 0. Treat that as unavailable pricing, not an executable 0% price.Matching markets
public, max-age=30, s-maxage=60, or max-age=3 / max-age=5 when live=true.
Request
Gotcha:
live is compared as a raw string, not parsed as a boolean — anything other than "true" is treated as false. live=1 and live=TRUE do nothing.live=true, a correction is applied if a stale Kalshi price appears to be on the wrong side of a binary flip.
Response
Returns
{} if slugs parses to no non-empty entries.
Trending matched
public, max-age=30, s-maxage=60.
Request
volume_1h falling back to total), and sorts descending.
Response
Returns
{"matches": [], "count": 0} immediately if the matching cache is empty.
Upcoming events
startingEvents: fixtures with a known kickoff in the
past ten minutes, retained while a live score may still be pending. These carry
awaitingLive: true; they are not confirmed live games and contain no scores.
Scheduled Polymarket fixtures with a known kickoff carry startingEligible: true
so a cached schedule can make the same transition. Consumers should replace a
starting fixture when its venue market ID appears in the live feed, and expire it
after ten minutes.
Not-yet-live sports events within a lookahead window, with matched cross-provider markets attached.
Auth: none — public. Rate limit: 100/minute. Cache-Control: public, max-age=30, s-maxage=60, or public, max-age=10, s-maxage=30 when a stale cached body is served while it refreshes in the background.
Request
category/league/series supplied, the query covers every league in the sports catalog, including UEFA Europa League (uel).
Served from a shared cache: the window is rebuilt in the background about once a minute. The default windows (windowHours=720&limitPerSeries=200 and windowHours=168&limitPerSeries=12, every league) are kept warm and a copy is never served more than two hours old; any other window is served for at most ten minutes and kept warm only while it is being requested. The Age header gives the seconds since it was built. Only a request with no copy at all waits for a build — returning 502 {"detail": "All upstream fetches failed"} if every upstream fetch fails, or 504 if the build runs out of time. A partial upstream failure still returns 200 with whatever resolved.
Loading a few days at a time: startFrom/startTo cut one slice of the window by startTime, so a schedule can fetch its next days as it scrolls. Adjacent slices never overlap. startingEvents appear only in the slice that covers the current time, and matchingMarkets holds only the entries the returned events reference.
Response
The response shape is described by the field table below rather than by a JSON example.
Every event row also carries
eventId, title, sport, sportFamily, league, teamA, teamB, image, primaryMarket, marketsCount, and eventSlug. sportFamily is the canonical category id from /sports/catalog and is equal to sport on this route. events is sorted by startTime ascending.
Returns 200 with {"events": [], "startingEvents": [], "matchingMarkets": {}} when the filters resolve to no leagues (for example series=,,), and on a genuine no-data result.
Metadata
public, max-age=3600, s-maxage=3600.
Request
No parameters.Age header gives the seconds since the body was built.
Response
Catalog
league values the other routes accept.
Auth: none — public. Rate limit: 100/minute. Cache-Control: public, max-age=3600, s-maxage=3600.
Request
No parameters.Response
Gotcha:
tagId is a hardcoded constant (100639) shared by every league — it is not league-specific, so it cannot be used to tell leagues apart.seriesId is resolved via a cached lookup and may be null if unresolved.
The soccer category includes UEFA Europa League as { "slug": "uel", "label": "UEFA Europa League" }.
Kalshi live games
public, max-age=240.
Request
No parameters.Response
Kalshi filters
public, max-age=21600 (6 hours, in-process and Redis).
Request
No parameters.Response
The upstream Kalshi payload is passed through verbatim, with no normalization:filters_by_sports, or a non-200 from upstream, is treated as a fetch failure and surfaces as a 500 on a cold cache.
Futures
public, max-age=60, s-maxage=300.
Request
With no parameters the whole list is returned, as before.
Response
outcomes is sorted favorites-first with unpriced contenders last, and is capped per event; events are returned richest-first by contender count. Per-game props and game lines are filtered out, and the unrecognised-sport bucket is additionally capped.
Tournament bracket
public, max-age=60, stale-while-revalidate=3600.
Request
league returns 200, not 404 — the body is {"leftRounds": [], "rightRounds": [], "final": null, "winner": null}. An unrecognised format is not rejected either; anything other than left-to-right renders the symmetric layout, and only the literal symmetric selects two-legged tie merging.
format=left-to-right returns all rounds in leftRounds with rightRounds empty. format=groups-then-knockout adds a groups array with standings tables. thirdPlace is present only for football-data competitions that have a third-place fixture.
league and format are both part of the cache key, so a novel pair pays a full synchronous build.
Response
Errors
Errors are returned as{ "detail": "<message>" }, with two exceptions: 429 uses an error key, and 422 uses FastAPI’s standard validation body. The Code column below holds the exact message string the service returns; — means the response carries no fixed string.
No route on this page returns 404
Valid but unresolvable input degrades to a200. Do not branch on 404 — branch on the empty body:
/sports/event-markets→{"gameId": "<value or \"unknown\">", "markets": []}/sports/matching-markets→{}when none of the requested slugs have a cross-venue match/sports/trending-matched→{"matches": [], "count": 0}/sports/upcoming-events→{"events": [], "startingEvents": [], "matchingMarkets": {}}/sports/tournament-bracket→{"leftRounds": [], "rightRounds": [], "final": null, "winner": null}for an unknown league/sports/game-markets→ the catalog withnullteam/sport metadata and empty sections when the Polymarket event is missing
/sports/game-markets and /sports/upcoming-events, the Predict.fun gate on /sports/live-events, combo-eligibility annotation, and background refreshes all fail open and degrade the payload rather than erroring.
GET /sports/poly-kalshi-pairings and GET /sports/combo-markets are documented on the matched markets page.
