Trading
Orders
An order spends a quote and returns an exact instruction for your ledger: what to debit, and what to give back. Every surface (a mobile sheet, a desktop panel, your API client) enters the same pipeline at the same point.
The pipeline
- 1Quote
- 2Confirm
- 3Order
- 4Route
- 5Fill
- 6Position
Confirmation is an input to this pipeline, never a shortcut around it. The order row is written before anything is sent, because an execution that succeeds upstream and then fails to be recorded is the one outcome with no recovery path.
The lifecycle
POST /quotes
│
▼ POST /orders { executionIntent: "record_intent" }
reserve_pending recorded, awaiting your reserve
│
│ you reserve authorizationAmount in YOUR ledger
▼ POST /orders/{id}/confirm-reserve
pending_venue_execution reserve confirmed, cleared to execute
│
▼
submitted ─▶ filled | partially_filled | rejected
│
└─▶ unknown after timeout ─▶ 409 reconciliation_requiredThe two reserve states are the reason the diagram does not start at submitted: an order is recorded and funded before it can execute. A sandbox tenant, or any caller sending executionIntent: "simulate", skips them and lands directly on a simulated fill.
| status | settlementState | What your ledger does |
|---|---|---|
filled | settled | Debit actualDebit, release releaseAmount (zero on a full fill). Done. |
partially_filled | pending | While the venue is still working the order (transmissionState unknown): debit nothing new beyond actualDebit and HOLD the rest. More can still fill. |
partially_filled | settled | Once the venue has closed the order (transmissionState resolved): debit actualDebit and release the non-zero releaseAmount. Debiting the full stake keeps money that is not ours to keep. |
rejected | settled | actualDebit is 0.00 and the whole authorization comes back. rejectReason says why. |
reserve_pending | pending | You have not confirmed the reserve. Nothing is debited, and the order cannot execute until you do. Confirm, or cancel and release. |
pending_venue_execution | pending | Your hold is confirmed and the order is cleared to execute. Keep holding: an order is not a fill. |
pending | pending | Not resolved. Hold the authorization and poll. |
submitted | pending | Not resolved. Hold the authorization and poll. This is the reconciliation case. |
cancelled | settled | Terminal, nothing debited. |
Swipe the table sideways for more columns.
settlementState collapses every status to the only question a wallet has to answer right now: is the money final, or must I hold and come back? Switch on it, and submitted can never be mistaken for terminal. An order awaiting execution is pending for the same reason an unresolved one is: it may still execute. To tell those two apart, read transmissionState.
A live fill
An order filled at a venue reports partially_filled as its fills arrive, however complete they look, because only the venue can say an order is finished. It is final when transmissionState is resolved and settlementState is settled; that is the moment to apply actualDebit and releaseAmount. While a fill's venue fee is still being read from the venue's records, the order stays open and the remainder stays held, so the release you apply never contains money the venue kept.
Submit an order
/api/v1/ordersAPI keyorders:write, plus X-Parity-User.{
"quoteId": "fc3be731-98dc-4c03-80ac-23576a708d9c",
"clientOrderId": "ticket-9031", // YOUR id. The idempotency key.
"executionIntent": "simulate", // optional: record_intent | simulate
"reserveReference": "hold-77120", // optional: your id for a hold already placed
"maxSlippage": 0 // optional: adverse price allowance, 0 to 0.05
}| Field | Type | Meaning |
|---|---|---|
quoteId | string, 1 to 100 | The quote to spend. Spendable once. |
clientOrderId | string, 1 to 200 | Your id for the order, and the idempotency key. Reuse it on every retry of the same intent. |
executionIntent | record_intent | simulate | Optional. The default follows your tenant: record_intent for a live tenant (the order is recorded and waits for your reserve), simulate for a sandbox tenant (a simulated fill). simulate on a tenant above the simulated ceiling is 422 simulation_not_permitted. |
reserveReference | string, up to 200 | Optional. Your id for a hold you have already placed on the customer’s funds. Present, the reserve is confirmed in this call and the order comes back pending_venue_execution; absent, it comes back reserve_pending and waits for confirm-reserve. |
maxSlippage | number, 0 to under 1 | Optional, default 0. How much worse than the quoted price you will accept, in price units, in the adverse direction only. Clamped to the platform maximum (0.05) rather than refused. |
Swipe the table sideways for more columns.
{
"orderId": "3dd27f36-7780-4aa7-8af2-1a6854fbf8cb",
"clientOrderId": "ticket-9031",
"executionRef": "Parity_Best_7K2M9QH4TR8DXZ", // quote this to Parity support
"status": "filled",
"contractId": "ctr_9f31c2…",
"side": "YES",
"action": "buy",
"filledQuantity": 68.6111,
"averagePrice": 0.72,
"platformFee": 0.6,
"platformFeeBps": 120,
"notional": 49.4,
"stake": 50,
"rejectReason": null,
"simulated": true,
"createdAt": "2026-08-20T04:43:40.282Z",
"idempotentReplay": false,
"routing": { "reason": "only_venue", "consideredVenues": 1 },
"settlementState": "settled", // settled | pending. Read this first
"transmissionState": "resolved", // not_transmitted | unknown | resolved
"operatorMoney": {
"currency": "USD",
"authorizationAmount": "50.00", // what you reserved
"actualDebit": "50.00", // TAKE THIS
"releaseAmount": "0.00", // GIVE THIS BACK
"tradeAmount": "49.40",
"platformFee": "0.60",
"venueFee": "0.00",
"venueFeeKnown": false
},
"operatorExitMoney": null
}operatorMoney is what your ledger acts on
The float fields above (stake, notional, platformFee, averagePrice) are for display. The money object carries exact decimal strings and two identities that hold on every buy:
authorizationAmount = actualDebit + releaseAmountactualDebit = tradeAmount + platformFee + venueFee
Assert both before you post. Parsing "12.30" as a float has already left the exact world before your own ledger sees the figure.
On a live fill the venue's taker fee is inside the stake: it is taken from the amount sent to the venue, so actualDebit never exceeds authorizationAmount, venueFee is the fee the venue took (with venueFeeKnown: true), and filledQuantity can be below the quote's contracts. Any cent the venue did not use comes back as releaseAmount.
A sell is a credit, and carries a different object
On an exit operatorMoney is null, deliberately, and operatorExitMoney.sellProceeds is what to credit, already net of the platformFee and venueFee reported beside it. A single object for both directions 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.
On a live cash out, sellProceeds is what the sale actually returned after the venue's fee and the platform fee, rounded down to the cent. It can differ from the estimate on the quote, and the order's figure is the one to credit.
| Field | Meaning |
|---|---|
status | The state machine above. A partial fill is a real state, not an error. |
filledQuantity | Contracts held. This, not the quote’s contracts, is what the player owns. |
averagePrice | Volume-weighted fill price, 0-1. Display only. |
platformFeeBps | The rate this order was struck at, snapshotted from its quote, so you can reconcile the charge against the terms you agreed without asking what our config currently says. |
routing | reason and consideredVenues: a count and a cause, useful when debugging why an order went where it did. The venue itself is never named. |
idempotentReplay | true when this response replayed an order that already existed. |
rejectReason | Populated when status is rejected. Null otherwise. Where the refusal text would quote an order id from the market Parity routed to, it quotes executionRef instead. |
executionRef | The Parity execution reference: Parity_Best_ followed by 14 characters, stable for the life of the order. Quote it to Parity support when you report a problem. Support accepts it in any letter case, with or without the prefix. It is also on every order webhook, on reconciliation feed items, and on an order refusal that closed a recorded order. |
Swipe the table sideways for more columns.
201, or a 200 replay
| Status code | When |
|---|---|
201 | The order was created by this call. |
200 | This call replayed an existing order: idempotentReplay: true, and the body is the original order verbatim, including its original averagePrice and its original money. Never a re-quote. |
Swipe the table sideways for more columns.
clientOrderId is unique per operator and player, enforced by a database index rather than by a check in application code. Two requests arriving together produce one order; the loser of the race re-reads the winner's.
- Derive the key from the reservation in your own ledger. A random value per attempt gives you no protection at all, because a retry has to present the same string. A clock gives you a key you cannot reproduce after a crash.
- A rejected order still consumes its key. Retrying a spent key after a rejection must not open a second live order, so the rejected record stays.
When the venue does not answer
409 reconciliation_required is not a rejection
The venue took the order or it did not, and Parity is still confirming which. Collateral stays committed. Hold the authorization. Do not release it.
Releasing on an order that did in fact execute leaves a real position with no cash behind it, and you have no way to discover the mistake because you were told it failed. That is the most expensive lie this API could tell, so it declines to tell it: “unknown” is the only true answer until reconciliation resolves it durably.
# 1. You have an orderId. Poll it.
GET /api/v1/orders/{orderId}
→ { "status": "submitted", "settlementState": "pending", "transmissionState": "unknown",
"operatorMoney": { "actualDebit": "0.00", "releaseAmount": "50.00", … } }
# 2. You never received a response at all. Look the order up by YOUR id.
GET /api/v1/orders?externalUserId=user-4471&clientOrderId=ticket-9031
→ { "orders": [ { "orderId": "3dd27f36-…", "status": "submitted",
"settlementState": "pending", "transmissionState": "unknown", … } ],
"count": 1, "nextCursor": null, "hasMore": false }
# 3. That row is the same read as step 1: settlementState, transmissionState
# and the order's intent all arrive on it, so act on it directly.clientOrderIdis the identifier that survives a crash mid-request: you chose it before the request was sent. Parity'sorderIdonly ever existed in a response you may not have received.- It is unique within a user, so it must be sent with
externalUserId. Alone it is400 invalid_request: a question with no single answer, and returning the first match would silently pick one. - The lookup is a filter on the list route rather than a second endpoint, so a reconciler uses one call for “this one order” and “everything since 09:00”.
- The list route returns
settlementStateandtransmissionStateon every order, computed by the same serializer asGET /api/v1/orders/{id}. The lookup byclientOrderIdalso carries the order'sintent(the reserve confirmation and, once withdrawn, the release instruction), so it is a complete recovery read on its own. Only paged results, withoutclientOrderId, omitintent. - While unresolved, the read reports
actualDebitof"0.00"with the whole authorization asreleaseAmount. That is a description of an unfinished order, not an instruction to release:settlementStateis what tells you whether to act.
import { ReconciliationRequiredError } from './parity-sdk'; // the client Parity provides
try {
const order = await player.submitOrder({ quoteId, clientOrderId: 'ticket-9031' });
ledger.settle(order.operatorMoney);
} catch (err) {
if (err instanceof ReconciliationRequiredError) {
// err.holdAuthorization === true. Keep the hold.
const found = await player.findOrderByClientOrderId('ticket-9031');
if (!found) return schedulePoll(); // not visible yet: hold and retry
// The list row carries settlementState too; reading the order itself
// is equivalent and also returns the fills when asked for.
const order = await player.getOrder(found.orderId);
if (order.settlementState === 'pending') return schedulePoll();
return apply(order.operatorMoney);
}
throw err;
}Reading orders back
/api/v1/ordersAPI keyorders:read.| Parameter | Notes |
|---|---|
externalUserId | Your own id for a player. |
clientOrderId | Your own id for one order. Requires externalUserId; resolves to that single order. |
status | pending | reserve_pending | pending_venue_execution | submitted | partially_filled | filled | cancelled | rejected. Not validated: an unknown value returns an empty page. |
since / until | Inclusive ISO-8601 bounds on createdAt. A value that does not parse is ignored rather than refused. |
limit | 1 to 200, default 50. Clamped, never refused: above 200 reads as 200. |
cursor | Opaque. Echo nextCursor from the previous page. |
Swipe the table sideways for more columns.
Paging is keyset for the same reason as the catalogue: orders arrive while you page, and an offset would let one placed between two requests push an older order across the page boundary where you never see it. A missing trade, reported as a successful page.
/api/v1/orders/{id}API keyorderId, with settlementState. orders:read. Add ?fills=true for the individual executions.- Every order leaves the API through the same serialiser the POST response uses. A stored POST response and a later GET describe the order identically, and the fields most likely to differ between two shapes are exactly the ones that decide money.
- An order belonging to another partner returns the same
404 unknown_orderas one that does not exist. Distinguishing them would make the route an oracle for whether a guessed id is real.
Why fills are nested under an order
There is no top-level fills feed, and that is deliberate. A fill carries no money contract of its own: operatorMoney belongs to the order, computed once over the whole execution. A partner booking per fill double-counts the moment a venue fills one order in two pieces. Fills are a reconciliation detail (fillId, quantity, price, filledAt) and are off by default because the order already carries the average price, the filled quantity and the money.
The reserve handshake
An order is recorded before it is executable, and it becomes executable when you confirm that the customer's funds are held. That handshake is what executionIntent: "record_intent" on POST /api/v1/orders asks for, and it is the default for a live tenant. A sandbox tenant defaults to the simulated fill instead, because a sandbox exists so that a fill can be made to happen.
The order comes back with a real Parity orderId and an actualDebit of 0.00, the whole authorization showing as releaseAmount: at the moment an order is recorded, nothing has been spent.
The sequence, and why it has two stops
POST /api/v1/ordersrecords the order and reserves against it. Statusreserve_pending. It cannot execute. Parity will never transmit an order whose reserve you have not confirmed.- You place the hold on your customer's funds, for the exact
authorizationAmountwe returned. POST /api/v1/orders/{id}/confirm-reserve, optionally with your ownreserveReference. Statuspending_venue_execution: the order is now cleared to execute. If you reserve BEFORE calling us, sendreserveReferenceon the order itself and skip a round trip.
When confirm-reserve refuses
| Status | error | What happened | What to do |
|---|---|---|---|
409 | order_cancelled | The order was cancelled before you confirmed. | Release the authorization. Nothing stands behind it. |
409 | not_an_intent | The order is not awaiting a reserve. It is already at a venue, filled or rejected. | Do not absorb it. Read the order back with GET /api/v1/orders/{id} and act on its own settlementState and operatorMoney. |
Swipe the table sideways for more columns.
Neither of these means the hold is safe to drop by default
order_cancelled is the one case where releasing is right, because a cancellation has already issued its own exact releaseAmount. not_an_intent is not: absorbing it would tell you your hold stands behind a live order when the order has moved on without you.
Every step recovers from your own id
No call here asks you to remember anything we do not already hold. If the order POST times out, resend it: the same clientOrderId replays the original rather than opening a second. If the confirmation times out, resend it, or read the order back with GET /api/v1/orders?externalUserId=&clientOrderId=: it says reserve_pending or pending_venue_execution and there is no third answer.
How long an order sits in pending_venue_execution
Confirming the reserve clears an order to execute. It does not transmit it. Transmission additionally requires that live execution is confirmed for your account and that this specific order has been armed, which is a deliberate per-order act rather than something an order acquires by being placed. Until both are true the order stays pending_venue_execution with transmissionState: not_transmitted.
That is a safe place for an order to be, and it is readable: nothing was sent, so the order is still cancellable and its release instruction is exact. What it is not is a transient state you should assume will clear on its own. Live execution for your account is confirmed with Parity as part of going live, not through this API. Build the handshake and the hold now; ask us what is armed before you point real customers at it.
Live caps: per order, per player and overall
Where Parity sends your orders under your live operator policy, every buy is checked against the caps agreed for your account before it is sent: a maximum stake per order; per player, a maximum open exposure and a maximum number of live orders in the trailing 24 hours; and the same two figures across all your players together, where an overall cap is set. A player at their own cap is refused alone; an overall cap refuses every player. A sell (cash out) is never measured against these caps and never uses up a player's orders for the day.
An order that breaks a cap is not sent. It is withdrawn before it reaches a venue: status cancelled, transmissionState: not_transmitted, and its hold released, which your ledger applies like any other cancellation. The same happens to an order waiting to be sent while your trading pause is on. Ask your account team for the caps in force on your account.
transmissionState, and the question it answers
settlementState answers “may I apply the money yet?”. transmissionState answers the other question, and they are not the same:
| transmissionState | What it means | What to do |
|---|---|---|
not_transmitted | Provably nothing was sent to a venue. | You may cancel and receive a release instruction. |
unknown | It may or may not be at a venue. We asked and have no answer. | HOLD the authorization. Do not release, do not cancel. Poll. |
resolved | The venue answered or refused. | Apply operatorMoney and move on. |
Swipe the table sideways for more columns.
Cancelling, exactly once
/api/v1/orders/{id}/cancelAPI keyorders:write. Returns a cancellation object: a cancellationId, a currency and an exact releaseAmount, with actualDebit of 0.00 so authorizationAmount = actualDebit + releaseAmount closes here exactly as it does on a fill.- A second call returns
200with the samecancellationIdand the samereleaseAmount, andidempotentReplay: true. A cancellation whose response you never saw is safe to resend and cannot credit a customer twice. - It refuses any order whose
transmissionStateis notnot_transmitted, andreconciliation_requiredabove all. That is the state where we asked a venue and got no answer, and a release issued against one would hand your customer back money standing behind a position that may really exist. The409carriestransmissionState: "unknown", which says the right thing: keep holding, and wait for reconciliation.
When an order is refused
| Status | error | What happened | What to do |
|---|---|---|---|
404 | quote_not_found | No such quote. | Quote again. |
409 | quote_expired | The quote passed expiresAt before this call landed. | Re-quote and re-confirm the new price with the player. |
409 | quote_already_used | That quote was spent by a DIFFERENT order. It cannot be the one you are sending: a resend of the same clientOrderId replays as a 200 and never reaches this branch. | Read orderId on the body, the order that spent the quote, with GET /api/v1/orders/{id}. Then quote afresh for whatever is still unfilled. Your own clientOrderId finds nothing here. |
409 | price_stale | The upstream price is older than the router will trade on. | Wait for the next tick and re-quote. |
409 | reconciliation_required | The venue did not answer in time. | Hold the authorization and poll. Never release, never re-quote. |
409 | price_moved | The venue's live book has moved off the quoted price, read at the moment of confirm. Adverse moves only. priceMove carries fromCents, toCents and adverseCents. | Show the new number and let the player decide. You do not have to re-quote blind. |
409 | price_unverified | We could not reach the venue to confirm the price, so we did not take the bet. Not the same as price_stale, which is about the age of a price we already hold. | Retry in a moment. Nothing was accepted, so it is safe. |
409 | line_not_available | An outcome on this contract is executable above the wagerable ceiling. Buys only, so a holder of the near-certain side can still exit. detail names the offending price. | Re-quote. It clears when the book moves back inside the band. |
409 | idempotency_conflict | This clientOrderId already names a DIFFERENT order. Two orders want one key and the first one has it. | Do not retry. Reconcile your own reservation against the order that holds the key. |
403 | quote_not_yours | The quote belongs to another operator. | A quote is an offer to one party. Quote it yourself. |
403 | market_not_offered | The contract exists; your operator is not offered it. | A commercial question, not a code one. Escalate. |
422 | market_unavailable | The contract closed or resolved between quote and submit, or it settles on a protocol Parity does not trade (detail: “This market is not available to trade yet.”). | Stop offering it. |
422 | no_eligible_venue | No source is configured to execute this contract. A configuration problem. | Escalate: this does not resolve itself on the next tick. |
422 | insufficient_position | The sell is larger than the position held free to sell, checked again here under the position lock. | Re-read the position and size the exit in contracts. |
403 | sell_not_available | A sell on a live account while cashing out before settlement is turned off. Checked again here, because a quote stands for up to thirty seconds. | Release nothing (a sell holds no authorization). The position stays open and settles normally. |
503 | trading_paused | The trading pause on your account is on. Checked again here, before anything is reserved or written. | Release your hold. Re-quote once the pause is lifted. |
503 | live_execution_unavailable | Live execution is unavailable for this account right now. Nothing was placed. | Release your hold and retry later. |
422 | insufficient_collateral | The execution layer has no collateral for this order. | Release the authorization and escalate. |
422 | simulation_not_permitted | You sent executionIntent: "simulate" and your tenant's execution mode does not allow it. | Send executionIntent: "record_intent", or have the ceiling raised. |
403 | user_suspended | You suspended this player. Your own configuration speaking, not a credential problem. | Not retryable. Lift the suspension on your side. |
403 | over_order_limit | Above the maximum order size configured for your operator. detail names the ceiling and the figure. | Your own configuration refused it. Send a smaller order, or have the ceiling changed. |
403 | over_wager_limit | Above a maximum wager in force for this account (checked again at order time). | Re-quote at a smaller stake. |
403 | over_period_limit | This account has staked its allowance for a rolling window. | Wait: the allowance refills with time. |
409 | over_tenant_exposure | Your operator has committed its aggregate stake for the current window. | Wait for the window to roll over, then re-quote. |
422 | insufficient_funds | The player’s own balance cannot back the order. Only raised where Parity holds the wallet (the sandbox included); where you hold it, the funds check is yours. | Tell the customer. Not the same as insufficient_collateral, which is the operator’s float. |
422 | rejected | The execution layer refused. detail carries the reason: price_moved when the book passed the accepted price. | Release the authorization and re-quote. |
Swipe the table sideways for more columns.
price_stale and no_eligible_venue are different problems
One resolves itself on the next tick; the other needs a human. They report different codes so that whoever is debugging goes straight to the right place.
Price protection
The quoted price is a ceiling. If the book has moved past it by the time the order is submitted, the order is rejected rather than filled: the player agreed to a number, and a fill at a worse one is not the trade they confirmed.
After the order
A filled order opens or extends a position, readable at GET /api/v1/positions and GET /api/v1/positions/{id}. A position carries operatorMoney.costBasis and operatorMoney.realisedPnl, which are facts, not instructions: cash already debited, and P&L already booked. There is no mark-to-market: a number that moves on its own is not something a ledger can reconcile against, and a partner who wants the mark quotes a sell.
Every money fact an order produces also lands on the reconciliation feed (order.state_changed, order.filled, fee.assessed, collateral.moved), totally ordered and resumable. See Reconciliation.

