Orders Channel

Private order status updates and fills via WebSocket

Orders Channel

The orders channel provides real-time updates for your private orders including status changes, partial fills, and completions. Authentication is required.

Authentication Required

You must authenticate before subscribing to the orders channel:

{
  "id": "auth-001",
  "type": "authenticate",
  "data": {
    "api_key": "your_api_key",
    "timestamp": 1702339200000,
    "signature": "hmac_sha256_signature"
  }
}

Subscribe

{
  "id": "sub-001",
  "type": "subscribe",
  "channel": "orders",
  "data": {
    "symbol": "BTC-USDT"
  }
}
ParameterTypeRequiredDescription
symbolstringNoFilter by symbol. Omit to receive all order updates.

Subscribe to all orders:

{
  "id": "sub-001",
  "type": "subscribe",
  "channel": "orders",
  "data": {}
}

Message Flow

┌──────────┐                           ┌──────────┐
│  Client  │                           │  Server  │
└──────────┘                           └──────────┘
     │                                       │
     │ ──── authenticate ──────────────────► │
     │ ◄─── auth_success ────────────────── │
     │                                       │
     │ ──── subscribe (orders) ────────────► │
     │ ◄─── subscribed ────────────────────  │
     │                                       │
     │ ◄─── orders_snapshot ─────────────── │  Open orders
     │                                       │
     │ ──── place_order ───────────────────► │  Client places order
     │ ◄─── order_accepted ────────────────  │  Order received
     │ ◄─── order_open ────────────────────  │  Order on book
     │                                       │
     │ ◄─── order_fill ────────────────────  │  Partial fill
     │ ◄─── order_fill ────────────────────  │  More fills
     │ ◄─── order_done ────────────────────  │  Order completed
     │                                       │

Orders Snapshot

Upon subscription, receive all open orders:

{
  "type": "orders_snapshot",
  "channel": "orders",
  "data": {
    "orders": [
      {
        "order_id": "o-123e4567-e89b-12d3-a456-426614174000",
        "client_order_id": "my-order-001",
        "symbol": "BTC-USDT",
        "side": "buy",
        "type": "limit",
        "price": 49500.00,
        "size": 1.0000,
        "filled_size": 0.2500,
        "remaining_size": 0.7500,
        "status": "open",
        "time_in_force": "GTC",
        "post_only": false,
        "reduce_only": false,
        "created_at": 1702339100000,
        "updated_at": 1702339150000
      }
    ]
  },
  "sequence": 1000,
  "timestamp": 1702339200000
}

Order Status Messages

Order Accepted

Order received and validated:

{
  "type": "order_accepted",
  "channel": "orders",
  "data": {
    "order_id": "o-123e4567-e89b-12d3-a456-426614174000",
    "client_order_id": "my-order-001",
    "symbol": "BTC-USDT",
    "side": "buy",
    "type": "limit",
    "price": 50000.00,
    "size": 1.0000,
    "time_in_force": "GTC",
    "post_only": false,
    "created_at": 1702339200100
  },
  "sequence": 1001,
  "timestamp": 1702339200100
}

Order Open

Order placed on the order book:

{
  "type": "order_open",
  "channel": "orders",
  "data": {
    "order_id": "o-123e4567-e89b-12d3-a456-426614174000",
    "client_order_id": "my-order-001",
    "symbol": "BTC-USDT",
    "status": "open",
    "price": 50000.00,
    "size": 1.0000,
    "filled_size": 0,
    "remaining_size": 1.0000
  },
  "sequence": 1002,
  "timestamp": 1702339200150
}

Order Fill

Order partially or fully filled:

{
  "type": "order_fill",
  "channel": "orders",
  "data": {
    "order_id": "o-123e4567-e89b-12d3-a456-426614174000",
    "client_order_id": "my-order-001",
    "symbol": "BTC-USDT",
    "trade_id": "t-1702339200-001",
    "side": "buy",
    "price": 50000.00,
    "fill_price": 49999.50,
    "fill_size": 0.2500,
    "fill_fee": 0.00125,
    "fee_currency": "USDT",
    "filled_size": 0.2500,
    "remaining_size": 0.7500,
    "liquidity": "taker",
    "timestamp": 1702339200200
  },
  "sequence": 1003,
  "timestamp": 1702339200200
}
FieldTypeDescription
fill_pricenumberActual execution price
fill_sizenumberSize of this fill
fill_feenumberFee for this fill
fee_currencystringCurrency of the fee
liquiditystringmaker or taker
filled_sizenumberCumulative filled size
remaining_sizenumberRemaining unfilled size

