order
Places one or more orders. Every order in the batch is signed together and applied in array order.
orderPlace one or more orders (limit, market, trigger, TP/SL).Agent-signed
How the batch is interpreted as a group.
"na""normalTpsl""positionTpsl""normalTrigger""reversePosition""closeAll"Order bodies; at most 50 per action.
Instrument's external id — the `index` value from `perpMeta`; Outcome markets use the `instrument_id`. Long form: `asset`.
`true` to buy, `false` to sell. Long form: `isBuy`.
Limit price. The literal `"bbo"` prices at the current best bid/offer. Long form: `limitPx`.
Size in base units. Long form: `sz`.
Either a `limit` or a `trigger` body. Long form: `orderType`.
Client order id, used later by `cancelByCloid`. Long form: `cloid`.
Required when `tif` is `Gtd`. Unix milliseconds, and must be in the future.
Margin mode. Long form: `marginMode`. Optional: when omitted the node routes the order to the account's current margin mode for this market (its saved position profile, `cross` for an account that has never set one) — so the same body can land on a different mode after an `updateMarginMode`. API clients should send `m` explicitly. The margin mode is part of the position key, so it also selects which position the order opens into or reduces.
"cross""isolated"Reduce-only. Defaults to `false`. Long form: `reduceOnly`. On a perpetual the node may set this to `true` by itself when your available margin does not cover the order and the order would only shrink an existing position — see Auto-conversion to reduce-only on the order page.
Must be `order`.
"order"Optional builder code. The order signature covers this object.
Builder wallet address.
Fee in tenths of a basis point: `rate = f / 100,000`, so `f = 10` is 1 bp (`0.01%`).
curl -X POST https://rpc.txflow-testnet.xyz/exchange \
-H "Content-Type: application/json" \
-H "x-api-version: 2026-08-16" \
-d '{
"action": {
"type": "order",
"grouping": "na",
"orders": [
{
"a": 1,
"b": true,
"p": "50000.0",
"s": "0.01",
"r": false,
"t": {
"limit": {
"tif": "Gtc"
}
}
}
]
},
"signature": {
"r": "0x<r>",
"s": "0x<s>",
"v": 27
},
"nonce": 1757992743994,
"vaultAddress": null,
"signatureChainId": null
}'`0` on success.
The action's internal name, which does not always match the request `type`.
"PlaceOrder"A rejected request returns `err` with a non-zero `code` and the reason in `message` (or `response`).
"ok"{
"status": "ok",
"code": 0,
"data": {
"type": "PlaceOrder",
"data": {
"statuses": [
"success"
]
}
}
}Outcome orders
For an Outcome market, a is its instrument_id. YES is the long leg and NO
the short leg; b and r together encode the leg and direction:
| Intent | b | r |
|---|---|---|
| Buy YES | true | false |
| Buy NO | false | false |
| Sell YES | false | true |
| Sell NO | true | true |
p is the price of the traded leg, between 0 and 1: buying NO at 0.30 sends
b: false, r: false, p: "0.30". Outcome orders use grouping: "na" and do not
support trigger/TP-SL order types. Time-in-force must be Gtc, Gtd, or Alo;
an Ioc order is rejected with Outcome limit orders only support GTC, GTD, and ALO orders, reported asynchronously on
outcomeOrderUpdate.
The initial HTTP response can be an asynchronous acceptance
({"status":"ok","traceId":…}) rather than a fill. If the queued order fails,
outcomeOrderUpdate pushes the reason under
that traceId; successes are not pushed. Follow the matching lifecycle on
outcomeOrderUpdates or read
outcomeFrontendOpenOrders.
Order bodies use abbreviated keys. The long forms noted above are accepted on input, but the abbreviated keys are what the signature covers. The optional builder object is documented under Attach a builder to an order.
Order body
Instrument's external id — the `index` value from `perpMeta`; Outcome markets use the `instrument_id`. Long form: `asset`.
`true` to buy, `false` to sell. Long form: `isBuy`.
Limit price. The literal `"bbo"` prices at the current best bid/offer. Long form: `limitPx`.
Size in base units. Long form: `sz`.
Either a `limit` or a `trigger` body. Long form: `orderType`.
Client order id, used later by `cancelByCloid`. Long form: `cloid`.
Required when `tif` is `Gtd`. Unix milliseconds, and must be in the future.
Margin mode. Long form: `marginMode`. Optional: when omitted the node routes the order to the account's current margin mode for this market (its saved position profile, `cross` for an account that has never set one) — so the same body can land on a different mode after an `updateMarginMode`. API clients should send `m` explicitly. The margin mode is part of the position key, so it also selects which position the order opens into or reduces.
"cross""isolated"Reduce-only. Defaults to `false`. Long form: `reduceOnly`. On a perpetual the node may set this to `true` by itself when your available margin does not cover the order and the order would only shrink an existing position — see Auto-conversion to reduce-only on the order page.
Price, size, and client order id
| Rule | Rejected with |
|---|---|
p is a multiple of the instrument's priceTick from perpMeta, with no more decimals than the tick | Price precision does not match the tick size: <tick> / Tick size does not match |
s has at most szDecimals decimals and lies between the minimum size and maxLimitOrderSize / maxMarketOrderSize | Minimum order amount is <x> <BASE> / Maximum limit order amount is … |
| A limit price stays inside the oracle price band | Order buy price is not within the price limit (Minimum price: …; Maximum price: …) |
c is any string up to 64 bytes, unique among your open orders. A 0x prefix is reserved for liquidity-provider accounts, on every market | cloid length exceeds maximum / Duplicate client order ID <cloid> / Invalid cloid format |
Order type
A limit order carries a time-in-force:
{ "limit": { "tif": "Gtc" } }tif | Behavior |
|---|---|
Gtc | Good-til-canceled; rests on the book |
Ioc | Immediate-or-cancel; unfilled remainder is canceled |
Alo | Add-liquidity-only (post-only); rejected if it would take |
Gtd | Good-til-date; requires expiresAt |
FrontendMarket | Treated as a market order |
A trigger order carries a trigger price instead:
{
"trigger": {
"isMarket": true,
"triggerPx": "48000.0",
"tpsl": "sl"
}
}tpsl is tp (take-profit) or sl (stop-loss). Trigger orders are only accepted under a normalTpsl, positionTpsl, or normalTrigger grouping; sending one with grouping: "na" is rejected with Trigger orders are not supported for grouping na.
Auto-conversion to reduce-only
On a perpetual, an order you sent with r: false can come back as a
reduce-only order. When the available margin in your perp account does not cover
the order's required margin, the node does not reject it outright — it first
checks whether the order would only shrink an existing position, and if so flips
r to true and places it with no margin frozen.
All of the following must hold, or the order is rejected with
Insufficient margin in account as before:
| Condition |
|---|
| The market is a perpetual (spot and Outcome orders are rejected, never converted) |
| The account is in One-Way position mode |
| You hold a position in that market, in the margin mode the order routes to, on the opposite side (a buy reduces a short, a sell reduces a long) |
| The order size does not exceed that position's size |
| The account is not a liquidity-provider / market-maker account (those skip the margin freeze entirely, so they never reach this path) |
Two consequences to plan for:
- The converted order can no longer open or flip. It will only shrink the position, and the remainder past the position size is not placed.
- Attached TP/SL children are dropped. A reduce-only order cannot carry
protection orders, so the
normalTpslchildren prepared with the parent are removed at conversion time. Re-arm the protection on the resulting position yourself.
The action response does not report the conversion — its statuses carry one
entry for the whole action, not per-order results. Read reduceOnly back from
frontendOpenOrders (or
historicalOrders once the order is terminal) to
see the effective flag, and send r: true explicitly whenever reduce-only is
what you actually want.
Triggered is not filled
A TP/SL that reaches its trigger price is fired, not guaranteed to execute. Two separate things happen:
- Trigger — the protection order's trigger state becomes
Triggered, and in a TP/SL pair the other side is canceled at the same moment (Canceledtrigger state). This is OCO: it happens on trigger, not on fill. - Execution — the fired order is then submitted to the matcher like any other order, and can be fully filled, partially filled, canceled, or rejected.
A fired order that does not fill does not revert to a pending protection order — it is in a terminal state and the position is left unprotected. Common reasons the fired leg does not fill:
| Outcome | Why |
|---|---|
| Canceled with no or partial fill | A fired market order carries the oracle price-protection band; the remainder past the band, or past the book's available liquidity, is canceled |
| Rejected | The limit price falls outside the price band or off the tick, or the instrument is halted / delisted, or the order is over the open-interest cap |
| Skipped entirely | The underlying position is already flat (closed, liquidated, or closed by the other side of the pair) when the trigger fires |
Because the sibling was already canceled in step 1, a failed fill leaves neither protection order live. Re-arm the protection yourself after confirming the outcome.
Confirm the result rather than assuming it: read the pair back with
tpslOrders (live) and
historicalTpslOrders (terminal, carrying
cancelSource / cancelReason), check the resulting order on
historicalOrders, and follow
orderUpdates for the live transitions.
Next steps
Read resting orders back with frontendOpenOrders. To reprice or resize a resting order, use modify2.