Skip to main content

Overview

The CLOB WebSocket API provides real-time streaming data for market updates and user-specific events. This is essential for building responsive trading applications that react to market changes instantly.

WebSocket Endpoints

The CLOB offers two WebSocket endpoints:
EndpointPurposeAuthentication
/ws/marketMarket data (orderbook, trades, prices)Not required
/ws/userUser-specific events (orders, fills, balances)Required

Prerequisites

  • WebSocket client library (the examples use ws package)
  • CLOB API credentials (for user endpoint only)
  • Token IDs or condition IDs to subscribe to
Install the WebSocket library:
npm install ws
npm install --save-dev @types/ws

Market Data Streaming

Subscribe to public market data without authentication:

Basic Market Connection

import { WebSocket } from "ws";

const YES_TOKEN = "71321045679252212594626385532706912750332728571942532289631379312455583992563";
const NO_TOKEN = "52114319501245915516055106046884209969926127482827954674443846427813813222426";

const host = process.env.WS_URL || "wss://ws-subscriptions-clob.polymarket.com";
const ws = new WebSocket(`${host}/ws/market`);

const subscriptionMessage = {
  type: "market",
  assets_ids: [YES_TOKEN, NO_TOKEN],
  markets: [],
  initial_dump: true, // Get current state on connection
};

ws.on("open", () => {
  console.log("Connected to market WebSocket");
  
  // Send subscription message
  ws.send(JSON.stringify(subscriptionMessage));
  
  // Send periodic pings to keep connection alive
  setInterval(() => {
    console.log("Sending ping...");
    ws.send("PING");
  }, 50000); // Every 50 seconds
});

ws.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  console.log("Market update:", data);
};

ws.on("error", (err) => {
  console.error("WebSocket error:", err);
});

ws.on("close", (code, reason) => {
  console.log(`Disconnected: ${code} - ${reason.toString()}`);
});

Market Data Events

The market WebSocket streams several event types:
Sent when the orderbook changes:
{
  "event_type": "book",
  "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  "timestamp": "2024-03-04T12:00:00Z",
  "market": "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1",
  "bids": [
    {"price": "0.52", "size": "1000"},
    {"price": "0.51", "size": "2500"}
  ],
  "asks": [
    {"price": "0.53", "size": "800"},
    {"price": "0.54", "size": "1500"}
  ]
}
Sent when trades are executed:
{
  "event_type": "trade",
  "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  "timestamp": "2024-03-04T12:00:15Z",
  "market": "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1",
  "price": "0.52",
  "size": "100",
  "side": "BUY",
  "trade_id": "0xabc123..."
}
Sent when last trade price changes:
{
  "event_type": "last_price",
  "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  "timestamp": "2024-03-04T12:00:15Z",
  "price": "0.52"
}

User Event Streaming

Subscribe to your personal trading events with authentication:

Authenticated User Connection

import { WebSocket } from "ws";
import { ApiKeyCreds } from "@polymarket/clob-client";

const CONDITION_ID = "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1";

const creds: ApiKeyCreds = {
  key: process.env.CLOB_API_KEY!,
  secret: process.env.CLOB_SECRET!,
  passphrase: process.env.CLOB_PASS_PHRASE!,
};

const host = process.env.WS_URL || "wss://ws-subscriptions-clob.polymarket.com";
const ws = new WebSocket(`${host}/ws/user`);

const subscriptionMessage = {
  auth: {
    apiKey: creds.key,
    secret: creds.secret,
    passphrase: creds.passphrase,
  },
  type: "user",
  markets: [CONDITION_ID],
  assets_ids: [],
  initial_dump: true,
};

ws.on("open", () => {
  console.log("Connected to user WebSocket");
  ws.send(JSON.stringify(subscriptionMessage));
  
  // Keep-alive ping
  setInterval(() => {
    ws.send("PING");
  }, 50000);
});

ws.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  console.log("User event:", data);
};

ws.on("error", (err) => {
  console.error("WebSocket error:", err);
});

ws.on("close", (code, reason) => {
  console.log(`Disconnected: ${code} - ${reason.toString()}`);
});

User Event Types

Sent when your order status changes:
{
  "event_type": "order",
  "timestamp": "2024-03-04T12:00:20Z",
  "order_id": "0xdef456...",
  "market": "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1",
  "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  "status": "MATCHED",
  "side": "BUY",
  "price": "0.52",
  "original_size": "100",
  "size_matched": "100",
  "type": "GTC"
}
Possible statuses: LIVE, MATCHED, CANCELLED, PARTIALLY_FILLED
Sent when your order is (partially) filled:
{
  "event_type": "fill",
  "timestamp": "2024-03-04T12:00:20Z",
  "order_id": "0xdef456...",
  "market": "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1",
  "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  "trade_id": "0xghi789...",
  "side": "BUY",
  "price": "0.52",
  "size": "100",
  "fee": "0.10",
  "is_maker": false
}
Sent when your balances change:
{
  "event_type": "balance",
  "timestamp": "2024-03-04T12:00:20Z",
  "asset_id": "USDC",
  "available": "9948.00",
  "locked": "52.00",
  "total": "10000.00"
}

Complete Example

Here’s a production-ready WebSocket client with reconnection logic:
import { WebSocket } from "ws";
import { ApiKeyCreds } from "@polymarket/clob-client";
import { config as dotenvConfig } from "dotenv";

dotenvConfig();

interface SubscriptionMessage {
  auth?: {
    apiKey: string;
    secret: string;
    passphrase: string;
  };
  type: "market" | "user";
  markets: string[];
  assets_ids: string[];
  initial_dump?: boolean;
}

