Response envelope

Every response uses the same envelope. On success the payload is in data and errors is an empty array:
On failure data is an empty object and errors is a single structured error object:
Note the asymmetry: errors is an array on success and a single object on failure. Parse defensively — a client that assumes an array in both cases will break on the first error it receives.

Pagination

List endpoints accept page (1-based) and limit query parameters, and return a pagination block alongside the rows:
count is the size of the current page; totalCount is the full result set.

Request headers you may send

Optional. Omitting it gives US Eastern; a value we do not recognise is ignored rather than rejected, so a bad header degrades to Eastern instead of failing the request. closeAt and every other timestamp stay UTC regardless — only the human-readable copy follows the header. Drive countdowns and sorting from the timestamps. Full detail in the Events guide — §3a.

Type and naming consistency

marketId, eventId, page, limit, isSettled, order status casing, and settlementTimerHours each have a single, unified type across every endpoint that uses them — so a client generated from the spec, or hand-written against it, can rely on one shape per field everywhere it appears.
Until every underlying service is confirmed to emit exactly these types on every response, integrate defensively: accept either a string or a number for marketId / eventId / page / limit at your parsing boundary, and compare order statuses case-insensitively — earlier builds returned the placement acknowledgement as PENDING, and it is now pending. This is the same guidance as the response-envelope asymmetry above — parse for the documented shape, but don’t hard-fail on close variants while the contract settles.

Orders are accepted asynchronously

This service validates an order, locks the funds or contracts behind it, and hands it to the matching engine. A 200 therefore means accepted for processing, not matched — the fill is not final when the call returns.
Read the resulting state from the order, position and trade endpoints, or subscribe to USER_UPDATE to receive fills as they happen instead of polling.

Money and units

The API uses two money units, and which one you get depends on what the field measures — not on which endpoint you called. The rule that covers almost every case:
Per-contract prices are cents. Amounts of money are dollars.A contract settles at 100¢ = $1.00, so contracts × priceInCents / 100 converts a position into dollars. That division is why the two units coexist: prices stay exact integers on a 1–99 grid, and money keeps cent precision as a decimal.

Cents — per-contract prices

Always whole numbers, 1–99. Never fractional, never negative. price, averagePrice, sellPrice, averageBuyPrice, limitPrice, newPrice, avgPrice, avgYesPrice, avgNoPrice, yesPrice, correctedPrice, currentPrice, dayMinYesPrice / dayMaxYesPrice / dayMinNoPrice / dayMaxNoPrice, and every price inside an order book snapshot or ORDERBOOK_DEPTH payload.
averagePrice on GET /orders can carry two decimal places — it is a quantity-weighted average of several fills (e.g. 70.67), not a single contract price. It is still cents; it is just not an integer.

Dollars — everything that is an amount of money

Decimals with 2 places. These are already divided by 100 for you.

The one endpoint that mixes both

POST /wallet/deposit and POST /wallet/withdraw take dollars but answer in cents.You send amount in dollars. The acknowledgement echoes amountCents in cents — the integer actually handed to the matching engine. They describe the same money in different units, so never compare them directly or feed amountCents back in as amount.
POST /wallet/deposit — request
Response
amount is converted with Math.round(amount × 100), so a third decimal place is rounded away — send at most 2.

Not money at all

A few numeric fields look monetary but are not. quantity, quantityFilled, remainingQuantity, yesContracts, noContracts, totalVolume and finalPosition are contract counts. returnPercent is a percentage. feeRate is a decimal coefficient (0.015 = 1.5%), not an amount. payoutIfWin is the exception that proves the rule — it equals the contract count because a winning contract pays exactly $1.00, so the number is simultaneously the count and the dollar payout.

Contract vocabulary

Position endpoints report contract counts under four prefixes. They are not interchangeable, and two of them do not describe contracts you own at all.
available is a subdivision of what you hold; open is not.yesContracts = lockedYesContracts + availableYesContracts always holds. openYesContracts sits outside that identity — adding it to yesContracts double-counts nothing, because those contracts do not exist until the order fills. A user with 10 YES, 4 of them on a resting SELL, and a resting BUY for another 25, reports yesContracts: 10, lockedYesContracts: 4, availableYesContracts: 6, openYesContracts: 25.
lockedBalance on a position is the cash counterpart: dollars committed to your resting BUY orders on that market, in the same way locked*Contracts is the contracts committed to your resting SELL orders. A resting order reserves one or the other — a BUY reserves cash, a SELL reserves contracts — but every resting order of either kind counts toward open*Contracts.
openYesContracts / openNoContracts are keyed on the order book side, not the side you traded. A resting SELL is stored as a BUY of the opposite contract, so a resting SELL YES adds to openNoContracts. This is the same conversion described in Order prices — apply it before showing these two fields next to a user’s YES/NO holdings, or a pending sell will appear on the wrong side.

Market and position lifecycle

A market moves through a fixed status sequence, and each position on that market carries three booleans that track where it sits in that sequence. status on a position object is the market’s status, not the position’s — the position’s own state is the three flags.

Market status

Only pending → open → halted → closed → settled/expired and the → terminated branches occur; the three terminal statuses never transition again. Check status per market — markets under one event are halted and settled independently.

