Trading
Quotes
A quote is a priced, time-bounded offer that binds contract, side, size, price, fee and quantity together, and tells your ledger exactly what to reserve. It is the record an order is checked against.
Create a quote
/api/v1/quotesAPI keyquotes:write, plus X-Parity-User.{
"marketId": "ctr_9f31c2…", // required. The contract id from /api/v1/events
"side": "YES", // required. YES | NO
"action": "buy", // optional, defaults to "buy"
"stake": 50 // cash. > 0 and ≤ 1,000,000
}{
"marketId": "ctr_9f31c2…",
"side": "YES",
"action": "sell",
"contracts": 68.6111 // quantity. The only way to exit in full.
}| Field | Type | Notes |
|---|---|---|
marketId | string | The opaque ctr_ contract id published by /api/v1/events. An id that names no contract is 404 market_not_found. |
side | string | YES or NO. Uppercase. |
action | string | buy opens or adds; sell exits. |
stake | number | Gross cash in dollars. Bounded on both ends: below a cent buys nothing, and unbounded is a denial-of-service vector dressed as an order. |
contracts | number | Quantity. Sizes a sell only. |
Swipe the table sideways for more columns.
Sizing: exactly one of stake or contracts
Send one. Sending both is 400 invalid_request, because a client bug that sends a stale stake alongside a computed contracts would otherwise trade a size the caller did not intend, and the response would look perfectly normal.
- A buy is sized in cash. Sizing a buy in
contractsis refused: it would commit the player to whatever that quantity costs at fill time, which is the open exposure the stake-first model exists to prevent. - An exit is sized in contracts. A partner holds a quantity, 68.6111 contracts, and to close it out by cash they would have to invert our fee arithmetic, including which way each rounding goes. Slightly high is an oversell; slightly low leaves an unsellable fraction behind. Naming the quantity moves that inversion to the side that owns the arithmetic.
The fee comes off the top
A $50 stake at 120bps wagers $49.40. The fee is taken from the stake before contracts are bought, not added on top of it, so the gross cash committed is the authorizationAmount, and a fully filled order releases nothing. A player who agreed to stake $50 is never charged $50.60.
stake 50.00
platformFee 0.60 = 50.00 × 120bps
tradeAmount 49.40 ← what buys contracts
contracts 68.6111 = 49.40 ÷ 0.72The venue's fee is inside the stake too
On a live account the execution venue may charge a taker fee of its own. It takes that fee from inside the amount it is sent and delivers fewer contracts to fit, so the customer is never debited more than the stake. Where the quote's venueFee is null (the usual case on a buy) the quote's contracts is the figure before that fee, and a live fill on a fee-charging market delivers fewer. Book what the order reports: filledQuantity is what the player holds, and operatorMoney.venueFee on the order is the fee the venue actually took. See The money contract.
The quote object
{
"quoteId": "fc3be731-98dc-4c03-80ac-23576a708d9c",
"contractId": "ctr_9f31c2…",
"side": "YES",
"action": "buy",
"executionPrice": 0.72, // WHAT YOU ARE CHARGED, 0-1
"priceBasis": "book", // book | reference
"priceSource": "stored_book", // venue_book | stored_book | stored_reference
"stake": 50,
"platformFee": 0.6,
"platformFeeBps": 120,
"commercialTerms": { "termsId": "…", "feeBps": 120, "revShareBps": 0 },
"venueFee": null,
"tradeAmount": 49.4,
"contracts": 68.6111,
"potentialPayout": 68.61,
"potentialProfit": 18.61,
"requestedStake": 50, // what you asked for
"supportedStake": null, // set when the book supports less than the request
"maxWagerNow": null, // set when YOUR exposure window narrowed it
"narrowedBy": null, // depth | tenant_exposure | null
"expiresAt": "2026-08-20T04:44:02.120Z",
"sourceTimestamp": "2026-08-20T04:43:27.496Z",
"simulated": true,
"freshness": { "level": "fresh", "ageSeconds": 5, "feed": "websocket" },
"routing": { "consideredVenues": 1, "routable": false },
"depth": null, // what the book could absorb, where levels are published
"priceQuality": { "midPrice": 0.715, "spread": 0.01, "impliedReturn": 0.981 },
"allIn": { // the whole trade for display. Book operatorMoney, not this
"stake": 50,
"parityFee": 0.6,
"venueFee": null,
"venueRate": null,
"venueRateSource": "unknown",
"tradeAmount": 49.4,
"contracts": 68.6111,
"executablePrice": 0.72,
"decimalOdds": 1.3722,
"totalCustomerDebit": 50,
"potentialPayout": 68.61,
"netResult": 18.61,
"charges": [
{ "name": "parity_platform_fee", "customerPays": 0.6, "splitOfExisting": 0, "bps": 120, "unconfigured": false }
],
"feeConfigVersion": 1
},
"operatorMoney": {
"currency": "USD",
"authorizationAmount": "50.00", // RESERVE THIS
"actualDebit": "50.00",
"releaseAmount": "0.00",
"tradeAmount": "49.40",
"platformFee": "0.60",
"venueFee": "0.00",
"venueFeeKnown": false
},
"operatorExitMoney": null
}{
…
"action": "sell",
"operatorMoney": null, // deliberately null on an exit
"operatorExitMoney": {
"currency": "USD",
"sellProceeds": "48.72", // CREDIT THIS. Already net of both fees.
"platformFee": "0.59",
"venueFee": "0.00",
"venueFeeKnown": false
}
}Two objects, because the money runs in two directions
A buy is a debit and a sell is a credit. A single object for both would hand a wallet an authorizationAmount on a trade that pays cash out, and a partner applying the contract as written would debit the player for selling their own position. So the entry object goes null on an exit rather than being filled with a plausible lie.
Both carry exact decimal strings. On the entry object, authorizationAmount = actualDebit + releaseAmount and actualDebit = tradeAmount + platformFee + venueFee, exactly. Assert them rather than tolerancing them.
| Field | Meaning |
|---|---|
quoteId | Present this to the order endpoint. Spendable once. |
executionPrice | The price per contract this quote will fill at, 0-1. A buy takes the ask, a sell takes the bid; buying NO costs 1 − bid. Never a midpoint. |
priceBasis | book means a real side of a real book. reference means no usable two-way price existed and this is an estimate: display it as one, or not at all. |
commercialTerms | The terms this quote was struck under, echoed back so your reconciliation can assert the rate you expected rather than inferring it from the fee and the stake. |
venueFee | null when the execution fee is not priced into the quote. The money objects then report it as 0.00 with venueFeeKnown: false, so you can see the difference between “free” and “unknown”. On a live cash out it is an estimate from the market’s published fee curve, rounded up to the cent, so the proceeds shown are not overstated; the order books what the sale actually returned. |
contracts | Quantity, rounded to four decimals for transport. The stored quote keeps full precision and is what the fill is checked against. Where venueFee is null and the venue charges a taker fee, a live buy delivers fewer contracts than this. On a live cash out it is the exact quantity that will be sent. |
potentialPayout | $1.00 per contract if the side is right, rounded down to the cent. |
potentialProfit | potentialPayout − the FULL stake, fee included. |
expiresAt | Absolute. 30 seconds by default on an ordinary market, 10 seconds on one in play or within 15 minutes of its close, 5 seconds on a five-minute contract. |
sourceTimestamp | When the underlying price was published upstream. Null when unknown. |
freshness | The server’s verdict on the price’s age: level, ageSeconds, feed. Render it; do not recompute it. The two disagreeing is the bug this field exists to make impossible. |
routing | How many sources could have competed, and whether any actually may. The alternatives are never listed: naming them is naming the venues. |
simulated | true when the fill is produced locally rather than at a venue. |
Swipe the table sideways for more columns.
There is no venue, no venueListingId and no upstream market id anywhere on this surface. A contract is addressed only by its ctr_… id, the same one the catalogue publishes and the same one you read back on the order.
The reference probability is not on the quote
To show a player both, read yesCents from the catalogue and executionPrice from the quote. Label the first as the probability and the second as the price.
Expiry
- A quote stands for 30 seconds by default, 10 seconds on a fixture in play or a market within 15 minutes of its close, and 5 seconds on a five-minute crypto contract, whose book moves too fast for anything longer.
expiresAtis absolute: use it rather than starting your own timer at receipt, and never hard-code either number. - Expiry is evaluated against the stored timestamp, never one the client sends. A client that could assert its own quote's age could hold a favourable price and submit it after the market moved.
- An expired quote is
409 quote_expiredand is never silently re-priced. Re-pricing executes at a number the player did not see, which is the precise failure quotes exist to prevent. - A quote is spendable once. A second order against the same
quoteIdis409 quote_already_used. - A quote issued to one operator cannot be spent by another:
403 quote_not_yours.
Stale prices: a cap before a refusal
Two different answers, because they are two different situations. A small bet on a slightly-behind price is ordinary business; a large one at the same moment is somebody acting on information we do not have yet.
| Response | Meaning | What to do |
|---|---|---|
422 stake_exceeds_stale_limit | Merely stale. The price is tradeable at a smaller size; this stake is not. | Re-request at no more than maxStake, which the refusal carries as a number beside detail. |
409 price_stale | Too far behind to stand behind at any size. | Wait for the next tick and re-quote. Not a payload problem. |
Swipe the table sideways for more columns.
The cap is checked against the ticket's stake, not the raw request: the ticket normalises and rounds what is actually being wagered, and gating on a number the ticket then changes would police the wrong figure.
When a quote is refused
| Status | error | What happened | What to do |
|---|---|---|---|
404 | market_not_found | No such contract. | Re-read the catalogue. Check the ctr_ id you sent. |
403 | market_not_offered | The contract exists; your operator is not offered it. Deliberately not a 404, because it is a real market and saying otherwise sends you to debug your ids. | A commercial question. Escalate rather than retrying. |
409 | price_stale | The upstream price is too far behind to stand behind at any size. | Wait for the next tick and re-quote. |
422 | stake_exceeds_stale_limit | Merely stale (or aging past its cap): a small stake is fine, this one is not. | Re-request at no more than maxStake. |
422 | market_not_open | The contract has closed, resolved or been voided. | Stop offering it. detail carries the status. |
422 | no_price | No usable price could be derived for that side. | Show “no price”. Do not fall back to the reference. |
422 | invalid_stake | The stake buys zero contracts, or a live cash out was sized with stake rather than contracts. | Raise the stake, or size the sell in contracts. |
422 | insufficient_position | Live accounts, sells. More contracts than the player holds free to sell, counting contracts already promised to their other cash outs in progress. | Offer sellableContracts, which the refusal carries. |
422 | below_venue_minimum | Live accounts, sells. Fewer contracts than the market’s minimum sale. A whole position under the minimum cannot be cashed out at all and settles normally. | Raise the sale to minimumContracts if the player holds enough; otherwise do not offer a cash out on this position. |
403 | sell_not_available | Live accounts, sells. Cashing out before settlement is turned off. The position stays open and settles normally. | Hide the cash-out control until it is available again. |
503 | trading_paused | The trading pause on your account is on. No new bets are taken; open positions settle normally. | Wait for the pause to be lifted. |
503 | order_lane_unavailable | Live accounts. The service that sends live orders has not reported recently, for example during a restart. Nothing was quoted. | Retry in a minute. |
503 | live_execution_unavailable | Live execution is unavailable for this account right now. Nothing was quoted. | Retry later. |
400 | invalid_request | The body failed validation, including sending both stake and contracts, or neither. | Read detail for the failing field. |
400 | missing_user | No X-Parity-User header. | Send your own id for the player. |
400 | invalid_json | The body did not parse. | Fix the request. |
409 | market_paused | A five-minute contract with no executable book this instant, or a contract that settles on a protocol Parity does not trade (detail: “This market is not available to trade yet.”). | Re-quote in a moment; the five-minute case clears by itself. The protocol case does not: stop offering the contract. |
409 | line_not_available | The line is not wagerable. On a five-minute contract: an outcome is executable above 95c. On any contract: after every fee the wager could not return more than it costs even if it wins. Buys only: a holder of the near-certain side can always quote an exit. maxCents carries the offending price. | Re-quote. The verdict is recomputed from the book on every request. |
409 | price_unverified | There is no book behind the price: it would fall back to the reference probability, which nobody has offered to trade at, so it is not quoted at any stake. | Retry in a moment. The payload is correct; the state of the market stopped it. |
409 | over_tenant_exposure | Your operator has committed its aggregate stake for the current window, across the API and the embed together. A budget with room for a smaller ticket produces a smaller quote instead (narrowedBy: tenant_exposure). | Wait retryAfterMs, then re-quote. Re-pricing does not help. |
409 | stake_exceeds_available_depth | The book cannot cover the requested stake at these terms. | Re-quote at supportedStake; the refusal carries it beside requestedStake. |
403 | user_suspended | You suspended this player. Your own configuration speaking. | Not retryable. Lift the suspension on your side. |
403 | over_wager_limit | Above a maximum wager in force for this account. | Send no more than maxStake. It works immediately. |
403 | over_period_limit | This account has staked its allowance for a rolling window. A smaller stake may not help. | Wait rather than re-pricing. The allowance refills with time. |
Swipe the table sideways for more columns.
What a quote refusal carries
Always error and detail. Where a refusal is about a number, the number is on the wire too, so you never parse prose: maxStake on stake_exceeds_stale_limit and over_wager_limit, maxCents on line_not_available, retryAfterMs on over_tenant_exposure, and requestedStake with supportedStake on stake_exceeds_available_depth, and sellableContracts with minimumContracts on a live cash out refused as insufficient_position or below_venue_minimum. Branch on error, show detail.
409 means re-quote, 422 means fix the request
The distinction is load-bearing. A 409 says the payload was correct and the state moved: the right response is to ask again in a moment. A 422 says the request itself needs to change. A client that retries a 422 unchanged will retry forever.
With the SDK
The two sizing rules are enforced by the type signatures rather than by a runtime error, so a buy cannot be sized in contracts and an exit cannot be sized in cash.
const player = parity.forUser('user-4471');
const buy = await player.quoteBuy({ contractId, side: 'YES', stake: '50.00' });
// buy.operatorMoney.authorizationAmount → "50.00", a branded Decimal string
const exit = await player.quoteSell({ contractId, side: 'YES', contracts: 68.6111 });
// exit.operatorExitMoney.sellProceeds → "48.72"
const order = await player.submitOrder({ quoteId: buy.quoteId, clientOrderId: 'ticket-9031' });Money comes back as a branded Decimal string that will not silently become a number, with toMinorUnits / addDecimal helpers for arithmetic. See the SDK.

