Trading
Place and cancel orders, close positions, query order and trade history.
Code samples assume the signed-request helper from Endpoints / Overview → Client setup.
POST /v2/orders
Place a new order.
| Auth | API_KEY (TRADE) |
| Rate limit | 300/min per accountId |
Body
{
symbol: string; // "BTCUSDT"
side: "BUY" | "SELL";
type: "LIMIT" | "MARKET" | "STOP" | "STOP_MARKET" | "TAKE_PROFIT" | "TAKE_PROFIT_MARKET";
quantity: string; // "0.001"
price?: string; // required for LIMIT
stopPrice?: string; // required for STOP / TAKE_PROFIT variants
positionSide?: "BOTH" | "LONG" | "SHORT";
reduceOnly?: boolean; // true to close-only
subAccountId?: string; // pin to a specific sub
clientOrderId?: string; // RECOMMENDED — UUID v4
}Response — new order
{
"ok": true,
"order": {
"id": "cmod1234...",
"accountId": "cmnw...",
"venue": "ZETARIUM",
"symbol": "BTCUSDT",
"side": "BUY",
"type": "LIMIT",
"quantity": "0.001",
"price": "30000",
"clientOrderId": "550e8400-...",
"venueOrderId": "12345678",
"status": "NEW",
"source": "API",
"createdAt": "...",
"updatedAt": "..."
},
"venue": { "...": "venue raw response" }
}Response — idempotent retry
{ "ok": true, "order": { "...": "same order" }, "idempotent": true }Errors
400— insufficient balance, RMS reject (reason: "max_order_notional_exceeded"), validation.403— missingTRADEpermission.409—clientOrderId already used by another account.
Side effects — order_new push on the WebSocket account channel.
Example
TS=$(($(date +%s) * 1000)); QUERY="timestamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
CLIENT_ID=$(uuidgen)
curl -X POST -H "X-API-KEY: $API_KEY" -H "Content-Type: application/json" \
-d "{\"symbol\":\"BTCUSDT\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"quantity\":\"0.001\",\"price\":\"30000\",\"clientOrderId\":\"$CLIENT_ID\"}" \
"$BASE_URL/v2/orders?${QUERY}&signature=${SIG}"import { randomUUID } from 'node:crypto';
const order = await signedRequest('POST', '/v2/orders', {
body: {
symbol: 'BTCUSDT',
side: 'BUY',
type: 'LIMIT',
quantity: '0.001',
price: '30000',
clientOrderId: randomUUID(),
},
});import uuid
order = signed_request("POST", "/v2/orders", body={
"symbol": "BTCUSDT",
"side": "BUY",
"type": "LIMIT",
"quantity": "0.001",
"price": "30000",
"clientOrderId": str(uuid.uuid4()),
}).json()import "github.com/google/uuid"
body := map[string]any{
"symbol": "BTCUSDT",
"side": "BUY",
"type": "LIMIT",
"quantity": "0.001",
"price": "30000",
"clientOrderId": uuid.NewString(),
}
res, _ := zdex.SignedRequest("POST", "/v2/orders", nil, body)use serde_json::json;
use uuid::Uuid;
let body = json!({
"symbol": "BTCUSDT",
"side": "BUY",
"type": "LIMIT",
"quantity": "0.001",
"price": "30000",
"clientOrderId": Uuid::new_v4().to_string(),
});
let order = signed_request(reqwest::Method::POST, "/v2/orders", &[], Some(&body)).await?;$order = signed_request('POST', '/v2/orders', [], [
'symbol' => 'BTCUSDT',
'side' => 'BUY',
'type' => 'LIMIT',
'quantity' => '0.001',
'price' => '30000',
'clientOrderId' => bin2hex(random_bytes(16)),
]);POST /v2/orders/batch
Submit up to 5 orders atomically (per-margin check).
| Auth | API_KEY (TRADE) |
| Rate limit | 100/min per accountId |
Body
{
orders: Array<{
symbol; side; type; quantity;
price?; stopPrice?;
positionSide?; reduceOnly?; subAccountId?;
clientOrderId? // unique per order
}>
}Response
{
"ok": true,
"results": [
{ "index": 0, "success": true, "orderId": "...", "symbol": "BTCUSDT", "status": "NEW" },
{ "index": 1, "success": false, "error": "Insufficient balance..." },
{ "index": 2, "success": true, "orderId": "...", "symbol": "ETHUSDT", "status": "NEW", "idempotent": true }
]
}Cumulative margin check — the total required margin across the batch is
checked against your balance in one shot. If the sum exceeds available margin,
none of the orders are placed and the call returns 400 cumulative balance.
Per-order clientOrderId idempotency works the same way.
Example
TS=$(($(date +%s) * 1000)); QUERY="timestamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -X POST -H "X-API-KEY: $API_KEY" -H "Content-Type: application/json" \
-d '{"orders":[
{"symbol":"BTCUSDT","side":"BUY","type":"LIMIT","quantity":"0.001","price":"30000","clientOrderId":"'"$(uuidgen)"'"},
{"symbol":"ETHUSDT","side":"BUY","type":"LIMIT","quantity":"0.01","price":"1800","clientOrderId":"'"$(uuidgen)"'"}
]}' \
"$BASE_URL/v2/orders/batch?${QUERY}&signature=${SIG}"const batch = await signedRequest('POST', '/v2/orders/batch', {
body: {
orders: [
{ symbol: 'BTCUSDT', side: 'BUY', type: 'LIMIT', quantity: '0.001', price: '30000', clientOrderId: randomUUID() },
{ symbol: 'ETHUSDT', side: 'BUY', type: 'LIMIT', quantity: '0.01', price: '1800', clientOrderId: randomUUID() },
],
},
});batch = signed_request("POST", "/v2/orders/batch", body={
"orders": [
{"symbol": "BTCUSDT", "side": "BUY", "type": "LIMIT",
"quantity": "0.001", "price": "30000", "clientOrderId": str(uuid.uuid4())},
{"symbol": "ETHUSDT", "side": "BUY", "type": "LIMIT",
"quantity": "0.01", "price": "1800", "clientOrderId": str(uuid.uuid4())},
]
}).json()body := map[string]any{"orders": []map[string]any{
{"symbol": "BTCUSDT", "side": "BUY", "type": "LIMIT",
"quantity": "0.001", "price": "30000", "clientOrderId": uuid.NewString()},
{"symbol": "ETHUSDT", "side": "BUY", "type": "LIMIT",
"quantity": "0.01", "price": "1800", "clientOrderId": uuid.NewString()},
}}
res, _ := zdex.SignedRequest("POST", "/v2/orders/batch", nil, body)let body = json!({
"orders": [
{ "symbol": "BTCUSDT", "side": "BUY", "type": "LIMIT",
"quantity": "0.001", "price": "30000", "clientOrderId": Uuid::new_v4().to_string() },
{ "symbol": "ETHUSDT", "side": "BUY", "type": "LIMIT",
"quantity": "0.01", "price": "1800", "clientOrderId": Uuid::new_v4().to_string() },
]
});
let res = signed_request(reqwest::Method::POST, "/v2/orders/batch", &[], Some(&body)).await?;$res = signed_request('POST', '/v2/orders/batch', [], [
'orders' => [
['symbol' => 'BTCUSDT', 'side' => 'BUY', 'type' => 'LIMIT',
'quantity' => '0.001', 'price' => '30000', 'clientOrderId' => bin2hex(random_bytes(16))],
['symbol' => 'ETHUSDT', 'side' => 'BUY', 'type' => 'LIMIT',
'quantity' => '0.01', 'price' => '1800', 'clientOrderId' => bin2hex(random_bytes(16))],
],
]);GET /v2/orders
List orders for the authenticated account.
| Auth | API_KEY |
Query
symbol(optional)status(optional) —NEW|FILLED|CANCELED| …limit(default 50, max 200)offset(default 0)
Response
{ "ok": true, "orders": [/* Order[] */], "total": 234 }Example
TS=$(($(date +%s) * 1000))
QUERY="limit=50&status=NEW&symbol=BTCUSDT×tamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -H "X-API-KEY: $API_KEY" "$BASE_URL/v2/orders?${QUERY}&signature=${SIG}"const orders = await signedRequest('GET', '/v2/orders', {
params: { symbol: 'BTCUSDT', status: 'NEW', limit: 50 },
});orders = signed_request("GET", "/v2/orders",
params={"symbol": "BTCUSDT", "status": "NEW", "limit": 50}).json()orders, _ := zdex.SignedRequest("GET", "/v2/orders", map[string]string{
"symbol": "BTCUSDT", "status": "NEW", "limit": "50",
}, nil)let orders = signed_request(
reqwest::Method::GET, "/v2/orders",
&[("symbol", "BTCUSDT"), ("status", "NEW"), ("limit", "50")], None,
).await?;$orders = signed_request('GET', '/v2/orders',
['symbol' => 'BTCUSDT', 'status' => 'NEW', 'limit' => 50]);GET /v2/orders/:id
Fetch a single order.
Path — :id must be the backend Order.id (cuid). The venueOrderId
is not accepted on this endpoint; use it via
POST /v2/orders/:id/cancel (which does accept either) or list orders and
match by venueOrderId client-side.
Response
{ "ok": true, "order": { "...": "Order" } }404 if the order belongs to another user or does not exist.
Example
ORDER_ID="cmod1234..."
TS=$(($(date +%s) * 1000)); QUERY="timestamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -H "X-API-KEY: $API_KEY" "$BASE_URL/v2/orders/${ORDER_ID}?${QUERY}&signature=${SIG}"const order = await signedRequest('GET', `/v2/orders/${orderId}`);order = signed_request("GET", f"/v2/orders/{order_id}").json()res, _ := zdex.SignedRequest("GET", "/v2/orders/" + orderID, nil, nil)let order = signed_request(reqwest::Method::GET,
&format!("/v2/orders/{order_id}"), &[], None).await?;$order = signed_request('GET', "/v2/orders/{$orderId}");POST /v2/orders/:id/cancel
Cancel a single order.
| Auth | API_KEY (TRADE) |
Path — :id is Order.id or venueOrderId.
Body (optional)
{ "subAccountId": "..." }Response
{
"ok": true,
"order": { "...": "updated Order, status: CANCELED" },
"venue": { "...": "venue raw cancel response" }
}Side effects — order_canceled WebSocket push.
Example
ORDER_ID="cmod1234..."
TS=$(($(date +%s) * 1000)); QUERY="timestamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -X POST -H "X-API-KEY: $API_KEY" \
"$BASE_URL/v2/orders/${ORDER_ID}/cancel?${QUERY}&signature=${SIG}"const cancel = await signedRequest('POST', `/v2/orders/${orderId}/cancel`);cancel = signed_request("POST", f"/v2/orders/{order_id}/cancel").json()res, _ := zdex.SignedRequest("POST", "/v2/orders/" + orderID + "/cancel", nil, nil)let cancel = signed_request(reqwest::Method::POST,
&format!("/v2/orders/{order_id}/cancel"), &[], None).await?;$cancel = signed_request('POST', "/v2/orders/{$orderId}/cancel");DELETE /v2/orders/open
Cancel all open orders.
| Auth | API_KEY (TRADE) |
Query
symbol(optional) — restrict to one symbol.subAccountId(optional)
Response — at least one open order
{
"ok": true,
"canceled": 12,
"total": 12,
"results": [
{ "orderId": "cmod...", "symbol": "BTCUSDT", "status": "CANCELED" },
{ "orderId": "cmod...", "symbol": "ETHUSDT", "status": "FAILED", "error": "Order not found on venue" }
]
}Response — nothing to cancel
{ "ok": true, "canceled": 0 }When there were no open orders the total and results fields are
omitted entirely. canceled reflects how many actually transitioned to
CANCELED; entries in results[] whose status !== "CANCELED" carry an
error string.
Example
TS=$(($(date +%s) * 1000))
QUERY="symbol=BTCUSDT×tamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -X DELETE -H "X-API-KEY: $API_KEY" \
"$BASE_URL/v2/orders/open?${QUERY}&signature=${SIG}"const res = await signedRequest('DELETE', '/v2/orders/open', {
params: { symbol: 'BTCUSDT' },
});res = signed_request("DELETE", "/v2/orders/open",
params={"symbol": "BTCUSDT"}).json()res, _ := zdex.SignedRequest("DELETE", "/v2/orders/open",
map[string]string{"symbol": "BTCUSDT"}, nil)let res = signed_request(reqwest::Method::DELETE, "/v2/orders/open",
&[("symbol", "BTCUSDT")], None).await?;$res = signed_request('DELETE', '/v2/orders/open', ['symbol' => 'BTCUSDT']);POST /v2/positions/close
Close a position fully or partially.
| Auth | API_KEY (TRADE) |
| Rate limit | 300/min per accountId |
Body
{
"positionId": "cmod...",
"quantity": "0.5"
}quantity is optional — omit it to close the entire position.
Behaviour
- Submits an automatic
MARKET+reduceOnly: trueorder. - Side is inverted automatically (
LONG→SELL,SHORT→BUY). - If the position is already closed on the venue, the call still returns
200with an "already closed" log entry (graceful no-op).
Response — close submitted
{
"ok": true,
"closed": {
"symbol": "BTCUSDT",
"side": "SELL",
"quantity": "0.5",
"remainingPosition": "0"
},
"venue": { "...": "venue raw close response" }
}Response — position already closed on the venue (graceful no-op)
{
"ok": true,
"closed": {
"symbol": "BTCUSDT",
"reconciled": true,
"remainingPosition": "0"
}
}There is no top-level order or position field — the closing market
order is fire-and-forget; subscribe to the WebSocket account channel to
catch the resulting order_new and position_update deltas.
Side effects — order_new + position_update WebSocket pushes.
Example
TS=$(($(date +%s) * 1000)); QUERY="timestamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -X POST -H "X-API-KEY: $API_KEY" -H "Content-Type: application/json" \
-d '{"positionId":"cmod1234..."}' \
"$BASE_URL/v2/positions/close?${QUERY}&signature=${SIG}"const res = await signedRequest('POST', '/v2/positions/close', {
body: { positionId: 'cmod1234...' },
});res = signed_request("POST", "/v2/positions/close",
body={"positionId": "cmod1234..."}).json()res, _ := zdex.SignedRequest("POST", "/v2/positions/close", nil,
map[string]any{"positionId": "cmod1234..."})let res = signed_request(reqwest::Method::POST, "/v2/positions/close",
&[], Some(&json!({ "positionId": "cmod1234..." }))).await?;$res = signed_request('POST', '/v2/positions/close', [],
['positionId' => 'cmod1234...']);GET /v2/futures/open-orders
Live open orders pulled directly from the venue.
| Auth | API_KEY |
Query — symbol (optional), subAccountId (optional).
Response — wrapped envelope around the venue's /fapi/v2/openOrders
payload:
{
"ok": true,
"orders": [
{
"symbol": "BTCUSDT",
"orderId": 12345678,
"side": "BUY",
"type": "LIMIT",
"price": "30000",
"origQty": "0.001",
"executedQty": "0",
"status": "NEW",
"time": 1714123456789
}
]
}The venue array lives under orders — it is not a raw passthrough.
Field names inside each entry follow the venue's snake/camel mix; treat
them as pass-through and don't assume normalisation.
Example
TS=$(($(date +%s) * 1000))
QUERY="symbol=BTCUSDT×tamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -H "X-API-KEY: $API_KEY" "$BASE_URL/v2/futures/open-orders?${QUERY}&signature=${SIG}"const open = await signedRequest('GET', '/v2/futures/open-orders', {
params: { symbol: 'BTCUSDT' },
});open_orders = signed_request("GET", "/v2/futures/open-orders",
params={"symbol": "BTCUSDT"}).json()res, _ := zdex.SignedRequest("GET", "/v2/futures/open-orders",
map[string]string{"symbol": "BTCUSDT"}, nil)let open = signed_request(reqwest::Method::GET, "/v2/futures/open-orders",
&[("symbol", "BTCUSDT")], None).await?;$open = signed_request('GET', '/v2/futures/open-orders', ['symbol' => 'BTCUSDT']);GET /v2/futures/myTrades
Trade history.
| Auth | API_KEY |
Query
symbol(required)subAccountId(optional)fromId(optional) — return trades with id strictly greater than this.
Response
{
"ok": true,
"data": [
{
"id": 12345,
"symbol": "BTCUSDT",
"orderId": "...",
"side": "BUY",
"price": "30000",
"qty": "0.001",
"quoteQty": "30",
"commission": "0.012",
"commissionAsset": "USDT",
"time": 1714123456789,
"isBuyer": true,
"isMaker": false,
"positionSide": "BOTH"
}
]
}Recommended pattern — store the last trade id you received and pass it as
fromId on the next poll. This gives you incremental, gap-free history.
Known limitation — MAKER fills are not returned. This endpoint
forwards the venue's /fapi/v1/userTrades response, which currently only
includes TAKER (aggressive) fills. Fills that came from your own resting
LIMIT orders being matched will not appear here. For complete fill
detection subscribe to the WebSocket account channel and synthesize
fills from position_update deltas, or reconstruct closed-position
realised PnL from GET /v2/futures/positionHistory. See
Known Limitations → MAKER fill visibility.
commissionAsset is overwritten to "USDT" on every entry regardless of
the underlying venue value.
Example
TS=$(($(date +%s) * 1000))
QUERY="fromId=0&symbol=BTCUSDT×tamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -H "X-API-KEY: $API_KEY" "$BASE_URL/v2/futures/myTrades?${QUERY}&signature=${SIG}"const trades = await signedRequest('GET', '/v2/futures/myTrades', {
params: { symbol: 'BTCUSDT', fromId: 0 },
});trades = signed_request("GET", "/v2/futures/myTrades",
params={"symbol": "BTCUSDT", "fromId": 0}).json()res, _ := zdex.SignedRequest("GET", "/v2/futures/myTrades",
map[string]string{"symbol": "BTCUSDT", "fromId": "0"}, nil)let trades = signed_request(reqwest::Method::GET, "/v2/futures/myTrades",
&[("symbol", "BTCUSDT"), ("fromId", "0")], None).await?;$trades = signed_request('GET', '/v2/futures/myTrades',
['symbol' => 'BTCUSDT', 'fromId' => 0]);GET /v2/futures/positionHistory
Closed-position history.
| Auth | API_KEY |
Query
symbol(optional, default = all)subAccountId(optional)page(default 1)limit(default 20, max 1000)startTime,endTime(optional, ms epoch)
Response — venue passthrough; field names are the venue's, not the Zetarium order/position camelCase you see elsewhere:
{
"ok": true,
"data": [
{
"symbol": "BTCUSDT",
"orderSide": 1,
"totalClosedQuantity": "0.1",
"entryPrice": "30000",
"averageClosePrice": "30500",
"realizedPnL": "50",
"fee": "0.6",
"fundingFee": "-0.1",
"openedAt": "...",
"closedAt": "...",
"liquidatedAt": null
}
]
}Field-name pitfalls — this endpoint is a venue passthrough. The shape
diverges from the rest of the REST surface. Parsers written against the
"standard" fields will read null for every value:
| Doc-style name (other endpoints) | This endpoint actually returns |
|---|---|
side: "LONG" | "SHORT" | orderSide: 1 (long) | 2 (short) — integer enum |
avgEntryPrice | entryPrice |
avgClosePrice | averageClosePrice |
realizedPnl | realizedPnL (capital L) |
qty | totalClosedQuantity |
If a normalised wrapper lands later it will be added as a sibling endpoint; this raw shape will continue to be returned for backwards compatibility. See Known Limitations → positionHistory raw shape.
Example
TS=$(($(date +%s) * 1000))
QUERY="limit=20&page=1&symbol=BTCUSDT×tamp=${TS}"
SIG=$(sign "$SECRET" "$QUERY")
curl -H "X-API-KEY: $API_KEY" "$BASE_URL/v2/futures/positionHistory?${QUERY}&signature=${SIG}"const history = await signedRequest('GET', '/v2/futures/positionHistory', {
params: { symbol: 'BTCUSDT', page: 1, limit: 20 },
});history = signed_request("GET", "/v2/futures/positionHistory",
params={"symbol": "BTCUSDT", "page": 1, "limit": 20}).json()res, _ := zdex.SignedRequest("GET", "/v2/futures/positionHistory",
map[string]string{"symbol": "BTCUSDT", "page": "1", "limit": "20"}, nil)let history = signed_request(reqwest::Method::GET, "/v2/futures/positionHistory",
&[("symbol", "BTCUSDT"), ("page", "1"), ("limit", "20")], None).await?;$history = signed_request('GET', '/v2/futures/positionHistory',
['symbol' => 'BTCUSDT', 'page' => 1, 'limit' => 20]);