Position flags

isDetermined and isSettled always move together. Settlement prices and pays a position in one transaction, so there is no window in which a position is determined but not yet settled. Treat them as one flag; branch on isSettled.
isSettled and isClosed are independent, and settling does not close. Settlement records the payout but leaves the contract counts standing, so a paid-out position normally reads isSettled: true, isClosed: false. Filter on isSettled for “resolved”, never on isClosed.isClosed alone is likewise ambiguous — true means either the user sold out of the position or the market stopped trading with the position intact. It also un-sets itself: buying back into a market flips it to false.
The practical filters:
  • Live holdings — isSettled=false&isClosed=false. This is what GET /positions returns when you pass neither flag.
  • Resolved history — isSettled=true, which is what GET /positions/portfolio-history returns.

Order prices

Every order endpoint speaks one of two price representations. Getting them mixed up is the single most common integration bug on this API.
Read this before rendering any order. The order endpoints return prices in the order book’s representation, not the one you submitted. For an order you placed as a SELL, the stored price is 100 - yourPrice and the stored side is the opposite contract. Display it without converting back and your UI shows a SELL YES @ 70¢ as a NO order at 30¢.

Why

The matching engine models only one side of each contract pair. A SELL is economically identical to a BUY of the complementary contract, so the gateway rewrites it before publishing: YES and NO always sum to 100¢, which is what makes the rewrite lossless — and reversible.

The rule

isConvertedOrder (or side: "sell" on the WebSocket feed) is the flag. When it is true:
When it is false, predictionSide and price are already what the user submitted — do nothing.
price is null on market orders — they carry no limit price. Guard the subtraction, or 100 - null renders as 100.

Which fields need it

Not every price is in order book space. Applying 100 - price to a field that is already user-facing is just as wrong as skipping it on one that isn’t.
USER_UPDATE is the easiest to get wrong: side is correct ("sell"), but the contractSide next to it is the stored side, so a SELL YES arrives as { side: "sell", contractSide: "no", price: 30 }. Flip contractSide and invert price whenever side === "sell".

Worked example

You place SELL 10 YES @ 70¢ and it fills completely.
GET /orders
Render it as SELL 10 YES @ 70¢ (100 - 30), average fill 70¢ (100 - 30), bought at 60¢ (averageBuyPrice, used as-is), realised P&L +$1.00 (used as-is: (70 - 60) × 10 / 100). Order book depth (GET /orderbook/orderbook, ORDERBOOK_DEPTH) is unaffected — it is already published as two independent YES/NO bid ladders and needs no conversion.

Trading fees

Fees are charged per fill, at execution, to both sides of every trade. There is no fee to place, modify, or cancel an order, and no fee on deposits, withdrawals, or settlement.

Formula

Equivalently, with P = price / 100 as a probability and the rate as a decimal:
The result is always rounded up to a whole cent.

Why price × (100 − price)

The fee tracks uncertainty, not notional. It peaks at 50¢ — a coin flip — and falls towards zero as a contract approaches 1¢ or 99¢, where there is little left to be wrong about. Because the term is symmetric, p × (100 − p) is identical for a price and its complement. A leg’s fee is the same computed from its own price or the other side’s — so the SELL → BUY conversion above never changes what you are charged. Only the maker/taker rate differs between the two legs of a trade.

Worked examples

A taker buys 100 contracts at 40¢:
The maker on the other side of that same fill sits at 60¢:
The maximum possible fee is at 50¢: rate × quantity × 0.25, i.e. 1.5¢ per contract for a taker and 0.375¢ for a maker.

Which rate applies

The role field on each trade tells you: "taker" (the incoming order that crossed the spread) pays 600 bps, "maker" (the order that was already resting) pays 150 bps. A single order can be both across different fills — check role per trade, not per order.

Where fees appear in the API

Fee amounts in the API are dollars; the formula above is in cents. Divide by 100.
Fees are not quoted before execution. Estimate client-side with the formula — but size the order against the worst case (50¢), because a market order can fill across several price levels and each fill is charged on its own price.

Rate limits and restrictions

The order APIs (create, cancel, modify, bulk-cancel) support admin-configurable rate limiting, enforced per-user or globally depending on how a given rule is scoped. These rules are disabled by default — the underlying configuration defaults to an empty rule set, so out of the box you will not see 429 TooManyRequests on these endpoints. Every other endpoint in this reference has no rate limiting at all, configurable or otherwise.
“Disabled by default” is a configuration default, not a platform guarantee. An admin can enable per-user or global limits on the order APIs at any time without a client-visible announcement — do not rely on their absence. Confirm your brand’s actual configuration before assuming no 429 is possible in production.
Build the normal courtesies in regardless of current configuration — retry with exponential backoff and jitter, and treat a 429 as retryable if you ever receive one — so an admin enabling a limit later does not require a client change.
Order placement remains geo-blocked in restricted jurisdictions, which surfaces as 403. That is a jurisdiction check, not a throttle: do not retry it.

CORS

Browser clients must be served from an origin present in the API’s allowlist. Add your domain before expecting browser-side requests to succeed — server-side calls are unaffected.