Perpetuals: trading
Placing and cancelling perp orders, positions, leverage, margin, take-profit/stop-loss and account history.
Every endpoint here is signed — see Authentication. The key is the account binding: there are no wallet or account fields in any request body, and a key can only ever act on the account it was minted for.
Place an order
POST /api/v1/perp/orders
{
"market": "BTC-USDC",
"side": "buy",
"type": "limit",
"size": "0.01",
"price": "80000",
"leverage": 10,
"timeInForce": "GTC",
"clientOrderId": "my-order-1"
}| Field | Required | Notes |
|---|---|---|
market | ✓ | BTC-USDC |
side | ✓ | buy / sell (aliases long / short, case-insensitive) |
type | ✓ | limit / market |
size | ✓ | base units, on stepSize grid |
price | for limit | on tickSize grid |
leverage | omit to use the account's setting for that market | |
timeInForce | GTC (default), IOC, FOK, GTX (aliases alo, post_only) | |
postOnly | reject if the order would take | |
reduceOnly | never increase the position | |
positionSide | hedge mode | long / short; omit in one-way mode |
clientOrderId | your idempotency key — strongly recommended | |
slippageTolerance | market orders, fraction: 0.005 = 0.5% | |
slippageBehavior | partial_fill / reject_all / warn_only | |
stpMode | self-trade prevention: none, expire_taker, expire_maker, expire_both |
The receipt echoes the order's real state:
{
"orderId": "…", "market": "BTC-USDC", "side": "buy", "type": "limit",
"size": "0.01", "filledSize": "0", "remainingSize": "0.01",
"status": "open", "timeInForce": "GTC", "leverage": 10,
"reduceOnly": false, "postOnly": false, "autoModified": false,
"createdAt": 1790095097073, "updatedAt": 1790095097073
}Statuses are created, open, partial, filled, canceled, expired (plus force_cancelled for risk-engine cancellations).
Leverage resolution
When you omit leverage, the server resolves it: the account's stored preference for that market → the platform default of 20x → clamped to the market's current maxLeverage. The receipt's leverage field is the value that actually applied, which may be lower than what you asked for if the market's cap moved. Read it rather than assuming; in batch responses each leg carries its own leverage for the same reason.
Auto-modification in one-way mode
In one-way mode, a reverse-direction order without reduceOnly is interpreted as "close what I have": the size is capped at the existing position and reduceOnly is forced on. The response tells you this happened — autoModified: true, with requestedSize / requestedReduceOnly preserving what you sent next to the size / reduceOnly that applied. Surface that to users; silently filling a smaller order than requested is how reconciliation bugs start.
Batch and cancel
| Call | Endpoint |
|---|---|
| Place many | POST /api/v1/perp/orders/batch — body {"orders": [ …PlaceOrderRequest… ]} |
| Cancel one | DELETE /api/v1/perp/orders/{id} |
| Cancel many | DELETE /api/v1/perp/orders/batch — body {"order_ids": [...], "market": "…", "side": "…"} |
| Cancel all | DELETE /api/v1/perp/orders/cancel-all — optional body {"market": "…"}; no body means every market |
| List open | GET /api/v1/perp/orders |
| Fetch one | GET /api/v1/perp/orders/{id} |
Batch place returns per-leg results (successCount, failedCount, and an entry per order carrying either orderId or error) — a batch is not atomic, so always walk the array. Batch cancel's canonical field is order_ids; orderIds is accepted as an alias but the snake_case form is the documented contract. Batch calls cost weight 3 against your rate budget — see Rate limits.
Positions
GET /api/v1/perp/positions # all, or ?market=BTC-USDC
GET /api/v1/perp/positions/{market} # array — hedge mode can hold long and short at once{
"id": "…", "market": "BTC-USDC", "side": "long", "positionSide": "net",
"size": "0.01", "entryPrice": "84120.4", "markPrice": "86148.95",
"leverage": 10, "marginMode": "cross",
"margin": "84.12", "maintenanceMargin": "3.44",
"liquidationPrice": null, "bankruptcyPrice": "…",
"unrealizedPnl": "20.28", "unrealizedPnlPercent": "24.11",
"realizedPnl": "0", "adlRanking": 2,
"tpSlStatus": "none", "protection": [], "accountIndex": 0
}Three fields that mean more than they look:
liquidationPriceisnullin cross margin — and that is correct, not missing data. In cross mode there is no per-position liquidation price; the account as a whole is what gets liquidated. In isolated mode it is a number.adlRanking(1–5, 5 = first in line) is your queue position for auto-deleveraging if the insurance fund is exhausted on the other side.protectionlists every live thing that will close this position at a trigger — TP/SL levels, bracket exits, OCO legs — with the engine that owns each.tpSlStatus(none/tp_only/sl_only/both) is the summary across all of them.
Close, leverage, margin
POST /api/v1/perp/positions/{market}/close # {"type":"market"} or {"type":"limit","price":"…","size":"…"}
PUT /api/v1/perp/positions/{market}/leverage # {"leverage": 10, "position_side": "long"}
POST /api/v1/perp/positions/{market}/margin # {"type":"add","amount":"50","position_side":"long"}
GET /api/v1/perp/positions/{market}/leverageOmitting size on close means a full close. position_side is required in hedge mode, omitted in one-way. Note the snake_case bodies here — see Notation.
Take profit and stop loss
POST /api/v1/perp/positions/{market}/tpsl # set
PUT /api/v1/perp/positions/{market}/tpsl # modify
DELETE /api/v1/perp/positions/{market}/tpsl # clear{ "take_profit_price": "95000", "stop_loss_price": "78000", "replace": false }TP/SL on perpetuals is market-priced only — the trigger fires a market close. There is no limit-priced TP/SL; fields that once suggested otherwise were removed rather than left as decoration.
If the position is already protected by a bracket or OCO leg, the default replace: false refuses with POS_TPSL_EXISTS (14005) and lists the order ids holding that protection, rather than arming a second engine against the same position. Send replace: true to cancel those and take over.
History
| Endpoint | Contents |
|---|---|
GET /api/v1/perp/orders/history | terminal orders; market / side / status / start_time / end_time / limit / cursor |
GET /api/v1/perp/history/trades | your fills |
GET /api/v1/perp/history/pnl | realized PnL summary |
GET /api/v1/perp/history/funding | funding paid and received |
GET /api/v1/perp/history/liquidations | your liquidations |
GET /api/v1/perp/positions/history | closed positions |
GET /api/v1/accounts/me/perp/margin | account-level margin state |
GET /api/v1/accounts/me/perp/trading-stats | volume, fees, tier inputs |
Paged endpoints return a cursor; follow it until empty rather than stopping at the first short page.
Before you go live
- Send a
clientOrderIdon every order. It is what makes a retry safe after a timeout. - Check
statusandfilledSizeon the receipt —IOC/FOKorders can come back immediately terminal. - Check
leverageandautoModifiedon the receipt before recording the fill in your own books. - Test on
https://api-testnet-stable.1024ex.comfirst; keys are separate and the faucet funds test accounts.
Updated 9 days ago
