Response envelope
Every response uses the same envelope. On success the payload is indata and
errors is an empty array:
data is an empty object and errors is a single structured error
object:
Pagination
List endpoints acceptpage (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. A200 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.
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 — 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.
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.- Live holdings —
isSettled=false&isClosed=false. This is whatGET /positionsreturns when you pass neither flag. - Resolved history —
isSettled=true, which is whatGET /positions/portfolio-historyreturns.
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.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:
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. Applying100 - price to a field that
is already user-facing is just as wrong as skipping it on one that isn’t.
Worked example
You place SELL 10 YES @ 70¢ and it fills completely.GET /orders
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:
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 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
Therole 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.
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.
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.403. That is a jurisdiction check, not a throttle: do not retry it.
