PredictorSDK
Guides

Filtering

Narrow the list of matched sports events by league, date or participant, and look up specific matches by identifier.

Two routes read the matched sports events. GET /v1/matching-markets/sports lists them, soonest scheduled start first (a game stays listed through the day after its scheduled date, so the first events can be games that have already been played; see Include settled events), and takes the filters below. GET /v1/matching-markets/sports/lookup finds the events that hold identifiers you already have and reports what each identifier found. The unified market catalog supports a provider filter and a canonical top-level category filter.

Filtering the list

Called without parameters, GET /v1/matching-markets/sports returns the first page of currently matched events. Narrow it with these parameters:

ParameterTypeDescriptionExample
leaguenba, wnba, nfl, mlb or nhlOnly that league's eventsnfl
scheduled_datedate, YYYY-MM-DDOnly the games on that day, read in America/New_York2026-10-25
participantstringOnly events with a participant whose key is this (participants[].key), case-insensitivelyokc
include_settledbooleanAlso draw from the settled archive: past-dated and cancelled games (see below)true
include_submarketsbooleanEvery matched submarket instead of the full-game moneylinetrue
include_rulesboolean, default falseEach game line also lists its rule_comparisons rows (see game-line rules)true
player_prop_matchstrict or same_propPlayer-prop admission policysame_prop

league, scheduled_date and participant are membership filters on the same matched set. They compose, so ?league=nfl&scheduled_date=2026-10-25 is the intersection, and pagination.total counts the filtered set rather than every event. Each is named for the field it selects on in the response. Only the five leagues above are accepted (case-insensitive), a scheduled_date must be a real calendar date in that form, and any other value is a 400 that quotes it: an ignored filter would return a full page that looks filtered, so none is ever ignored. An empty value (league=) is a 400 too; omit the parameter to ask for everything.

Participant keys are unique only within a league: la is the Rams in the NFL and the Sparks in the WNBA. Pair participant with league unless you want both. A key is the lower-case code in participants[].key (okc, sas, no for the Saints), the same one source_markets[] outcomes refer to.

Cursors are bound to the filters that issued them. limit and cursor work as described in Pagination, but a next_cursor replayed with a different league, scheduled_date, participant, include_settled, include_submarkets or player_prop_match is a 400; start again from the first page when you change a filter. include_rules is the exception: it shapes each event and never selects one, so a cursor continues with or without it.

For canonical player props, include_submarkets=true uses strict settlement equivalence by default. Add player_prop_match=same_prop to include verified same-prop identities with different or unverified rules. This policy works with every filter on this page, list pagination, and include_settled; it does not change game-line matching. See the options and rule matrix.

When a game is scheduled

Every event, in the list and in a lookup, says when it is scheduled:

FieldValue
scheduled_dateThe game's calendar day in America/New_York, YYYY-MM-DD. Always present.
scheduled_startThe start in UTC at millisecond precision (2026-10-21T00:30:00.000Z) when a venue published an exact start time, otherwise null.

A game that starts at 8:30 PM Eastern on the 20th is on the 20th although its scheduled_start falls on the 21st in UTC; scheduled_date is the day to filter, group and display by. The day is the one the default list judges the game by: the day of its start in Eastern time, and never earlier than the date its event_id ends in. A postponed game takes its new day.

scheduled_start is null when no venue published an exact time. Kalshi's tickers carry a date and no time, a venue's "time to be announced" placeholder is not a start, and games matched before start times were recorded read null too (archived games for up to 30 days after that change). null means "no exact start is known", never a guess at one, and scheduled_date still names the day. When venues disagree, the earliest exact start is published. Both fields are schedule identity, not a status: they do not say whether a game has started, ended or been postponed.

Looking events up by identifier

GET /v1/matching-markets/sports/lookup takes identifiers instead of filters, and needs at least one:

ParameterTypeDescriptionExample
event_idstring (repeatable)Look up canonical event key(s)nba-okc-sas-2026-10-20
source_idstring (repeatable)Look up a venue market's market_id, market_slug or event_id (or a Polymarket, Predict or AlphaArcade outcome token, or a Polymarket condition ID), as {provider}:{id}kalshi:KXNBAGAME-26OCT20OKCSAS
include_settledbooleanAlso search the settled archive: past-dated and cancelled gamestrue
include_submarketsboolean, default trueEvery matched submarket (false for the moneyline alone)false
include_rulesboolean, default falseEach game line also lists its rule_comparisons rows (see game-line rules)true
player_prop_matchstrict or same_propPlayer-prop admission policysame_prop

The lookup answers { data, lookups, snapshot }: the events found, each once, what each identifier found, and the snapshot's age. It is unpaginated, so it takes neither limit nor cursor, nor the list filters, and sending one is a 400 that lists what the route accepts. The identifier parameters were part of the list route until 2026-10-09; sent there now, event_id and source_id are a 400 that names the lookup route.

source_id takes any market_id, market_slug or event_id that a source_markets[] entry publishes, prefixed with that entry's provider. That covers Kalshi event or market tickers, Polymarket market IDs, slugs and event slugs, Predict market IDs, SX Bet market hashes and L… fixture IDs, AlphaArcade ULIDs and slugs, ProphetX event IDs and <event>:<market> IDs (a player prop's names its line, <event>:<market>:<line>), Pred parent IDs and <parent>:<child> pairs, Limitless market slugs, and Hyperliquid outcome IDs. On Polymarket, Predict, AlphaArcade and Hyperliquid an outcome token (outcomes[].outcome_id) works too (send a Hyperliquid token's leading # as %23), so a trader holding only a token can find the game, and on Polymarket so does a market's condition ID, the condition_id that wallet positions carry. The other venues' outcome IDs name a side within one market: Kalshi's, Pred's and Limitless's yes/no and SX Bet's outcomeOne/outcomeTwo are a 400 that points you to the market's market_id, and ProphetX's small integers are not lookup identifiers. Only the text before the first colon is the provider, so an ID that itself contains colons needs no escaping. The provider must be one of kalshi, polymarket, predict, sxbet, alpha-arcade, prophetx, pred or limitless. Anything else returns 400, and so does a bare ID with no provider, because bare IDs are not unique across venues. Every 400 quotes the value it rejected, so you can find it in a batch.

Matching is case-insensitive for both parameters: event_id=NBA-OKC-SAS-2026-10-20 finds the same game as the canonical lower-case key, and a Kalshi ticker typed in lower case finds the same game as the venue's own upper-case spelling.

Repeat a parameter for several values. A comma-joined value (source_id=kalshi:<id-1>,kalshi:<id-2>) and an empty one (source_id=) are both 400s, because no identifier contains a comma and an empty value would otherwise answer an unrelated result. event_id and source_id combine, across any number of venues, up to 100 unique identifiers per request, and a request with neither is a 400 too. The lookup returns all requested matches in one bounded, unpaginated response.

Market categories

Use GET /v1/categories to list the canonical category values, then pass one to GET /v1/markets:

GET /v1/markets?category=sports
GET /v1/markets?category=crypto

Only the canonical category values are accepted (case-insensitive). Any other value returns 400, so use the list from GET /v1/categories rather than provider-native tags like mlb or btc.

The category filter is part of pagination scope, so keep the same category value when following a next_cursor.

Market providers

GET /v1/markets walks providers in a fixed order — kalshi, polymarket, predict, sxbet, hyperliquid, alpha-arcade, prophetx, limitless, pred — so without a filter the first pages are all Kalshi, and reaching a later provider means paginating through every earlier one. Pass provider instead:

GET /v1/markets?provider=sxbet
GET /v1/markets?provider=alpha-arcade

The value is each row's own provider field, so it is the same token Sources lists as that platform's source key. Only those nine values are accepted (case-insensitive). Any other value returns 400 listing the legal ones — including spellings the provider override on GET /v1/markets/{market_id} tolerates, such as sx_bet, hl, or px:

{
  "code": "invalid_parameter",
  "message": "invalid provider \"sx_bet\": must be one of kalshi, polymarket, predict, sxbet, hyperliquid, alpha-arcade, prophetx, limitless, pred",
  "status_code": 400,
  "param": "provider"
}

pagination.total counts only the selected provider's rows, not the whole catalog, and the filter is part of pagination scope — keep the same provider value when following a next_cursor.

snapshot.observed_at narrows with it. Unfiltered it is when the oldest provider crawl behind the page began — a floor for the whole catalog — while under provider it is when that provider's own crawl began. Filtering is therefore the cheapest way to get an exact freshness bound instead of a conservative one; see Pagination.

Both filters compose, and the result is the intersection:

GET /v1/markets?provider=polymarket&category=sports

provider on GET /v1/markets filters a list. The provider parameter on GET /v1/markets/{market_id} and GET /v1/events/{event_id} has the same name and the same values but another job: it names the provider a single identifier should be resolved against (it was platform until 2026-10-09), and it also tolerates a few spellings, such as sx_bet, that the list filter rejects.

Examples

Filter the list

GET /v1/matching-markets/sports?league=nfl&scheduled_date=2026-10-25
GET /v1/matching-markets/sports?league=nba&participant=okc

The second request is the Thunder's games. pagination.total is the number of events that match, and the cursor continues that filtered list.

Look up a canonical event directly

Use this route when you already hold the stable event key; there is no need to page through the list to rediscover it.

GET /v1/matching-markets/sports/lookup?event_id=nba-okc-sas-2026-10-20

Look up by a venue's own ID

Prefix the ID with its provider. A source market's market_id, market_slug or event_id works. These are the source_id values of GET /v1/matching-markets/sports/lookup:

?source_id=kalshi:KXNBAGAME-26OCT20OKCSAS        # Kalshi event ticker
?source_id=kalshi:KXNBAGAME-26OCT20OKCSAS-OKC    # Kalshi market ticker
?source_id=sxbet:L19766755                       # SX Bet fixture
?source_id=polymarket:nfl-pit-no-2026-10-25      # Polymarket slug

ProphetX lists games only about a week ahead, and AlphaArcade and Pred IDs are long, so list current matches and copy their IDs from a source market: a ProphetX market_id (an <event_id>:<market_id> pair, with the line appended for a player prop) or its event_id, an AlphaArcade market_id ULID, or a Pred market_id (a <parent_market_id>:<child_market_id> pair of 0x-hex IDs) or its event_id (the bare parent), or a Limitless market_id, which is its slug and its event_id as well. Any venue ID leaves the matching snapshot when the venue stops listing it: after the game for most markets, and sometimes while the game is in play for player props.

?source_id=prophetx:YOUR_EVENT_ID
?source_id=prophetx:YOUR_EVENT_ID:YOUR_MARKET_ID
?source_id=alpha-arcade:YOUR_MARKET_ULID
?source_id=pred:YOUR_PARENT_ID:YOUR_CHILD_ID
?source_id=pred:YOUR_PARENT_ID
?source_id=limitless:<market_slug>
?source_id=polymarket:YOUR_CLOB_TOKEN_ID          # an outcome token

A lookup finds the game an identifier belongs to and returns it with every matched submarket, so the spread, total or prop market you looked up is in it. Add include_submarkets=false for the moneyline alone.

Look up several events at once

Repeat either parameter, and mix them and the venues freely:

GET /v1/matching-markets/sports/lookup?event_id=nba-okc-sas-2026-10-20&source_id=kalshi:KXNBAGAME-26OCT20BOSDET&source_id=sxbet:L19766755

Return all matched events

GET /v1/matching-markets/sports

No parameters required. Returns the first page of currently matched sports events with its cross-platform identifiers and schedule. See Pagination for how to traverse larger result sets.

Include settled events

?include_settled=true

include_settled picks which events the request draws from, and it applies to the lookup route as well as to the list.

By default you get only events whose scheduled start has not certainly passed — today's games, plus a one-day grace so a late start that runs past midnight Eastern is never dropped while it is still being played. Set include_settled=true to also get the settled archive: events that left the live matching run, usually because the venues settled or delisted them, kept for 30 days. That adds games whose date is further in the past, including ones a venue still lists as open, and games a venue cancelled, which can be future-dated (see below).

The date comes from the game, not from the venue: its scheduled_date, which is its scheduled start's day and never earlier than the date its event_id ends in. scheduled_date=2026-10-01 therefore matches nothing in the default population once that day is past, and matches that day's games with include_settled=true. Venues settle and delist a game's markets at different times, so an event that is missing from the default page has not necessarily been resolved upstream — it just is not current. A postponed game takes its new date and stays current until the makeup game. If you are holding a canonical event_id for a game that has already been played, ask for it with include_settled=true; without the flag the lookup answers 200 and reports your identifier in lookups with no events.

A game that has started is served with the fullest mapping it had: the version from before its first venue settled and delisted it, rather than only the venues still listing it. That is the mapping a settlement reconciliation needs. It holds in the default population, so today's finished games stay listed with every venue until they are past-dated, and with include_settled=true it holds for older games too.

Games archived before 2026-10-04 21:26 UTC are the exception. They kept only the venues that still listed them when they left, usually two, so a lookup by another venue's identifier finds nothing for them. They age out of the 30-day archive by 2026-11-04.

A game that was cancelled and never played is not one of today's games. Polymarket settles its moneyline 50-50 when a game is cancelled with no make-up, and the default population then leaves the game out, in the list and in lookups. include_settled=true still returns it, with the Polymarket and Predict markets that were matched to it, so a position held on one of them can still be reconciled. The game leaves the default list within about ten minutes of Polymarket settling it. Polymarket's settlement is the only signal used, so a cancelled game that never had a Polymarket market stays listed until its date passes, and so does a cancelled NFL game, because a tied NFL game settles 50-50 as well.

This works alongside pagination and the filters above, but include_settled is part of cursor scope, so changing it mid-traversal requires starting over from page 1.

Check how old the answer is

Matching runs continuously in the background, and this endpoint reads the stored result. Every response — from the list or the lookup, including a lookup that matched nothing — carries snapshot.observed_at: when the oldest venue read behind that result began.

{
  "data": [],
  "lookups": { "nba-okc-sas-2026-10-20": { "event_ids": [] } },
  "snapshot": { "observed_at": "2026-09-19T21:14:07.412Z" }
}

Each venue is re-read about every 30 seconds plus the time the read takes, and a venue's last good read is kept for at most 10 minutes after it finished. So while matching is healthy, observed_at is never more than about 12 minutes old, and normally under two minutes. An older value means the snapshot has stopped updating. In that case an event the response does not contain is unknown, not unmatched — a game listed after the snapshot froze cannot be in it. Bound the age before acting on an absence:

from datetime import datetime, timedelta, timezone

from predictorsdk import PredictorSDK

client = PredictorSDK(token="YOUR_API_KEY")
response = client.lookup_sports_matching_markets(event_id="nba-okc-sas-2026-10-20")

observed = response.snapshot.observed_at
if observed is None or datetime.now(timezone.utc) - observed > timedelta(minutes=15):
    raise RuntimeError("matching snapshot is stale; treat a missing event as unknown")

A recent observed_at says the snapshot is updating; it does not promise every venue is in it. A venue whose reads keep failing is dropped from the snapshot once its last good read is 10 minutes old, so its events can be missing even from a current one.

observed_at is null when the age is unknown, for example while a snapshot written before this field existed is still being served. Pages of one cursor traversal can come from successive snapshots, so the value can advance between pages; settled events added by include_settled=true do not affect it.

See every submarket

GET /v1/matching-markets/sports?include_submarkets=true

A list page covers events whose full-game moneyline is matched, and each carries only that submarket, so submarkets is never empty and a page stays small. include_submarkets=true adds every other matched submarket in the same shape, and events matched only on those submarkets. A lookup returns every submarket already; include_submarkets=false narrows it to the moneyline. Each submarket carries its normalized market type, segment, signed spread or positive total/prop line, settlement rules where material, canonical outcomes, and exact provider-native market/outcome references. It is deliberately identity-only: it does not add prices, status, liquidity, fees, or order-book depth, and an event's schedule is only the scheduled_date and scheduled_start described above. Join the returned exact IDs to market detail or a venue client for those values.

What a lookup found

data lists each matched event once, soonest scheduled start first, on both routes. A lookup adds lookups, which reports every event_id and source_id you sent with the events it found:

"lookups": {
  "kalshi:KXNBAGAME-26OCT20OKCSAS": { "event_ids": ["nba-okc-sas-2026-10-20"] },
  "sxbet:L19766755": { "event_ids": ["nba-okc-sas-2026-10-20"] },
  "kalshi:KXNBA-27-SAS": { "event_ids": [] }
}

Each identifier is reported as you sent it, with whitespace around the provider and ID trimmed. Matching ignores case, but the key keeps your spelling, so you can index the result by your own strings. Two identifiers of one game list the same event, which data holds once. An identifier that found nothing lists no events: it is unknown, the market is matched by no other venue, the game is past-dated (see include_settled), the game's only matches are player props that the player_prop_match policy excludes (send player_prop_match=same_prop), or, under include_submarkets=false, the game's full-game moneyline is not matched.

Do not confuse the canonical event_id with source_markets[].event_id. The canonical key is PredictorSDK's stable cross-venue ID, which the lookup's event_id takes. The source-market field is native to one venue and names that venue's parent event, for GET /v1/events/{event_id}; to look it up here, prefix it with its provider and send it as source_id. A venue ID sent as event_id (a Kalshi ticker, a numeric ID, an L… fixture, an 0x hash) is a 400 that says so, and so is one that matches no canonical event but is a venue's identifier. That is most often a Polymarket slug, which can differ from the canonical key: a night game's slug carries the UTC date (nfl-sea-den-2026-10-16 for the canonical nfl-sea-den-2026-10-15), and Polymarket spells some teams differently. The 400 names the provider to send it under. See Which identifier goes where.

List and lookup

The two routes differ in what they answer:

GET /v1/matching-markets/sportsGET /v1/matching-markets/sports/lookup
Use it toBrowse or filter the scheduleFind the events behind identifiers you hold
Selects withleague, scheduled_date, participantevent_id and source_id (at least one, 100 unique at most)
Responsedata, pagination, snapshotdata, lookups, snapshot
PagesKeyset cursor, default limit=25, max 100None: one bounded response
SubmarketsThe full-game moneyline unless include_submarkets=trueEvery matched submarket unless include_submarkets=false

limit, cursor, league, scheduled_date, participant, include_settled, include_submarkets, include_rules and player_prop_match each take one value; sending one twice is a 400, as is a value they do not accept, and every such 400 quotes the value. Both routes include the snapshot block described in Check how old the answer is, and both are read with the same markets.matching.read permission and counted as one request each.

Response size

Responses are gzip-compressed when the request allows it (every SDK does). A limit=100 page measured on 2026-10-04:

RequestJSONGzipped
Default (full-game moneylines)241 KB58 KB
include_submarkets=true2.2 MB454 KB
include_submarkets=true&player_prop_match=same_prop10.2 MB1.2 MB

Player props make the last row heavy on an NFL weekend. Use a smaller limit, or look up the events you need, if that size matters to you.

The game-line rule rows (include_rules=true) are heavy too: on 2026-10-09 a 100-event page with every submarket reached about 11 MB when every game line carried them, about 60% of it those rows, which is why they are opt-in. Ask for them on a lookup or a small page, where you want the evidence.

On this page