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
A200 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.
request_completed is the only accepted order outcome. Treat anything else as a rejection or a retry signal.
Walkthrough
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.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.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.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.- Limit (PlaceLimitOrder)
- Market (PlaceMarketOrder)
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).Read the RequestAck status
Branch on
These are application-level rejections on a
status. request_completed is the accepted path. Everything below is a rejection or a retry signal — full list in error codes.status | What happened | What to do |
|---|---|---|
request_completed | Order applied. | Continue to Step 5. |
insufficient_margin_to_place | Not enough margin to rest the order. | Reduce size, add collateral, or raise leverage (Step 2). |
insufficient_margin_to_trade | Not enough margin to execute the resulting trade. | Same as above. |
rejected_invalid_price | Price failed validation — usually unaligned to the tick. | Re-floor price to the market tick. |
rejected_invalid_size | Size 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_book | A post-only order would take instead of make. | Re-price behind the touch, or drop post_only. |
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.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) or504(ack_timeout). - A
200carrying a transientRequestAckstatus:request_droppedorretry_required.
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.
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.