Overview

Authenticated WebSocket connections enable access to private account data including order updates, balance changes, position updates, and account notifications.
Currently, only the API key is validated on the server. The X-API-Timestamp and X-API-Signature headers are accepted but not enforced yet. HMAC verification is coming soon.

Authentication Requirements

To establish an authenticated connection, you need:
  1. API Key: Your account’s API key with appropriate permissions (required)
  2. Timestamp: Current timestamp in milliseconds (optional for now)
  3. HMAC Signature: Base64-encoded HMAC-SHA256 signature (optional for now)

Connection Headers

Include these headers when establishing the WebSocket connection:
HeaderDescriptionExample
X-API-KeyYour API key"your-api-key-here"
X-API-TimestampCurrent timestamp in milliseconds"1640995200000"
X-API-SignatureBase64-encoded HMAC signature"generated-signature"

HMAC Signature Generation (Coming Soon)

When enforced, the signature will be generated by creating an HMAC-SHA256 hash of the timestamp using your API secret:
function generateHMACSignature(secret, timestamp) {
  const crypto = require('crypto');
  return crypto
    .createHmac('sha256', secret)
    .update(timestamp)
    .digest('base64');
}

// Usage
const timestamp = Date.now().toString();
const signature = generateHMACSignature(apiSecret, timestamp);

Authenticated Connection Examples

JavaScript Example (API key only)

const apiKey = 'your-api-key';
const apiSecret = 'your-api-secret';

function createAuthenticatedConnection() {
  const headers = {
    'X-API-Key': apiKey
  };

  const ws = new WebSocket('wss://ws.roxom.com/ws', [], { headers });

  ws.onopen = function() {
    console.log('🔐 Authenticated connection established');
  };

  ws.onmessage = function(event) {
    const data = JSON.parse(event.data);
    
    // Handle private account data
    switch (data.type) {
      case 'order':
        handleOrderUpdate(data.data);
        break;
      case 'balance':
        handleBalanceUpdate(data.data);
        break;
      case 'position':
        handlePositionUpdate(data.data);
        break;
    }
  };

  return ws;
}

// HMAC signature generation will be required later (see section above)

Python Example

import websocket
import json
import hmac
import hashlib
import base64
import time

class AuthenticatedWebSocketClient:
    def __init__(self, api_key, api_secret):
        self.api_key = api_key
        self.api_secret = api_secret
        self.ws = None

    def connect(self):
        headers = {
            'X-API-Key': self.api_key
        }

        self.ws = websocket.WebSocketApp(
            "wss://ws.roxom.com/ws",
            header=headers,
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close
        )

        self.ws.run_forever()

    def on_open(self, ws):
        print("🔐 Authenticated WebSocket connected")

    def on_message(self, ws, message):
        data = json.loads(message)
        self.handle_private_message(data)

    def on_error(self, ws, error):
        print(f"WebSocket error: {error}")

    def on_close(self, ws, close_status_code, close_msg):
        print("WebSocket connection closed")

    def handle_private_message(self, data):
        message_type = data.get('type')
        
        if message_type == 'order':
            self.handle_order_update(data['data'])
        elif message_type == 'balance':
            self.handle_balance_update(data['data'])
        elif message_type == 'position':
            self.handle_position_update(data['data'])

    def handle_order_update(self, order_data):
        print(f"📋 Order update: {order_data['orderId']} -> {order_data['status']}")

    def handle_balance_update(self, balance_data):
        print(f"💰 Balance update: {balance_data['units']} {balance_data['amount']}")

    def handle_position_update(self, position_data):
        print(f"📊 Position update: {position_data['symbol']} PnL: {position_data['unrealizedPnl']}")

# Usage
client = AuthenticatedWebSocketClient('your-api-key', 'your-api-secret')
client.connect()

cURL Example

For testing authentication, you can use wscat or similar tools:
wscat -c wss://ws.roxom.com/ws \
  --header "X-API-KEY: your-api-key" \
  --header "X-API-TIMESTAMP: $(date +%s000)" \
  --header "X-API-SIGN: $(echo -n $(date +%s000) | openssl dgst -sha256 -hmac 'your-api-secret' -binary | base64)"

