ZetariumZetariumDex

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.

AuthAPI_KEY (TRADE)
Rate limit300/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 — missing TRADE permission.
  • 409clientOrderId already used by another account.

Side effectsorder_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).

AuthAPI_KEY (TRADE)
Rate limit100/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.

AuthAPI_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&timestamp=${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.

AuthAPI_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 effectsorder_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.

AuthAPI_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&timestamp=${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.

AuthAPI_KEY (TRADE)
Rate limit300/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: true order.
  • Side is inverted automatically (LONGSELL, SHORTBUY).
  • If the position is already closed on the venue, the call still returns 200 with 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 effectsorder_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.

AuthAPI_KEY

Querysymbol (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&timestamp=${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.

AuthAPI_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&timestamp=${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.

AuthAPI_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
avgEntryPriceentryPrice
avgClosePriceaverageClosePrice
realizedPnlrealizedPnL (capital L)
qtytotalClosedQuantity

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&timestamp=${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]);

On this page