Order Done

Order completed (filled or cancelled):

{
  "type": "order_done",
  "channel": "orders",
  "data": {
    "order_id": "o-123e4567-e89b-12d3-a456-426614174000",
    "client_order_id": "my-order-001",
    "symbol": "BTC-USDT",
    "status": "filled",
    "reason": "filled",
    "side": "buy",
    "type": "limit",
    "price": 50000.00,
    "size": 1.0000,
    "filled_size": 1.0000,
    "remaining_size": 0,
    "avg_fill_price": 49999.75,
    "total_fees": 0.005,
    "fee_currency": "USDT",
    "created_at": 1702339200100,
    "done_at": 1702339200500
  },
  "sequence": 1004,
  "timestamp": 1702339200500
}

Status values:

StatusDescription
filledFully filled
cancelledUser cancelled
expiredTime-in-force expired
rejectedOrder rejected

Reason values:

ReasonDescription
filledOrder fully filled
user_cancelledUser requested cancel
self_trade_preventionWould have matched own order
post_only_would_takePost-only order would take liquidity
insufficient_balanceBalance no longer sufficient
ioc_incompleteIOC order not fully filled
fok_incompleteFOK order not fully filled
gtd_expiredGTD order expired

Order Rejected

Order failed validation:

{
  "type": "order_rejected",
  "channel": "orders",
  "data": {
    "client_order_id": "my-order-001",
    "symbol": "BTC-USDT",
    "side": "buy",
    "type": "limit",
    "price": 50000.00,
    "size": 1000.0000,
    "reason": "INSUFFICIENT_BALANCE",
    "message": "Insufficient USDT balance. Required: 50000000, Available: 10000",
    "timestamp": 1702339200100
  },
  "sequence": 1005,
  "timestamp": 1702339200100
}

Placing Orders via WebSocket

Limit Order

{
  "id": "req-001",
  "type": "place_order",
  "data": {
    "client_order_id": "my-order-001",
    "symbol": "BTC-USDT",
    "side": "buy",
    "type": "limit",
    "price": 50000.00,
    "size": 1.0,
    "time_in_force": "GTC",
    "post_only": false
  }
}

Market Order

{
  "id": "req-002",
  "type": "place_order",
  "data": {
    "client_order_id": "my-order-002",
    "symbol": "BTC-USDT",
    "side": "buy",
    "type": "market",
    "size": 1.0
  }
}

Order Parameters

ParameterTypeRequiredDescription
client_order_idstringNoYour custom order ID (max 36 chars)
symbolstringYesTrading pair
sidestringYesbuy or sell
typestringYeslimit, market, stop_limit, stop_market
pricenumberLimit ordersLimit price
sizenumberYesOrder size
time_in_forcestringNoGTC, IOC, FOK, GTD (default: GTC)
post_onlybooleanNoOnly make liquidity (default: false)
reduce_onlybooleanNoOnly reduce position (default: false)
stop_pricenumberStop ordersTrigger price

Time in Force

ValueDescription
GTCGood Till Cancelled
IOCImmediate Or Cancel
FOKFill Or Kill
GTDGood Till Date (requires expire_time)

Cancelling Orders

Cancel Single Order

{
  "id": "req-003",
  "type": "cancel_order",
  "data": {
    "order_id": "o-123e4567-e89b-12d3-a456-426614174000"
  }
}

Or by client order ID:

{
  "id": "req-003",
  "type": "cancel_order",
  "data": {
    "client_order_id": "my-order-001"
  }
}

Cancel All Orders

{
  "id": "req-004",
  "type": "cancel_all_orders",
  "data": {
    "symbol": "BTC-USDT"
  }
}

Omit symbol to cancel all orders across all symbols.

Cancel Response

{
  "id": "req-003",
  "type": "order_cancel_accepted",
  "data": {
    "order_id": "o-123e4567-e89b-12d3-a456-426614174000",
    "client_order_id": "my-order-001"
  },
  "timestamp": 1702339200300
}

Code Examples

TypeScript Order Manager

interface Order {
  orderId: string;
  clientOrderId?: string;
  symbol: string;
  side: 'buy' | 'sell';
  type: string;
  price?: number;
  size: number;
  filledSize: number;
  remainingSize: number;
  status: string;
  avgFillPrice?: number;
  totalFees: number;
  createdAt: number;
  updatedAt: number;
}

