Skip to main content
A working order flow is five calls: read your cash, optionally set leverage, place the order, read the acknowledgement, then confirm the position and fill. The hard part is not the happy path — it is reading the acknowledgement correctly and retrying without double-filling. This guide covers both. For the bare placement sequence with credential setup, see the Quickstart. This page assumes you already have an account, an X-API-KEY for reads, and a session key for signing. Run against staging: https://staging.zerolatencylabs.com. Collateral is USDC, provisioned out of band — there is no faucet, and spot deposit is not yet available.

The one rule that matters

A 200 OK is not proof of acceptance. Signed-write endpoints (order placement) return 200 with a RequestAck body even when the exchange rejects the order; the outcome is in status, not the status line. Account, master-key, and session writes carry a success boolean instead. Always read the body. The error model explains why, and the error codes list every status and code value you branch on.
{ "status": "request_completed", "processed_at_ns": 1719158400000000000 }
request_completed is the only accepted order outcome. Treat anything else as a rejection or a retry signal.

Walkthrough

1

Check your cash balance

GET /api/v1/portfolio/balance, authenticated with the X-API-KEY header. Pass subaccount_index and portfolio_index (both required) as query parameters.The response is a PortfolioBalanceResponse. The field you care about is cash_balance — a decimal string in USDC, the cash available for trading and withdrawal. Balances read back as decimal strings even though writes take integer units. Parse the string; do not assume a float.
{
  "account_id": 1024,
  "subaccount_index": 0,
  "portfolio_index": 0,
  "cash_balance": "1000.00",
  "name": "Main",
  "status": "ACTIVE"
}
A cash_balance of 0 means no collateral has been provisioned yet — arrange it out of band before placing an order, or the order rejects on margin. A status other than ACTIVE (IN_LIQUIDATION, CLOSED) means the portfolio cannot take new risk.
2

Set leverage (optional)

PUT /api/v1/leverage, a plain JSON write authenticated with the X-API-KEY header. Skip this step to keep the portfolio’s current setting.The SetLeverageRequest body takes subaccount_index, portfolio_index, market (for example BTC-PERP), and leverage (an integer multiplier). Leverage is set per portfolio per market. The response is a LeverageSettingResponse echoing the stored value.Higher leverage lowers the margin a position requires, which changes whether the next order fits. Positions, leverage, and funding covers how leverage maps to margin.
3

Place the order

POST /api/v1/trading/order/place/limit for a limit order, or POST /api/v1/trading/order/place/market for a market order. Both take a session-key signed body and both return a RequestAck.Prices and sizes are raw integers, never decimals: price is a PriceOfAtom (1e-16 USDC per atom), quantity is signed atoms where the sign is the side — positive is long/buy, negative is short/sell. Markets, assets, and precision has the per-asset scales and the conversion math; align price to the market tick and size to the step before signing, or the order is rejected.
portfolio_id, price, signed quantity, flags (the expiry field encodes time-in-force — see orders and time-in-force), and asset (BTC is 0, ETH is 1).
Only LIMIT and MARKET orders are placeable. Conditional orders — stop, take-profit — have no endpoint yet. Self-trade-prevention flags are accepted on the wire but not yet enforced.
4

Read the RequestAck status

Branch on status. request_completed is the accepted path. Everything below is a rejection or a retry signal — full list in error codes.
statusWhat happenedWhat to do
request_completedOrder applied.Continue to Step 5.
insufficient_margin_to_placeNot enough margin to rest the order.Reduce size, add collateral, or raise leverage (Step 2).
insufficient_margin_to_tradeNot enough margin to execute the resulting trade.Same as above.
rejected_invalid_pricePrice failed validation — usually unaligned to the tick.Re-floor price to the market tick.
rejected_invalid_sizeSize failed validation — usually unaligned to the step or below minimum.Re-floor quantity to the step; check the minimum size.
rejected_post_only_would_cross_bookA post-only order would take instead of make.Re-price behind the touch, or drop post_only.
These are application-level rejections on a 200. Fix the input and submit a new order with a fresh request_id — they were not applied, so reusing the id is not required.
5

Confirm the position and fill

GET /api/v1/positions returns an array of PositionResponse; narrow it with optional subaccount_index, portfolio_index, and market query parameters. Each entry carries market, side (LONG/SHORT), quantity, avg_entry_price, and net_funding as decimal strings in human units. An empty array means the order rested without filling (a resting limit) or did not execute.GET /api/v1/fills returns an array of FillResponse. subaccount_index is required; portfolio_index, limit (1–1000, default 100), and offset are optional. Each fill carries the request_id that links it back to your order, plus quantity, price, and fee as decimal strings. Match on request_id to confirm your specific order executed.

Retrying without double-filling

A transport failure is not a rejection — it means you do not know whether the order was applied. This happens two ways:
  • A non-2xx status: 503 (request_dropped, at_capacity, retry_required, service_unavailable) or 504 (ack_timeout).
  • A 200 carrying a transient RequestAck status: request_dropped or retry_required.
In every one of these cases, resubmit with the same request_id. The request_id is a UUIDv7 and the idempotency key. If the original was already applied, the retry returns duplicate_request_id — a success signal, not an error; the order executed exactly once. If it was not applied, the retry processes it for the first time.
On 503, 504, or a network drop, never resubmit with a new request_id. A new id is a new order, and if the first one landed you have just placed the trade twice. Reuse the same id and let duplicate_request_id tell you the original already executed.
One caveat on idempotency: the request_id’s embedded UUIDv7 timestamp must be current. Generate it fresh at first send and keep it for retries of that same order; a too-old id is rejected with request_timestamp_skew before it reaches the book.

Full reference

The endpoint schemas and field-by-field detail live in the Trading API reference (see the Overview). For signing the order body from your own client — there is no SDK yet — see signed payloads.