class ClobWebSocket {
  private ws: WebSocket | null = null;
  private pingInterval: NodeJS.Timeout | null = null;
  private reconnectTimeout: NodeJS.Timeout | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  
  constructor(
    private host: string,
    private type: "market" | "user",
    private subscription: SubscriptionMessage,
  ) {}
  
  connect() {
    const endpoint = `${this.host}/ws/${this.type}`;
    console.log(`Connecting to ${endpoint}...`);
    
    this.ws = new WebSocket(endpoint);
    
    this.ws.on("open", () => {
      console.log(`✓ Connected to ${this.type} WebSocket`);
      this.reconnectAttempts = 0;
      
      // Send subscription
      this.ws!.send(JSON.stringify(this.subscription));
      
      // Start ping interval
      this.pingInterval = setInterval(() => {
        if (this.ws?.readyState === WebSocket.OPEN) {
          this.ws.send("PING");
        }
      }, 50000);
    });
    
    this.ws.onmessage = (msg) => {
      if (msg.data === "PONG") {
        return; // Ignore pong responses
      }
      
      try {
        const data = JSON.parse(msg.data.toString());
        this.handleMessage(data);
      } catch (error) {
        console.error("Failed to parse message:", error);
      }
    };
    
    this.ws.on("error", (err) => {
      console.error("WebSocket error:", err.message);
    });
    
    this.ws.on("close", (code, reason) => {
      console.log(`Disconnected: ${code} - ${reason.toString()}`);
      this.cleanup();
      this.attemptReconnect();
    });
  }
  
  private handleMessage(data: any) {
    switch (data.event_type) {
      case "book":
        console.log(`Orderbook update for ${data.asset_id}`);
        console.log(`  Best bid: ${data.bids[0]?.price}`);
        console.log(`  Best ask: ${data.asks[0]?.price}`);
        break;
        
      case "trade":
        console.log(`Trade: ${data.side} ${data.size} @ ${data.price}`);
        break;
        
      case "order":
        console.log(`Order ${data.order_id}: ${data.status}`);
        break;
        
      case "fill":
        console.log(`Fill: ${data.side} ${data.size} @ ${data.price}`);
        console.log(`  Fee: ${data.fee} | Maker: ${data.is_maker}`);
        break;
        
      default:
        console.log("Event:", data);
    }
  }
  
  private cleanup() {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }
  
  private attemptReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error("Max reconnection attempts reached. Giving up.");
      return;
    }
    
    this.reconnectAttempts++;
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    
    console.log(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})...`);
    
    this.reconnectTimeout = setTimeout(() => {
      this.connect();
    }, delay);
  }
  
  disconnect() {
    this.cleanup();
    
    if (this.reconnectTimeout) {
      clearTimeout(this.reconnectTimeout);
      this.reconnectTimeout = null;
    }
    
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// Usage
async function main() {
  const host = process.env.WS_URL || "wss://ws-subscriptions-clob.polymarket.com";
  
  // Market data stream
  const marketWs = new ClobWebSocket(
    host,
    "market",
    {
      type: "market",
      assets_ids: [
        "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "52114319501245915516055106046884209969926127482827954674443846427813813222426",
      ],
      markets: [],
      initial_dump: true,
    }
  );
  
  marketWs.connect();
  
  // User event stream
  const creds: ApiKeyCreds = {
    key: process.env.CLOB_API_KEY!,
    secret: process.env.CLOB_SECRET!,
    passphrase: process.env.CLOB_PASS_PHRASE!,
  };
  
  const userWs = new ClobWebSocket(
    host,
    "user",
    {
      auth: {
        apiKey: creds.key,
        secret: creds.secret,
        passphrase: creds.passphrase,
      },
      type: "user",
      markets: ["0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1"],
      assets_ids: [],
      initial_dump: true,
    }
  );
  
  userWs.connect();
  
  // Graceful shutdown
  process.on("SIGINT", () => {
    console.log("\nShutting down...");
    marketWs.disconnect();
    userWs.disconnect();
    process.exit(0);
  });
}

main();

Subscription Options

Subscribe by Token IDs

Receive updates for specific outcome tokens:
const subscription = {
  type: "market",
  assets_ids: [
    "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  ],
  markets: [],
  initial_dump: true,
};

Subscribe by Market/Condition

Receive updates for all tokens in a market:
const subscription = {
  type: "user",
  markets: ["0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1"],
  assets_ids: [],
  initial_dump: true,
};

Initial Dump

Set initial_dump: true to receive the current state immediately upon connection:
const subscription = {
  type: "market",
  assets_ids: [YES_TOKEN],
  markets: [],
  initial_dump: true, // Get current orderbook immediately
};

Best Practices

Keep-Alive Pings

Send PING every 50 seconds to prevent connection timeout.

Reconnection Logic

Implement exponential backoff for reconnection attempts.

Error Handling

Handle malformed messages and connection errors gracefully.

Subscription Limits

Limit subscriptions to necessary assets to reduce bandwidth.

Common Issues

Problem: WebSocket disconnects after 60 seconds.Solution: Send PING messages every 50 seconds:
setInterval(() => ws.send("PING"), 50000);
Problem: User WebSocket rejects connection.Solution: Verify your API credentials are correct and active:
const creds = {
  key: process.env.CLOB_API_KEY,
  secret: process.env.CLOB_SECRET,
  passphrase: process.env.CLOB_PASS_PHRASE,
};
Problem: Connected but not receiving updates.Solution: Ensure you’re subscribed to active markets and your subscription message is correct. Set initial_dump: true to test.

Next Steps

Market Data Guide

Learn to interpret and use market data

Order Management

React to order events and manage positions

Basic Orders

Create orders programmatically

Trading Concepts

Understand CLOB trading mechanics

Build docs developers (and LLMs) love