interface Fill {
  tradeId: string;
  orderId: string;
  fillPrice: number;
  fillSize: number;
  fee: number;
  liquidity: 'maker' | 'taker';
  timestamp: number;
}

class OrderManager {
  private ws: WebSocket;
  private orders: Map<string, Order> = new Map();
  private fills: Map<string, Fill[]> = new Map();
  private pendingRequests: Map<string, {
    resolve: (value: any) => void;
    reject: (error: any) => void;
    timeout: NodeJS.Timeout;
  }> = new Map();

  private onOrderUpdate: ((order: Order) => void) | null = null;
  private onFill: ((fill: Fill) => void) | null = null;

  constructor(ws: WebSocket) {
    this.ws = ws;
    ws.onmessage = (event) => this.handleMessage(JSON.parse(event.data));
  }

  setOrderUpdateHandler(handler: (order: Order) => void) {
    this.onOrderUpdate = handler;
  }

  setFillHandler(handler: (fill: Fill) => void) {
    this.onFill = handler;
  }

  async placeOrder(params: {
    clientOrderId?: string;
    symbol: string;
    side: 'buy' | 'sell';
    type: 'limit' | 'market';
    price?: number;
    size: number;
    timeInForce?: string;
    postOnly?: boolean;
  }): Promise<Order> {
    const requestId = `place-${Date.now()}-${Math.random().toString(36).slice(2)}`;

    return this.sendRequest(requestId, {
      type: 'place_order',
      data: {
        client_order_id: params.clientOrderId,
        symbol: params.symbol,
        side: params.side,
        type: params.type,
        price: params.price,
        size: params.size,
        time_in_force: params.timeInForce || 'GTC',
        post_only: params.postOnly || false
      }
    });
  }

  async cancelOrder(orderId: string): Promise<void> {
    const requestId = `cancel-${Date.now()}`;

    return this.sendRequest(requestId, {
      type: 'cancel_order',
      data: { order_id: orderId }
    });
  }

  async cancelAllOrders(symbol?: string): Promise<void> {
    const requestId = `cancel-all-${Date.now()}`;

    return this.sendRequest(requestId, {
      type: 'cancel_all_orders',
      data: symbol ? { symbol } : {}
    });
  }

  getOrder(orderId: string): Order | undefined {
    return this.orders.get(orderId);
  }

  getOpenOrders(symbol?: string): Order[] {
    const orders = [...this.orders.values()].filter(o =>
      o.status === 'open' || o.status === 'pending'
    );

    if (symbol) {
      return orders.filter(o => o.symbol === symbol);
    }
    return orders;
  }

  getFills(orderId: string): Fill[] {
    return this.fills.get(orderId) || [];
  }

  private sendRequest<T>(id: string, message: any): Promise<T> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pendingRequests.delete(id);
        reject(new Error('Request timeout'));
      }, 10000);

      this.pendingRequests.set(id, { resolve, reject, timeout });
      this.ws.send(JSON.stringify({ id, ...message }));
    });
  }

  private handleMessage(msg: any) {
    // Handle request responses
    if (msg.id && this.pendingRequests.has(msg.id)) {
      const pending = this.pendingRequests.get(msg.id)!;
      clearTimeout(pending.timeout);
      this.pendingRequests.delete(msg.id);

      if (msg.type.includes('error') || msg.type.includes('rejected')) {
        pending.reject(new Error(msg.data?.message || 'Request failed'));
      } else {
        pending.resolve(msg.data);
      }
      return;
    }

    // Handle order updates
    switch (msg.type) {
      case 'orders_snapshot':
        for (const o of msg.data.orders) {
          this.updateOrder(this.parseOrder(o));
        }
        break;

      case 'order_accepted':
      case 'order_open':
      case 'order_done':
        this.updateOrder(this.parseOrder(msg.data));
        break;

      case 'order_fill':
        this.handleFill(msg.data);
        break;

      case 'order_rejected':
        if (this.onOrderUpdate) {
          this.onOrderUpdate({
            ...this.parseOrder(msg.data),
            status: 'rejected'
          });
        }
        break;
    }
  }

  private parseOrder(data: any): Order {
    return {
      orderId: data.order_id,
      clientOrderId: data.client_order_id,
      symbol: data.symbol,
      side: data.side,
      type: data.type,
      price: data.price,
      size: data.size,
      filledSize: data.filled_size || 0,
      remainingSize: data.remaining_size || data.size,
      status: data.status || 'pending',
      avgFillPrice: data.avg_fill_price,
      totalFees: data.total_fees || 0,
      createdAt: data.created_at || Date.now(),
      updatedAt: data.updated_at || data.done_at || Date.now()
    };
  }

  private updateOrder(order: Order) {
    this.orders.set(order.orderId, order);
    if (this.onOrderUpdate) {
      this.onOrderUpdate(order);
    }
  }

  private handleFill(data: any) {
    const fill: Fill = {
      tradeId: data.trade_id,
      orderId: data.order_id,
      fillPrice: data.fill_price,
      fillSize: data.fill_size,
      fee: data.fill_fee,
      liquidity: data.liquidity,
      timestamp: data.timestamp
    };

    // Store fill
    if (!this.fills.has(fill.orderId)) {
      this.fills.set(fill.orderId, []);
    }
    this.fills.get(fill.orderId)!.push(fill);

    // Update order
    const order = this.orders.get(data.order_id);
    if (order) {
      order.filledSize = data.filled_size;
      order.remainingSize = data.remaining_size;
      order.updatedAt = data.timestamp;
      this.updateOrder(order);
    }

    if (this.onFill) {
      this.onFill(fill);
    }
  }
}