Environment Configuration

Environment Variables

Store your credentials securely using environment variables:
# Production
ROXOM_ENV=production
ROXOM_WS_URL=wss://ws.roxom.com/ws
ROXOM_API_KEY=your_production_api_key
ROXOM_API_SECRET=your_production_api_secret

# Sandbox
ROXOM_ENV=sandbox
ROXOM_WS_URL=wss://ws.roxom.io/ws
ROXOM_API_KEY=your_sandbox_api_key
ROXOM_API_SECRET=your_sandbox_api_secret

Error Handling

Authentication Errors

Security Best Practices

Complete Authenticated Client

Here’s a production-ready authenticated WebSocket client:
class SecureRoxomWebSocket {
  constructor(apiKey, apiSecret, environment = 'sandbox') {
    this.apiKey = apiKey;
    this.apiSecret = apiSecret;
    this.environment = environment;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.baseDelay = 1000;
  }

  get wsUrl() {
    return this.environment === 'production' 
      ? 'wss://ws.roxom.com/ws'
      : 'wss://ws.roxom.io/ws';
  }

  generateAuthHeaders() {
    const timestamp = Date.now().toString();
    const signature = this.generateSignature(timestamp);

    return {
      'X-API-KEY': this.apiKey,
      'X-API-TIMESTAMP': timestamp,
      'X-API-SIGN': signature
    };
  }

  generateSignature(timestamp) {
    const crypto = require('crypto');
    return crypto
      .createHmac('sha256', this.apiSecret)
      .update(timestamp)
      .digest('base64');
  }

  async connect() {
    try {
      const headers = this.generateAuthHeaders();
      this.ws = new WebSocket(this.wsUrl, [], { headers });

      this.ws.onopen = () => {
        console.log(`🔐 Authenticated connection to ${this.environment}`);
        this.reconnectAttempts = 0;
        this.onConnected();
      };

      this.ws.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          this.handleMessage(data);
        } catch (error) {
          console.error('Error parsing message:', error);
        }
      };

      this.ws.onclose = (event) => {
        console.log(`Connection closed: ${event.code} ${event.reason}`);
        if (event.code !== 1000) { // Not a normal closure
          this.handleReconnect();
        }
      };

      this.ws.onerror = (error) => {
        console.error('WebSocket error:', error);
      };

    } catch (error) {
      console.error('Connection error:', error);
      this.handleReconnect();
    }
  }

  onConnected() {
    // Override in subclasses or set callback
    console.log('Ready to receive private account data');
  }

  handleMessage(data) {
    if (data.event === 'error') {
      this.handleError(data);
      return;
    }

    switch (data.type) {
      case 'order':
        this.onOrderUpdate?.(data.data);
        break;
      case 'balance':
        this.onBalanceUpdate?.(data.data);
        break;
      case 'position':
        this.onPositionUpdate?.(data.data);
        break;
      default:
        console.log('Unknown message type:', data.type);
    }
  }

  handleError(error) {
    console.error(`WebSocket error ${error.code}: ${error.msg}`);
    
    if (error.code === '600010') { // Authentication failed
      console.error('Authentication failed - check credentials');
      // Don't reconnect on auth failures
      return;
    }
    
    this.handleReconnect();
  }

  handleReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      const delay = this.baseDelay * Math.pow(2, this.reconnectAttempts - 1);
      
      console.log(`🔄 Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
      
      setTimeout(() => {
        this.connect();
      }, delay);
    } else {
      console.error('❌ Max reconnection attempts reached');
    }
  }

  disconnect() {
    if (this.ws) {
      this.ws.close(1000, 'Client disconnect');
      this.ws = null;
    }
  }

  // Event handlers (override or set as callbacks)
  onOrderUpdate(orderData) {
    console.log('Order update:', orderData);
  }

  onBalanceUpdate(balanceData) {
    console.log('Balance update:', balanceData);
  }

  onPositionUpdate(positionData) {
    console.log('Position update:', positionData);
  }
}

// Usage
const client = new SecureRoxomWebSocket(
  process.env.ROXOM_API_KEY,
  process.env.ROXOM_API_SECRET,
  'sandbox'
);

client.connect();

Next Steps