client_order_id is the contract
You supply client_order_id on every order. It must be unique per order and is
the idempotency key for the whole placement path.
The rule that matters: on any failure where you are unsure, retry with the
same client_order_id. At most one order ever results. Retrying with a new id
after a timeout is how duplicate orders happen.
Order states
Cancellations arrive as success
A 201 does not mean your order is on the book.Post-only orders that would have crossed, unfillable fok orders, ioc
remainders, self-trade prevention, and market buys that would breach max_cost
all return a successful response with status: "canceled" and a reason.They are execution outcomes, not errors. Always read status on a success
response instead of assuming placement succeeded.
Placement responses are real outcomes
The API holds your request until the order is sequenced, so the response
describes what actually happened — filled_qty, resting_qty, and a fills
array in stream order. There is no separate “pending” state to poll through.
seq on the response is the sequencer stamp, which is your audit join back to
canonical history.
Balances are not in the response
Reservation and fee accounting are deliberately absent from placement responses
— they arrive on a different lane. Get them from
GET /v1/portfolio/balance or, better, the user WebSocket channel.
Reading your orders
status takes a comma-separated list. Querying by client_order_id is how you
resolve a timeout.
Cancelling
decrease shrinks resting quantity without losing queue priority — use it
instead of cancel-and-replace when you only want to reduce size.
Cancels charge the write bucket, same as placements.
Batching
POST /v1/portfolio/orders/batch places several orders in one request. It
charges one write token per order, so batching saves round trips, not rate
limit budget.