Markets
Events and outcomes
An event is a question. An outcome is a contract you can actually buy, addressed by an opaque ctr_ id. Quotes are always against an outcome, never against an event.
The partner catalogue
/api/v1/eventsAPI keymarkets:read./api/v1/events/{id}API key{id} is the event id or its slug. markets:read./api/v1/categoriesAPI key?category= accepts, with a live event count for each. markets:read./api/v1/images/{id}No auth{id} is the event id. No credentials: it is an <img> in a browser./api/events and /api/markets are the public surface, not your contract
Those routes power Parity's own site. They page by offset, they are not narrowed to what you are offered, and the contract feed carries upstream identifiers and source metadata in its payload. Build against /api/v1: it is keyset-paged, scoped to your operator, and every contract is named by an opaque ctr_… id that identifies no venue.
The model
| Object | What it is | Example |
|---|---|---|
| Event | One question, grouped across sources. What a player browses. | “Fed decision in September?” |
| Outcome | One binary contract belonging to that event, addressed by a ctr_ id. What is traded. | “No change” |
| Side | YES or NO on one outcome. Both sides of every outcome are tradeable. | YES on “No change” |
Swipe the table sideways for more columns.
A binary event has one outcome and the question is answered by which side you take. A multi-answer race has one outcome per answer, each with its own YES and NO. Every contract settles at exactly $1.00 or exactly $0.00: a market that resolved 99% certain did not resolve.
Event kinds
| kind | Meaning | How to render it |
|---|---|---|
| binary | One outcome. | YES / NO on the question itself. |
| mutually_exclusive | A field of answers, exactly one of which is true. | The leaders, with a “+N more”. Probabilities across the field are comparable. |
| cumulative_ladder | Nested thresholds where one rung implies the next. Every contract on the event points the same way. Check outcome.direction. | Never the two LIKELIEST rungs: on a nested set those are the two loosest, 86.5% and 76.5%, nearly the same statement. Preview from the rung priced nearest 50%, which is the one the market has not decided, and take at most one more from the same direction. Order the full set by threshold, and take the count from outcomeCount. |
| ambiguous | The shape could not be established. That includes a set of thresholds that points BOTH ways, e.g. “dip to $X” beside “reach $Y” under one question. | Present the leaders, never as a complete picture. Where the outcomes carry a threshold, group by outcome.direction first: two rows pointing opposite ways are two questions, and rendering them as one range is wrong whatever their strikes are. |
Swipe the table sideways for more columns.
Listing events
| Parameter | Type | Notes |
|---|---|---|
| category | string | One canonical category, from GET /api/v1/categories, which includes sports whenever it has events. An unknown value is 400 invalid_category, not a silently unfiltered catalogue. |
| sport | string | Narrows inside sports: a sport slug such as nfl or soccer; a family slug covers every competition under it. An unknown value is 400 invalid_sport, and the message lists the sports your offering makes available. |
| league | string | Narrows inside sport: a league slug such as epl. |
| search | string | Free text over the question. |
| sort | string | trending (default), volume, closing-soon, new. volume orders by contracts traded, not by cash |
| dir | string | asc or desc. |
| limit | integer | Default 25, 1 to 100. Outside that range is 400 invalid_limit. |
| locale | string | Opt-in translation: en, es, pt or hr. Anything else, or nothing, is English. See Language below. |
| tradable | boolean | true restricts to events with at least one executable side. |
| closesAfter / closesBefore | ISO-8601 | Window on the close time. An event past its close is never listed, except a sports fixture that is being played. |
| cursor | string | Opaque. Echo nextCursor from the previous page. |
Swipe the table sideways for more columns.
curl -s "$PARITY_BASE/api/v1/events?category=economy&sort=volume&limit=10" \
-H "Authorization: Bearer $PARITY_KEY"{
"data": [
{
"id": "evt_…",
"slug": "fed-decision-september",
"title": "Fed decision in September?",
"subtitle": null,
"category": "economy",
"categories": ["economy"], // every shelf it sits on; always contains category
"sport": null, // sport slug on a sports event
"league": null,
"eventType": null, // "game" | "future" on a sports event
"liveMatch": null, // score and period of a fixture in play, with observedAt
"imageUrl": "/api/v1/images/evt_…", // a Parity path, not an upstream CDN
"kind": "mutually_exclusive",
"outcomeCount": 5,
"impliedLevel": null,
"impliedLevelLabel": null,
"closesAt": "2026-09-16T00:00:00.000Z",
"opensAt": "2026-06-01T00:00:00.000Z",
"updatedAt": "2026-08-20T04:43:08.322Z",
"volume": 8102934.11, // CONTRACTS traded at the venue. Not dollars
"volume24h": 415244.02, // same count, last 24h
"volumeContracts": 8102934.11, // identical values, unambiguously named
"volume24hContracts": 415244.02,
"status": "open",
"outcomes": [ /* see below */ ]
}
],
"pagination": { "limit": 25, "total": 812, "nextCursor": "…", "hasMore": true }
}Volume is a count of contracts, not a dollar amount
volume and volume24h are the venue’s count of contracts (shares) traded. The upstream figure is a quantity. It is not notional, and there is no conversion: measured across live markets the count runs between 1.8× and 9.3× the cash actually exchanged, moving with the price of the contract. A share of a 10¢ contract and a share of a 90¢ one count alike and cost nine times apart.
So do not render these with a currency symbol. volumeContracts and volume24hContracts carry the identical values under a name that states the unit. Prefer them in new code. The original names keep their original meaning, because integrations already read them.
There is deliberately no USD volume field. The market surface publishes no notional, and deriving one from a lifetime count and today’s price produces a figure that moves without a trade behind it. A missing field beats a wrong dollar.
The figure is also not your flow. It is all trading on the market surface, not what you or Parity routed.
Fields that appear only when they apply
trendingon an event, on a page ranked by activity: ascorefrom 0 to 1, the namedcomponentsbehind it, one or two plainreasons, and thefreshnessof the trade tape it was measured from. Absent everywhere else.jurisdictionat the top level of the list, only for an operator whose jurisdiction mode isenforced:{ "enforced": true, "message": "…" }. This endpoint names no customer, so there is no single catalogue to return anddatamay be empty. That is the same answer a quote would give, not a fault.
The cursor contract
- Paging is keyset, not offset. Events arrive while you page, and an offset would let one that appeared between two requests push an older event across the boundary where you never see it. That is a missing row, reported as a successful page.
- A cursor is checked against the ordering it was minted under. Replaying a
sort=volumetoken againstsort=newis400 invalid_cursorrather than a confidently wrong slice. Restart from the first page. - A full page issues a cursor, so the last page is whichever comes back short. You may spend one empty request discovering the end.
pagination.totalis the size of the whole filtered set, not what remains after the cursor.
// The SDK hides the cursor loop entirely.
for await (const event of parity.iterateEvents({ category: 'economy', tradable: true })) {
upsert(event);
}The outcome object
{
"id": "ctr_9f31c2…", // QUOTE AGAINST THIS. Opaque; names no venue.
"label": "No change",
"teamLogoUrl": null, // a crest on a team fixture, served from your Parity host
"teamColor": null,
"teamMarks": null, // both clubs of a two-sided match result
"teamAbbr": null,
"probability": 0.715, // reference, 0 to 1
"yesCents": 71.5, // reference, in cents
"noCents": 28.5,
"executableYesCents": 72, // what a BUY costs. null = no usable side
"executableNoCents": 29,
"tradable": true, // executableYesCents !== null || executableNoCents !== null
"tradingClosed": false, // true once trading has stopped, whatever status says
"yesLabel": null, // the venue's word for a side, e.g. "Up". Display only
"noLabel": null,
"sportsMarketType": null, // "moneyline", "totals"... on a sports contract
"spreadCents": 1, // ask − bid. Large means the reference is a poor guide
"priceBasis": "midpoint", // midpoint | last | source | none
"priceUpdatedAt": "2026-08-20T04:43:08.322Z",
"priceFeed": "websocket", // websocket | rest
"status": "open",
"threshold": null, // parsed strike, for ordering a ladder
"direction": null, // "above" | "below" | null. Which side of the strike pays
"volume24h": 415244.02, // CONTRACTS traded at the venue in 24h. Not dollars
"volume24hContracts": 415244.02, // the identical number, unambiguously named
"closesAt": "2026-09-16T00:00:00.000Z"
}The display rule
yesCents is the probability. executableYesCents is the price. A buy takes the ask and a sell takes the bid; buying NO costs 1 − bid, which is why both executable fields are published rather than one.
A null executable price means we hold no usable book side: show “no price”. Never fall back to the reference. Falling back is the exact bug these two fields exist to prevent. The player would have seen a number the fill was never going to match.
null is not the same as “untradeable”. Ask POST /quotes before you disable anything: the quote endpoint is the authoritative price and it can quote contracts this payload prices at null.
Five-minute contracts are always null here
The recurring five-minute crypto contracts (btc, eth, sol, xrp) always report executableYesCents: null, executableNoCents: null, spreadCents: null and tradable: false. They are fully tradeable.
The reason is honesty about latency. Those contracts live 300 seconds and their book can move 50 cents inside one minute; no catalogue snapshot keeps up, so rather than publish a figure that would be wrong by the time you read it, we publish nothing and price them from the live order book at quote time. For these, POST /quotes is the only source of a price, and the price it returns is the one you are filled at.
A threshold without a direction is half a proposition
direction says which side of threshold a contract pays on: "above" for “will it reach $X”, "below" for “will it dip to $X”, and null where nothing states one: a date threshold, a candidate, a bucket.
threshold alone is not enough to group, sort, bracket or preview a set. One event routinely carries both arms: “What price will Bitcoin hit in August?” has a contract asking whether the price falls to $80,000 and another asking whether it rises to $80,000. They share a strike and buy contradictory propositions, so rendering them as one range sells a fall as a rise. Group by direction first, then by threshold.
Read this field; do not parse the label. The direction is derived at the source from the contract’s own question, which is where whole families of this catalogue put it. Those contracts are labelled with a bare "80,000", and there is nothing in the label to parse. Where a venue does put an arrow in the label it is the same fact, already read for you.
null means unknown, and it is not a default of "above". Fail closed on it: an undirected contract inside a set that has directions is one you should not put beside a directed one.
- Filter a “what can I offer” list on
tradable, never onyesCents !== null. The second is true on almost every contract and is the reference, not a price. - A crossed book (ask at or below bid) is bad data that looks like a gift, so both sides are discarded and the outcome reads as untradable rather than cheap.
- Prices of exactly 0 and exactly 1 are settled, not tradeable, and are never published as a price.
Contract ids
A contract is addressed only by its opaque ctr_… id. It is the id /api/v1/events publishes, the id you post to /api/v1/quotes, and the id you read back on an order, a position and a settlement. One id space, end to end, and no second identifier to store or to get wrong.
- It names no venue and carries no upstream identifier. The mapping is one-way and derived, so nothing about Parity's sourcing crosses the boundary.
- An event id (
evt_…) and a contract id are different things. You browse by event and you trade by contract. imageUrlis a Parity path,/api/v1/images/{eventId}, and the bytes are proxied rather than redirected. Prefix it with your issued host. The SDK hasparity.imageUrl(event).
One event, every outcome
The list caps outcomes per event for card-sized payloads. The detail route returns the whole set, which is what rendering a ladder or a nine-candidate field needs. It accepts either the evt_… id or the slug. They are the same identity wearing different clothes, and forcing a caller to know which one they are holding buys nothing.
curl -s "$PARITY_BASE/api/v1/events/fed-decision-september" \
-H "Authorization: Bearer $PARITY_KEY"
# → { "data": { …, "outcomes": [ … ] } }An event your operator is not offered reads as 404 unknown_event, not as a separate refusal: this route answers for things in your catalogue, and your catalogue does not contain it.
A contract whose source moves it to a settlement protocol Parity does not trade is withdrawn: it stops being open, so it leaves what you can offer, and a quote or order against it is refused (409 market_paused on a quote, 422 market_unavailable on an order, both with the detail “This market is not available to trade yet.”), or with 422 market_not_open once its status has moved. Positions already held on it stay where they are and settle when it resolves.
Categories
{
"data": [
{ "category": "politics", "label": "Politics", "eventCount": 412 },
{ "category": "economy", "label": "Economy", "eventCount": 188 }
],
"totalEvents": 600
}- Counted over events, under your own offering policy, by the same filter the list endpoint applies, so a tab that says 188 returns 188. A count of contracts would be a larger number and the wrong one next to an event list, because one event carries many contracts.
- Only categories with something in them are returned. An integrator building a nav should not discover which of sixteen tabs are empty by opening all sixteen.
- Ordered canonically rather than by count, so a nav does not reshuffle itself every time volume moves.
sportsis an ordinary category: it is listed whenever it has events, and?category=sportsreturns them. Narrow inside it withsportandleague.
Language
The catalogue is English unless you ask. Send ?locale=es, pt or hr on GET /api/v1/events or GET /api/v1/events/{id} and the human text (the event title and outcome label) comes back translated where a translation exists: the source's own published translation first, then a machine translation Parity holds, then the original.
- Where a string was translated, the original travels beside it as
sourceTitleon the event andsourceLabelon the outcome. Printtitle; parsesourceTitle. Both are absent (not null) in English. - Nothing a decision is made on changes with the locale: ids, ordering, cursors, prices and
tradableare identical in every language. - A locale outside the four is treated as English, not refused. Tags are matched exactly, so send
pt, notpt-BR.