// Usage
const ws = new WebSocket('wss://api.lux.network/ws');
const orderManager = new OrderManager(ws);

orderManager.setOrderUpdateHandler((order) => {
  console.log(`Order ${order.orderId}: ${order.status}`);
});

orderManager.setFillHandler((fill) => {
  console.log(`Fill: ${fill.fillSize} @ ${fill.fillPrice}`);
});

// Place order
const order = await orderManager.placeOrder({
  clientOrderId: 'my-order-001',
  symbol: 'BTC-USDT',
  side: 'buy',
  type: 'limit',
  price: 50000,
  size: 0.1
});

Python Order Manager

import asyncio
import websockets
import json
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional
import time

@dataclass
class Order:
    order_id: str
    symbol: str
    side: str
    type: str
    price: Optional[float]
    size: float
    filled_size: float = 0.0
    remaining_size: float = 0.0
    status: str = "pending"
    client_order_id: Optional[str] = None
    avg_fill_price: Optional[float] = None
    total_fees: float = 0.0
    created_at: int = 0
    updated_at: int = 0

@dataclass
class Fill:
    trade_id: str
    order_id: str
    fill_price: float
    fill_size: float
    fee: float
    liquidity: str
    timestamp: int

class OrderManager:
    def __init__(self, ws):
        self.ws = ws
        self.orders: Dict[str, Order] = {}
        self.fills: Dict[str, List[Fill]] = {}
        self.pending_requests: Dict[str, asyncio.Future] = {}
        self.on_order_update: Optional[Callable[[Order], None]] = None
        self.on_fill: Optional[Callable[[Fill], None]] = None

    async def start(self):
        """Start processing messages."""
        asyncio.create_task(self._message_loop())

    async def _message_loop(self):
        async for message in self.ws:
            msg = json.loads(message)
            await self._handle_message(msg)

    async def place_order(
        self,
        symbol: str,
        side: str,
        order_type: str,
        size: float,
        price: Optional[float] = None,
        client_order_id: Optional[str] = None,
        time_in_force: str = "GTC",
        post_only: bool = False
    ) -> Order:
        """Place a new order."""
        request_id = f"place-{int(time.time() * 1000)}"

        data = {
            "symbol": symbol,
            "side": side,
            "type": order_type,
            "size": size,
            "time_in_force": time_in_force,
            "post_only": post_only
        }
        if price is not None:
            data["price"] = price
        if client_order_id:
            data["client_order_id"] = client_order_id

        return await self._send_request(request_id, "place_order", data)

    async def cancel_order(self, order_id: str) -> None:
        """Cancel an order."""
        request_id = f"cancel-{int(time.time() * 1000)}"
        await self._send_request(request_id, "cancel_order", {"order_id": order_id})

    async def cancel_all_orders(self, symbol: Optional[str] = None) -> None:
        """Cancel all orders."""
        request_id = f"cancel-all-{int(time.time() * 1000)}"
        data = {"symbol": symbol} if symbol else {}
        await self._send_request(request_id, "cancel_all_orders", data)

    def get_open_orders(self, symbol: Optional[str] = None) -> List[Order]:
        """Get all open orders."""
        orders = [o for o in self.orders.values() if o.status in ("open", "pending")]
        if symbol:
            orders = [o for o in orders if o.symbol == symbol]
        return orders

    async def _send_request(self, request_id: str, msg_type: str, data: dict):
        future = asyncio.Future()
        self.pending_requests[request_id] = future

        await self.ws.send(json.dumps({
            "id": request_id,
            "type": msg_type,
            "data": data
        }))

        try:
            return await asyncio.wait_for(future, timeout=10.0)
        except asyncio.TimeoutError:
            self.pending_requests.pop(request_id, None)
            raise TimeoutError("Request timed out")

    async def _handle_message(self, msg: dict):
        # Handle request responses
        request_id = msg.get("id")
        if request_id and request_id in self.pending_requests:
            future = self.pending_requests.pop(request_id)
            if "error" in msg.get("type", "") or "rejected" in msg.get("type", ""):
                future.set_exception(Exception(msg.get("data", {}).get("message", "Request failed")))
            else:
                future.set_result(msg.get("data"))
            return

        # Handle order updates
        msg_type = msg.get("type")

        if msg_type == "orders_snapshot":
            for o in msg["data"]["orders"]:
                self._update_order(self._parse_order(o))

        elif msg_type in ("order_accepted", "order_open", "order_done"):
            self._update_order(self._parse_order(msg["data"]))

        elif msg_type == "order_fill":
            self._handle_fill(msg["data"])

        elif msg_type == "order_rejected":
            order = self._parse_order(msg["data"])
            order.status = "rejected"
            if self.on_order_update:
                self.on_order_update(order)

    def _parse_order(self, data: dict) -> Order:
        return Order(
            order_id=data["order_id"],
            client_order_id=data.get("client_order_id"),
            symbol=data["symbol"],
            side=data["side"],
            type=data["type"],
            price=data.get("price"),
            size=data["size"],
            filled_size=data.get("filled_size", 0),
            remaining_size=data.get("remaining_size", data["size"]),
            status=data.get("status", "pending"),
            avg_fill_price=data.get("avg_fill_price"),
            total_fees=data.get("total_fees", 0),
            created_at=data.get("created_at", int(time.time() * 1000)),
            updated_at=data.get("updated_at", data.get("done_at", int(time.time() * 1000)))
        )

    def _update_order(self, order: Order):
        self.orders[order.order_id] = order
        if self.on_order_update:
            self.on_order_update(order)

    def _handle_fill(self, data: dict):
        fill = Fill(
            trade_id=data["trade_id"],
            order_id=data["order_id"],
            fill_price=data["fill_price"],
            fill_size=data["fill_size"],
            fee=data["fill_fee"],
            liquidity=data["liquidity"],
            timestamp=data["timestamp"]
        )

        # Store fill
        if fill.order_id not in self.fills:
            self.fills[fill.order_id] = []
        self.fills[fill.order_id].append(fill)

        # Update order
        if fill.order_id in self.orders:
            order = self.orders[fill.order_id]
            order.filled_size = data["filled_size"]
            order.remaining_size = data["remaining_size"]
            order.updated_at = data["timestamp"]
            self._update_order(order)

        if self.on_fill:
            self.on_fill(fill)


