Skip to navigation

Orders

All order actions are signed by your trading key — see Authentication & Signing for the domain and signing rules. account_id and wallet are as defined in Account format.

Submit orders

POST /orders

The endpoint is a batch: 1–20 independently signed legs per request, all for the same account_id.

{
"account_id": "0x…",
"orders": [
{
"market": "BTC",
"side": "BUY",
"order_type": "MARKET",
"size": "0.001",
"limit_price": "65129",
"time_in_force": "IOC",
"reduce_only": false,
"leverage": 10,
"client_order_id": "my-order-1",
"nonce": "1787067314951",
"signature": "0x…"
}
]
}
FieldRequiredSignedNotes
market, side, size, limit_priceyesyesside is BUY or SELL
order_typenoyesDefaults to MARKET — but sign orderType: "MARKET" anyway
time_in_forceMARKET/LIMITyesGTC, IOC, or ALO (post-only). Omit on TRIGGER; still sign IOC
reduce_onlynoyesDefaults to false
leveragenoyesInteger. Not range-checked at validation
client_order_idyesnoNot a member of Order. Omitting it is a 400
triggerTRIGGER onlyyesPlain orders still sign triggerPrice: "0", isMarket: false, tpsl: ""
parent_client_order_idnonoAttaches a TP/SL leg to an entry leg in the same batch — see Bracket orders
nonceyesyesDecimal string in the body, uint256 when signed

Order types

order_typeBehavior
MARKETTakes liquidity immediately. Always IOC. The default when order_type is omitted
LIMITRests on the book at limit_price, time_in_force: "GTC" — or IOC to take-or-cancel, or ALO to post only
TRIGGERA TP/SL order, armed until its trigger fires

The schema also carries LIQUIDATION and ORPHAN_LIQUIDATION. They are server-generated, appear on read routes only, and must never be submitted — accept them when parsing.

What limit_price means changes with the type:

  • On a MARKET order it is your slippage bound, which you compute yourself as mark × (1 ± tolerance). You can never be filled beyond it; whatever cannot fill within the bound is cancelled.
  • On a LIMIT order it is the resting price.
  • On a TRIGGER order it is the price the order fires at — not the price that arms it. That is trigger.trigger_price.

TRIGGER orders omit time_in_force entirely; the server derives it from trigger.is_market.

Post-only

Post-only is not a separate flag: it is the time_in_force value ALO (add liquidity only). An order carries exactly one time_in_force — GTC, IOC, or ALO — so post-only cannot be combined with another time in force. A post-only order rests until filled or cancelled, like GTC, but it never takes: it rests or it is rejected.

{
"account_id": "0x…",
"orders": [
{
"market": "BTC", "side": "BUY", "order_type": "LIMIT",
"size": "0.001", "limit_price": "59000", "time_in_force": "ALO",
"leverage": 10, "client_order_id": "maker-1",
"nonce": "1787067314950", "signature": "0x…"
}
]
}

The leg signs timeInForce: "ALO".

  • LIMIT only. A MARKET leg with ALO is rejected with market_order_must_be_ioc. A TRIGGER leg’s time in force is always derived from trigger.is_market, so a trigger cannot be post-only.
  • The crossing check happens at the venue, not at validation. A post-only order whose price would match the book passes POST /orders as SUBMITTED and then reads REJECTED with reason: post_only_would_take on GET /orders/{orderId}. Keep a buy below the best ask and a sell above the best bid.
  • A post-only fill is always a maker fill, so it is charged the maker rate.
  • A modify inherits ALO, so the new order can be rejected with post_only_would_take in the same way.

Trigger orders

{
"market": "BTC",
"side": "SELL",
"order_type": "TRIGGER",
"size": "0.001",
"limit_price": "60000",
"trigger": { "trigger_price": "60500", "is_market": true, "tpsl": "SL" },
"reduce_only": true,
"client_order_id": "my-sl-1",
"nonce": "1787067314952",
"signature": "0x…"
}
  • trigger_price — the raw mark-price threshold that arms the fire (no slippage applied).
  • is_market — true fires an IOC order at limit_price. Only is_market: true is supported today; trigger-limit (false) is rejected.
  • tpsl — TP or SL. SL fires when the mark moves against the position, TP when it moves in favour. A trigger already past the mark on its fire side is rejected.
  • size: "0" on a reduce-only trigger means close the full position at fire time.

Bracket orders

A bracket is an opening MARKET or LIMIT entry with a whole-position take-profit and/or stop-loss attached in the same batch. The TP/SL legs are accepted with the entry, wait while it works, and arm themselves once it fills — no polling for the fill, no window where the position is unprotected.

