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, NOT YET EXECUTABLE
│
│ 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 | settled | 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. |
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.
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.
}{
"orderId": "3dd27f36-7780-4aa7-8af2-1a6854fbf8cb",
"clientOrderId": "ticket-9031",
"status": "filled",
"contractId": "ctr_9f31c2…",
"side": "YES",
"action": "buy",
"filledQuantity": 68.6111,
"averagePrice": 0.72,
"platformFee": 0.6,
"platformFeeBps": 120,
"predictaFee": 0.6,
"predictaFeeBps": 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 },
"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.
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.
| 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. |
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. |
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 does not yet know 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. This route carries settlementState.
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", … } ],
"count": 1, "nextCursor": null, "hasMore": false }
# 3. Then read THAT orderId back with step 1. The list route does not
# carry settlementState, so it finds the order; it does not settle it.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 does not return
settlementStateortransmissionState. OnlyGET /api/v1/orders/{id}computes them. So the recovery is two calls: find the order by yourclientOrderId, then read thatorderIdback. Branching onsettlementStatefrom a list response readsundefined, and a wallet that treatsundefinedas “not pending” releases a hold it must keep. - 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 '@/sdk';
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
// findOrderByClientOrderId reads the LIST route, which carries no
// settlementState. Read the order itself for the verdict.
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 | submitted | partially_filled | filled | cancelled | rejected. |
since / until | Inclusive ISO-8601 bounds on createdAt. |
limit | 1-200, default 50. |
cursor | Opaque. Echo nextCursor from the previous page. |
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. |
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 venue execution is enabled for your deployment 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. Whether venue execution is enabled for your deployment is settled 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.
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. |
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 no longer offers 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. | 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. | Re-read the position and size the exit in contracts. |
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. |
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. |
price_stale and no_eligible_venue are different problems
One resolves itself on the next tick; the other needs a human. They used to report identically, which sent anybody debugging it to the wrong 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.