# Usage
async def main():
    async with websockets.connect("wss://api.lux.network/ws") as ws:
        # Authenticate first...

        manager = OrderManager(ws)

        def on_order(order: Order):
            print(f"Order {order.order_id}: {order.status}")

        def on_fill(fill: Fill):
            print(f"Fill: {fill.fill_size} @ {fill.fill_price}")

        manager.on_order_update = on_order
        manager.on_fill = on_fill

        await manager.start()

        # Place order
        order = await manager.place_order(
            symbol="BTC-USDT",
            side="buy",
            order_type="limit",
            price=50000,
            size=0.1,
            client_order_id="my-order-001"
        )
        print(f"Placed: {order}")

asyncio.run(main())

Error Codes

CodeDescription
INSUFFICIENT_BALANCENot enough balance
INVALID_PRICEPrice outside allowed range
INVALID_SIZESize below minimum or above maximum
INVALID_SYMBOLSymbol not found
ORDER_NOT_FOUNDOrder ID not found
POST_ONLY_WOULD_TAKEPost-only order would take liquidity
RATE_LIMIT_EXCEEDEDToo many orders
SELF_TRADE_PREVENTIONOrder would match own order

Rate Limits

OperationLimit
Place orders10/second
Cancel orders50/second
Cancel all1/second

Next Steps