GET /events and GET /events/details
reference pages. It assumes no prior knowledge of our data model. If you read
one page before integrating, read this one — every section below documents
something that has tripped up an integration in practice.
The five-minute summary. Events contain markets, and markets are what you
trade. Sports events return their markets grouped by betting line — that
grouping is the intended display structure, not an obstacle. Non-sports events
return a flat list. Separately, some text fields arrive with unrendered
placeholders you must substitute yourself. Handle those two things and the
rest of the API is conventional.
1. The object model
Three levels, from broadest to narrowest:
You place orders against a market
id. Events and groups exist only to
organise markets for display.
2. Sports events group their markets by betting line
A sports fixture has dozens of markets, and they are not independent questions — they belong to betting lines. “Moneyline 3-Way” is one line with three selectable outcomes; “Puck Line” is another with two. The API returns them already grouped that way, and that grouping is the intended display structure: render each group as one block with its outcomes side by side, the way a sportsbook does. Non-sports events have no betting lines, so their markets arrive as a flat list. Each element ofevent.markets is therefore either a group object or a
market object:
null
marketDisplayName are collected into a group; a market whose
marketDisplayName is null is emitted on its own. In practice that means
sports fixtures come back as groups and everything else comes back flat — but a
single event can contain both forms, so handle both.
To confirm with real proportions: a sample of 10 live fixtures on the dev
environment returned 39 groups, 97 markets inside them, and 0 bare markets.
Rendering it
Walk the array and branch onoutcomes — group entries become a titled block,
bare entries a single row:
- Grouping is keyed purely on the display name, so a lone market with a
non-null
marketDisplayNamestill arrives wrapped — a group of one. In the sample above, 4 of the 39 groups had a single outcome. Your group component must render sensibly with one child. outcomesorder is not guaranteed. For over/under and handicap lines, sort bymetaData.opticOdds.points; otherwise sort bytitlefor a stable display.
When you do want them flat
Some jobs are genuinely per-market — resolving a position, totalling exposure, looking up an order’s market. For those, flatten; just don’t flatten for display, or you throw away the sports structure your users expect:3. Rendering titles, rules and links
event.title, event.subtitle and event.rulesSummary arrive fully rendered. Display them as
they are. There are no placeholders to substitute to get a working integration.
Two things are worth knowing:
- §3a — one request header puts the closing time on your own viewer’s clock.
- §3b —
$TOKEN$placeholders in rule and contract copy turn data-source names into links.
3a. Showing the closing time in your viewer’s zone
By default a title names the closing time in US Eastern, because that is where our market sits:/events,
/events/details, portfolio, bookmarks, orders, positions and trade history — so the same market
reads the same time everywhere in your app.
Omit the header and nothing changes. You get US Eastern, exactly as before this existed.
An unrecognised zone is ignored rather than rejected, so a bad value degrades to Eastern instead
of failing the request.
Which values you can send
Any zone name from the IANA time zone database. There are 418, and every one of them is tested — if the zone you need is not in the table below, send it anyway and it will work. United States & Canada
United Kingdom & Europe
Middle East & Africa
Asia
Australia & New Zealand
Latin America
Two things worth knowing about the zone label:
- It follows the event’s own closing date, not today’s. The same template reads
EDTfor a July close andESTfor a January close. You do not have to handle that. - Only US and Canadian zones get letter abbreviations. Everywhere else the label is numeric, so
London reads
GMT+1rather thanBSTand Tokyo readsGMT+9rather thanJST. That is the standard US-English rendering and it is unambiguous, just less pretty.
America/Vancouver reads PDT all year because British Columbia stopped observing daylight
saving. We follow the time zone database, so changes like this reach you without an API change.What moves and what does not
A date that names when trading stops follows your zone, together with the time — they describe one instant:
A date that names which period the market is about does not move, because that would change
the question:
The second market settles on October’s figure. It closes at 22:59 UTC on 31 October, so a viewer in
Tokyo is already into November when it closes — but it is still October’s number being asked about.
The same applies to
week ending … copy.
metaData.period — which period a market is about
Present on the cadences named after a period (monthly, quarterly, yearly, season,
annual_release, per_meeting) and absent on the rest. Format follows the cadence: 2026-09,
2026-Q3, 2026.
Read it whenever you need to group, label or sort by period. It is already the answer — formatting
it is all that is left to do, and it is identical for every viewer in the world.
3b. $TOKEN$ tokens in rule and contract text
Rule copy references data sources through uppercase tokens:
metaData.links, keyed by the token
name without the dollar signs:
$BINANCE$ becomes a link to that URL with that label. Tokens appear in
event.rulesSummary, in every event.metaData.contractSpec.* section, and in
each market’s metaData.underlying.
BINANCE, NWS_CLI, USGS_EQ and NWS_TORNADO, but
this list grows as data sources are added — resolve dynamically from
links, never from a hard-coded map. If a token has no entry, render the bare
token name rather than dropping the sentence.
Also: underlying contains real \n\n paragraph breaks. Render them as
paragraphs; collapsing whitespace produces an unreadable wall of text.
Rule and contract text names a fixed timezone on purpose — do not re-render
it. “settles at 4:00 p.m. ET on Oct 16” describes the exchange’s own clock,
and it is that instant for everyone whatever zone they read it in. The
user-timezone header moves title and subtitle only; rulesSummary,
contractSpec.* and each market’s metaData.underlying are settlement terms,
and shifting their stated times would misstate the contract.3c. Building your own copy instead
If you need different wording or another language,metaData.rulebook_variables
exposes the values that were substituted into underlying:
4. Two families of metadata
metaData is a union with no discriminator field. Probe for a key:
Never assume a key exists, and ignore keys you do not recognise — new ones are
added as new market types launch.
Strike fields depend on strikeType
On templated markets, read metaData.strikeType first, then the field it
points at:
The other strike fields may be absent or present-but-stale.
strikeType is
the authority.
5. Categories and the subcategory tree
Every event carries acategory and a subcategory. The subcategory is the
leaf of an up-to-two-level tree — a subcategory can itself sit inside
another subcategory, exposed as parent.
category
sits above the tree and is a separate field — it is not part of the parent
chain.
Most events are not nested: parent is null, the subcategory is already
top-level, and the breadcrumb is simply Category › Subcategory. Both forms
occur in the same listing, so handle both:
parentId is null. In current
data the tree is never deeper than two levels, but walking parent costs
nothing and will not break if a third level is ever added.
Building the navigation
Three endpoints feed a category menu, and they return different projections:/categories/hierarchy is the efficient choice for a menu, with two caveats: its
nested subcategories are a reduced six-field shape, and that list is flat
— parent/child nesting is not expressed, so it alone cannot render
Metals › Gold. For that, use /subcategories.
Also note these endpoints validate categoryId, page and limit as
integers, whereas GET /events wants strings of digits. Same concepts,
different encodings.
Cadence buckets in the navigation
GET /subcategories?withCadence=true powers a “by cadence” menu. It prepends
virtual entries to the same subcategories array:
isVirtual: true means the entry is synthesized, not a row in the
subcategories table — the server groups the events table by
metaData.cadence on each request and shapes the result to look like a
subcategory so it can sit in the same menu.
Four things to handle:
- Test
isVirtualto separate buckets from real subcategories. Buckets have noidand cannot be passed assubcategoryIdanywhere — to filter events by cadence useGET /events?cadence=. countis currently-live events, not all events at that cadence. The15minbucket reads6while thousands of 15-minute events exist, because only six windows are open at once. Top-level bucket counts also honourcategoryIdonly — they ignoresubCategoryIdandisActive.pagination.countcounts only the real rows, so it is smaller thansubcategories.length. The virtual entries are also repeated on every page — don’t accumulate them while paging.- Every bucket is a valid filter value. The menu and
GET /events?cadence=are seeded from the same canonical set, so a filter link can be built straight from any bucket.
subcategoryId. The filter includes descendants:
requesting subcategoryId=81 (Metals) returns events whose subcategory is Gold
or Silver, not just events assigned to Metals directly. Passing a leaf id
(82 = Gold) returns only that leaf.
So you can render a “Metals” tab with one call — no need to fan out over child
ids and merge client-side.
The expansion is one level deep: the server collects the id you passed plus
its direct children. Since the tree is never deeper than two levels today, that
covers everything — but it would not reach grandchildren if a third level were
ever added.
6. Recurring events: grouping rounds by cadence
Short-cadence markets — 5-minute and 15-minute crypto, hourly ranges, daily settlements — are published as a stream of separate events, one per period. Each round is its own event with its own id, its own markets and its own order book. There is no single “BTC 15-minute market” object to subscribe to. A real sequence looks like this:
To build a “rounds” strip — past, live and upcoming periods for one asset — the
grouping key is
subcategoryId + cadence.
Step 1 — find the subcategoryId
Cadence filtering is almost always scoped to one underlying, and you address that
by numeric id. Fetch the list once and cache it:
This endpoint returns
data.subcategories (plus a success flag) — a
different envelope from /events, which returns data.events. Don’t assume a
shared response shape across modules.Step 2 — query by cadence
cadence is an ordinary query parameter. The canonical set is 5min,
10min, 15min, 1hr, daily, weekly, monthly, quarterly, yearly,
season, per_meeting, annual_release and election; anything else returns
400.
A cadence describes a recurring window. One-off markets — a sports fixture,
an IPO announcement, a company’s KPI report — carry no metaData.cadence and
cannot be reached through this filter. The absence is deliberate: treat such an
event as a one-off rather than inferring a cadence from its dates.
cadence composes correctly with categoryId and subcategoryId. All
filters are applied together, so
?categoryId=34&subcategoryId=67&cadence=5min&status=open returns exactly the
live 5-minute BTC round — verified against a running instance. If you have seen
advice to omit cadence and filter client-side because “it is ignored when
subcategoryId is present”, that is not the current behaviour; passing it
server-side is both correct and cheaper than over-fetching.status=open with isFuture left at its default of false, and it is a single
event. Upcoming rounds are also status: "open" but isFuture: true, so
status alone does not separate them — you must pass isFuture. Because these
calls are scoped to a crypto subcategory, isFuture here means the pre-reveal
round; isFutureTradable=true is the same query under a clearer name.
Future rounds have not revealed their target yet, which is visible in the data:
their titles read BTC 15 min · $TBD target until they open.
Reading cadence off an event you already have
Our own web client readsevent.metaData.cadence and nothing else as the
primary source — that is the canonical field. But it defends against three
real-world variations, and you should too if you accept events from more than
one endpoint:
As a last resort the cadence can be recovered from
metaData.templateId, which
ends with it — BTC_15min → 15min, BNB_1hr_range → (no suffix; falls
through). Our client does exactly this:
templateId is opaque and not every template
encodes its cadence in the name.
Where cadence actually lives
Cadence is a property of the event, not of the category or subcategory. It is
event.metaData.cadence, and the ?cadence= filter is a containment test
against that JSON — there is no cadence field on a subcategory, so you cannot ask
“which cadences does Bitcoin offer?” without enumerating its events.
The two are orthogonal dimensions, which is why grouping needs both:
subcategoryId= which underlying (Bitcoin, Gold, EUR/USD)cadence= how often it repeats (5min, 15min, 1hr, daily, …)
Why all three parameters are needed
subcategoryIdscopes to one asset. Crypto subcategories are per-asset, so this filters server-side — no ticker-prefix matching needed.cadenceseparates overlapping series. The same asset runs 5-minute, 15-minute and hourly series at once. Drop it and you get them interleaved.isFuturesplits revealed from unrevealed. By default one call returns only one kind, so make two requests, as above — or sendisFuture=allto get both in a single listing.isFutureTradableis the same filter under a name that cannot be confused with a sports outright — prefer it in new code.
7. Prices
All prices are integer cents, 1–99 — the same scale you submit orders on.63 means 63¢, which reads as roughly a 63% implied probability.
priceDelta is the change against lastDayPrice and is legitimately negative.
totalVolume and volume are different fields from different systems:
volume is the live figure that sortBy=volume and minVolume operate on and
is always present; totalVolume is a persisted column that is frequently
null. Use volume.
8. Status, results and settlement
Checkstatus per market, not per event — individual markets can be halted
while the event stays open. open is the only status that accepts orders.
Market statuses: pending, open, halted, closed, settled, expired,
terminated. Events add live (sports fixture in progress).
result is null until the market resolves, then YES or NO.
Two settlement fields deserve explicit warnings:
metaData.earlySettlementdescribes conditions under which a market can settle before expiry. It is informational. Settlement is performed server-side; do not evaluate these conditions to predict a payout. Itsconditionsobject is keyed bystrikeType— look up the entry matching this market’s ownstrikeType, since the others describe sibling markets.metaData.settlementis internal worker bookkeeping. Astatusof"failed"here does not mean the market failed to settle — it records one unsuccessful price-capture attempt. Usestatusandresultas the authority on whether a market is resolved, andsettlementPricefor the value.
9. Which endpoint returns what
There are seven events endpoints and no two return the same projection. Pick by what you need to render, not by what reads best in a URL.
Three traps in that table:
/events/search also omits marketDisplayName on its markets, so the grouping
rule from section 2 never fires there: its markets is always flat. The
m.outcomes ?? [m] flatten remains safe everywhere.
GET /events vs GET /events/details in detail
The two endpoints return the same kind of object, but not the same fields.
This is the difference most likely to bite you: you build a list screen against
/events, then reuse the same model on a detail screen and find fields
missing — or vice versa.
Both wrap the event in data.events[]. The list adds pagination; the detail
endpoint does not, and always returns exactly one element.
Practical consequence: if you generate a shared type from one endpoint, mark
every field in the table above as optional, or model the two responses
separately. The market grouping rule, all metadata, and all placeholder
behaviour are identical across both — only this field set differs.
10. Query parameters
Two behaviours to know before you build filters: Unknown parameters are rejected. The query string is strictly validated — anything not in the documented list returns400 rather than being ignored. A
typo fails loudly, and you cannot pass your own tracking parameters through.
isFuture defaults to false. It means two different things depending on
the category the call is scoped to: under Sports it selects season-long
outrights (Championship Winner, MVP), and everywhere else it selects
pre-reveal rounds — published ahead of time, tradable before their window, with
the target revealed at open. Both are hidden unless you ask for them. An unscoped
call with isFuture=true returns both sets.
isFutureTradable is the unambiguous name for the rounds, whatever the call
is scoped to, and it wins when both params are sent. Prefer it in new code. Each
event and market carries both booleans in the response, so you never have to
infer which meaning applied. Pair either with sortBy=openAt to list upcoming
rounds in order.
status takes a list, and isFuture takes all. To read every event a
player could see in one request, instead of one per status plus one for future
events:
400. Events from every listed status come back interleaved in one paginated
set, each with its own status and isFuture, so there is nothing to merge or
de-duplicate. When the list includes live, results are ordered live first
and then by openAt, and sortBy / sortOrder are ignored.
page and limit are strings of digits ("2", not 2). Drive pagination
from pagination.hasNextPage, not by comparing count to limit — the latter
breaks on the final page.
Finally, GET /events/details takes a capital-I Id. Lowercase id is
rejected as an unknown parameter.
11. A worked example
Rendering one event end to end:lines[].markets carries the id you submit orders
against, its own status to gate the buy button, and its own nullable prices to
display.