Attach a leg by naming the entry’s client_order_id in parent_client_order_id:

{
"account_id": "0x…",
"orders": [
{
"market": "BTC", "side": "BUY", "order_type": "LIMIT",
"size": "0.001", "limit_price": "60000", "time_in_force": "GTC",
"leverage": 10, "client_order_id": "entry-1",
"nonce": "1787067314951", "signature": "0x…"
},
{
"market": "BTC", "side": "SELL", "order_type": "TRIGGER",
"size": "0", "limit_price": "62500",
"trigger": { "trigger_price": "63000", "is_market": true, "tpsl": "TP" },
"reduce_only": true, "parent_client_order_id": "entry-1",
"client_order_id": "tp-1", "nonce": "1787067314952", "signature": "0x…"
},
{
"market": "BTC", "side": "SELL", "order_type": "TRIGGER",
"size": "0", "limit_price": "57500",
"trigger": { "trigger_price": "58000", "is_market": true, "tpsl": "SL" },
"reduce_only": true, "parent_client_order_id": "entry-1",
"client_order_id": "sl-1", "nonce": "1787067314953", "signature": "0x…"
}
]
}

Shape rules, checked before anything is queued:

RuleReject
parent_client_order_id names an entry leg of the same batch; every client_order_id in the batch is unique400 INVALID_REQUEST
An attached leg is a reduce-only TRIGGER with size: "0"400 INVALID_REQUEST
The entry is an opening (not reduce-only) MARKET or LIMITbracket_parent_invalid
At most one TP and one SL, same market, opposite side to the entrybracket_child_invalid
TP above the entry price for a long and below for a short; SL the other way. The reference is limit_price on a LIMIT entry and the current mark on a MARKET entrybracket_tp_wrong_side, bracket_sl_wrong_side
  • On a MARKET entry, check TP/SL against the mark at submit, not the entry’s limit_price — that is only your slippage bound. A TP placed just above a long’s slippage bound but at or below the mark is rejected with bracket_tp_wrong_side.
  • parent_client_order_id is not signed — each leg still signs its own plain Order.
  • The group is decided together: if any member fails validation, every member of the group comes back REJECTED with the same reason. Other legs in the batch are unaffected.
  • Attached legs count toward max_open_orders from the moment they are accepted.

Lifecycle of an attached leg:

Entry outcomeAttached TP/SL
WorkingPENDING_PARENT — not armed, cannot fire, does not block a close. Listed by /account/open-orders with parent_order_id set, not yet by /position-triggers
Any fill, however the entry ended (FILLED or PARTIALLY_FILLED_CANCELLED)Arms as a normal whole-position trigger once the fill has settled into a position — it reads SUBMITTED, and /account/position-triggers lists it
Cancelled or rejected with no fillCANCELLED, reason: bracket_parent_not_filled
Filled but no position on the entry’s side by settlementCANCELLED, reason: bracket_no_position
Entry never persistedCANCELLED, reason: bracket_parent_missing

When a bracket leg arms, it replaces any armed whole-position trigger of the same tpsl kind on that market — the older one is cancelled with reason: bracket_replaced, so a position never carries two full-size stops.

While it is PENDING_PARENT the leg can be cancelled (order_type: "TRIGGER") but not modified (modify_not_modifiable). Modifying the entry re-checks the pending legs against the new limit_price and rejects the modify with bracket_tp_wrong_side / bracket_sl_wrong_side if a level would end up on the wrong side; otherwise the legs follow the modify to the new order_id.

Price and size rules

RuleSource
limit_price is an exact multiple of tick_sizetick_size
Price has at most px_decimals places; size at most sz_decimalspx_decimals, sz_decimals
size × price ≥ min_order_value, checked after rounding. Reduce-only legs are exemptmin_order_value

tick_size binds harder than px_decimals: BTC allows one decimal place but its tick is 1, so 60000.5 is rejected with price_off_tick.

Signing an order

Each leg signs Order(bytes32 account, …) — the struct is on Authentication & Signing. Every signed field must byte-match the body; a mismatch is a 403, not a validation error.

Response

data is an array index-aligned with your legs; top-level success is true only if every leg was SUBMITTED:

{
"success": true,
"data": [
{ "order_id": "…", "client_order_id": "my-order-1", "status": "SUBMITTED" }
],
"timestamp": 1787067314999
}

Legs are independent — one REJECTED (with reason) never cancels the others. The one exception is a bracket: an entry and its attached TP/SL are accepted or rejected as a group. SUBMITTED means validated and queued, not filled; the order then advances asynchronously.

