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” |
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. |
Listing events
| Parameter | Type | Notes |
|---|---|---|
| category | string | One canonical category. An unknown value is 400 invalid_category, not a silently unfiltered catalogue. Sports is never returned. |
| 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. |
| tradable | boolean | true restricts to events with at least one executable side. |
| closesAfter / closesBefore | ISO-8601 | Window on the close time. |
| cursor | string | Opaque. Echo nextCursor from the previous page. |
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",
"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",
"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. liquidity, where the market surface publishes it, is dollars.
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",
"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
"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.
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.
- Sports markets are excluded from the catalogue entirely and are never persisted.
Language
Titles and outcome labels are returned in the provider's own translation when one was published, and in the original language otherwise. Parity does not machine-translate a question and present it as the published wording.

