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:
| Parameter | Type | Description | Example |
|---|---|---|---|
league | nba, wnba, nfl, mlb or nhl | Only that league's events | nfl |
scheduled_date | date, YYYY-MM-DD | Only the games on that day, read in America/New_York | 2026-10-25 |
participant | string | Only events with a participant whose key is this (participants[].key), case-insensitively | okc |
include_settled | boolean | Also draw from the settled archive: past-dated and cancelled games (see below) | true |
include_submarkets | boolean | Every matched submarket instead of the full-game moneyline | true |
include_rules | boolean, default false | Each game line also lists its rule_comparisons rows (see game-line rules) | true |
player_prop_match | strict or same_prop | Player-prop admission policy | same_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:
| Field | Value |
|---|---|
scheduled_date | The game's calendar day in America/New_York, YYYY-MM-DD. Always present. |
scheduled_start | The 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:
| Parameter | Type | Description | Example |
|---|---|---|---|
event_id | string (repeatable) | Look up canonical event key(s) | nba-okc-sas-2026-10-20 |
source_id | string (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_settled | boolean | Also search the settled archive: past-dated and cancelled games | true |
include_submarkets | boolean, default true | Every matched submarket (false for the moneyline alone) | false |
include_rules | boolean, default false | Each game line also lists its rule_comparisons rows (see game-line rules) | true |
player_prop_match | strict or same_prop | Player-prop admission policy | same_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=cryptoOnly 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-arcadeThe 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=sportsprovider 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=okcThe 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-20Look 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 slugProphetX 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 tokenA 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:L19766755Return all matched events
GET /v1/matching-markets/sportsNo 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=trueinclude_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=trueA 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/sports | GET /v1/matching-markets/sports/lookup | |
|---|---|---|
| Use it to | Browse or filter the schedule | Find the events behind identifiers you hold |
| Selects with | league, scheduled_date, participant | event_id and source_id (at least one, 100 unique at most) |
| Response | data, pagination, snapshot | data, lookups, snapshot |
| Pages | Keyset cursor, default limit=25, max 100 | None: one bounded response |
| Submarkets | The full-game moneyline unless include_submarkets=true | Every 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:
| Request | JSON | Gzipped |
|---|---|---|
| Default (full-game moneylines) | 241 KB | 58 KB |
include_submarkets=true | 2.2 MB | 454 KB |
include_submarkets=true&player_prop_match=same_prop | 10.2 MB | 1.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.