Rejections

StageShapeScope
Validation400, error.code: INVALID_REQUESTWhole request
Signature403, FORBIDDENWhole request
Business rules200, leg status: "REJECTED" + reasonThat leg only
reasonCause
price_off_ticklimit_price is not a multiple of tick_size
close_trigger_coexistenceReduce-only order on a position that still has an armed reduce-only TRIGGER. Cancel the trigger first — see Exit
market_order_must_be_iocA MARKET leg with time_in_force other than IOC — including ALO
post_only_would_takeAsynchronous: a post-only order that would have crossed the book. Read it from GET /orders/{orderId}, not the submit response
bracket_parent_invalid, bracket_child_invalid, bracket_tp_wrong_side, bracket_sl_wrong_sideBracket shape or level rules — see Bracket orders. Returned on every leg of the group

The list is not exhaustive.

Order lifecycle

GET /orders/{orderId}

The happy path is SUBMITTED → ACCEPTED → EXECUTING → FILLED.

StatusMeaning
SUBMITTEDValidated and queued. A TRIGGER order stays here while armed — a long-lived SUBMITTED trigger is normal, not stuck
ACCEPTEDLive at the venue. A trigger reaches this only once it fires
EXECUTINGFilling
PENDING_MODIFYThe new order a modify created, held until the order it replaces terminates
PENDING_PARENTA TP/SL attached to an entry that has not filled yet. Arms (→ SUBMITTED) once the entry fills — see Bracket orders

Five statuses are terminal:

Terminal statusMeaning
FILLEDFully executed
PARTIALLY_FILLED_CANCELLEDPartly filled, remainder cancelled
CANCELLEDCancelled before filling. reason says why when the system did it: bracket_parent_not_filled, bracket_no_position, bracket_parent_missing, bracket_replaced
REJECTEDTurned down — see reason
FAILEDCould not be routed to the venue

GET /orders/{orderId} can briefly return 404 right after submit — orders persist asynchronously. Retry with backoff. Statuses here are uppercase; list reads use a different vocabulary.

Cancel an order

POST /orders/{orderId}/cancel

Signed by your trading key as:

Cancel(address account, string orderId, string orderType, uint256 nonce)

Note that account here is your wallet, not the account_id:

{
"account": "0xYourWallet",
"order_type": "LIMIT",
"nonce": "1787067314953",
"signature": "0x…"
}

order_type is LIMIT (resting order) or TRIGGER (armed trigger, or a TP/SL still waiting for its entry); it is part of the signed payload, as is orderId — taken from the URL path, not the body. Returns status: SUBMITTED | REJECTED; 403 when the signature does not authorize the account. Cancellation is asynchronous — confirm with open-orders or position-triggers. Cancelling an entry that still has attached TP/SL cancels them too, with reason: bracket_parent_not_filled, unless the entry had already partly filled — then they arm against the position that fill opened.

Cancel in batch

POST /orders/cancel-batch
{
"account": "0xYourWallet",
"cancels": [
{ "order_id": "…", "order_type": "LIMIT", "nonce": "1787067314954", "signature": "0x…" }
]
}

1–20 legs, each signing its own Cancel. Per-leg status is SUBMITTED or FAILED (with reason) — FAILED, not REJECTED. A bad signature fails only its leg, as 200 with top-level success: false.

Modify an order

POST /orders/{orderId}/modify

Signed by your trading key as:

Modify(bytes32 account, string orderId, string clientOrderId,
string limitPrice, string size, string triggerPrice, uint256 nonce)
{
"account_id": "0x…",
"client_order_id": "my-order-1b",
"limit_price": "64900",
"size": "0.002",
"trigger_price": "0",
"nonce": "1787067314955",
"signature": "0x…"
}
  • Every field is required on every call; there is no partial modify.
  • client_order_id must be new — reusing the modified order’s id is rejected. It is signed here, unlike on an order leg.
  • Only price, size, and trigger price can change. Market, side, time in force (including ALO), reduce-only, leverage, and order type are all inherited from the original order.
  • For a limit order send trigger_price: "0"; for a trigger order limit_price is the fire price.
  • size: "0" (full-position close) is allowed only on TP/SL triggers.
  • Success returns the new order’s order_id. If a trigger modify races the trigger firing (or an open-order-cap check at apply time), the modify is dropped and the original order is untouched.
  • A TP/SL in PENDING_PARENT cannot be modified (modify_not_modifiable). Modifying its entry re-validates the attached levels against the new limit_price and moves them to the new order_id — see Bracket orders.

Next: Position Lifecycle