WebSocket API
The WebSocket API provides real-time streaming of market data, trades, order book updates, and position changes.
Endpoint: ws://{host}:8084/ws
Connecting
Authenticate using your API key via the X-Api-Key header or the apiKey query parameter.
Via Header
const ws = new WebSocket('wss://polymarket.sandbox.playbatman.com/ws', {
headers: { 'X-Api-Key': 'your-api-key' }
});
Via Query Parameter
const ws = new WebSocket('wss://polymarket.sandbox.playbatman.com/ws?apiKey=your-api-key');
Authentication Response
On successful connection, you receive:
{
"type": "AUTHENTICATED",
"payload": {
"clientId": "ws-client-uuid",
"operatorId": "operator-uuid"
}
}
If authentication fails, the connection is closed with code 4001.
Subscribing to Markets
After connecting, subscribe to markets to receive updates.
Subscribe
{
"type": "SUBSCRIBE",
"payload": {
"marketIds": ["market-uuid-1", "market-uuid-2"]
}
}
You can also subscribe to a single market:
{
"type": "SUBSCRIBE",
"payload": {
"marketIds": "market-uuid-1"
}
}
Response:
{
"type": "SUBSCRIBED",
"payload": {
"marketIds": ["market-uuid-1", "market-uuid-2"]
}
}
Unsubscribe
{
"type": "UNSUBSCRIBE",
"payload": {
"marketIds": ["market-uuid-1"]
}
}
Response:
{
"type": "UNSUBSCRIBED",
"payload": {
"marketIds": ["market-uuid-1"]
}
}
Keep-Alive
Send periodic pings to keep the connection alive:
{ "type": "PING" }
Response:
{ "type": "PONG" }
Event Types
Once subscribed to a market, you receive the following event types:
PRICE_UPDATE
Fired when market prices change (after trades or order book changes).
{
"type": "PRICE_UPDATE",
"payload": {
"marketId": "market-uuid",
"prices": [
{
"marketId": "market-uuid",
"outcomeId": "outcome-uuid-1",
"outcomeName": "Yes",
"bestBid": 0.65,
"bestAsk": 0.68,
"lastPrice": 0.66,
"midPrice": 0.665,
"spread": 0.03,
"volume24h": 5000
},
{
"marketId": "market-uuid",
"outcomeId": "outcome-uuid-2",
"outcomeName": "No",
"bestBid": 0.32,
"bestAsk": 0.35,
"lastPrice": 0.34,
"midPrice": 0.335,
"spread": 0.03,
"volume24h": 4800
}
],
"timestamp": 1708293600000
}
}
ORDERBOOK_UPDATE
Fired when the order book for an outcome changes.
{
"type": "ORDERBOOK_UPDATE",
"payload": {
"marketId": "market-uuid",
"outcomeId": "outcome-uuid",
"orderBook": {
"marketId": "market-uuid",
"outcomeId": "outcome-uuid",
"bids": [
{ "price": 0.65, "shares": 150, "orderCount": 3 }
],
"asks": [
{ "price": 0.68, "shares": 75, "orderCount": 2 }
],
"bestBid": 0.65,
"bestAsk": 0.68,
"spread": 0.03,
"timestamp": 1708293600000
},
"timestamp": 1708293600000
}
}
TRADE
Fired when a trade executes in a subscribed market.
{
"type": "TRADE",
"payload": {
"marketId": "market-uuid",
"outcomeId": "outcome-uuid",
"price": 0.66,
"shares": 50,
"side": "BUY",
"timestamp": 1708293600000
}
}
MARKET_STATUS
Fired when a market's status changes (opened, suspended, closed, resolved, etc.).
{
"type": "MARKET_STATUS",
"payload": {
"marketId": "market-uuid",
"status": "RESOLVED",
"timestamp": 1708293600000
}
}
POSITION_UPDATE
Fired when a player's position in a market changes. Only sent to the operator that owns the position.
{
"type": "POSITION_UPDATE",
"payload": {
"marketId": "market-uuid",
"operatorId": "operator-uuid",
"playerId": "player-123",
"position": {
"id": "position-uuid",
"marketId": "market-uuid",
"outcomeId": "outcome-uuid",
"operatorId": "operator-uuid",
"playerId": "player-123",
"shares": "150",
"avgPrice": "0.62",
"totalCost": "93",
"realizedPnl": "0"
},
"timestamp": 1708293600000
}
}
ORDER_UPDATE
Fired when one of your operator's orders is updated (filled, partially filled, cancelled). Only sent to the operator that owns the order.
{
"type": "ORDER_UPDATE",
"payload": {
"marketId": "market-uuid",
"operatorId": "operator-uuid",
"order": {
"id": "order-uuid",
"marketId": "market-uuid",
"outcomeId": "outcome-uuid",
"operatorId": "operator-uuid",
"playerId": "player-123",
"side": "BUY",
"type": "LIMIT",
"shares": "50",
"price": "0.65",
"filledShares": "50",
"avgFillPrice": "0.63",
"remainingShares": "0",
"status": "FILLED",
"transactionId": "txn-001"
},
"timestamp": 1708293600000
}
}
ERROR
Sent when an error occurs processing a message.
{
"type": "ERROR",
"payload": {
"code": "INVALID_MESSAGE",
"message": "Unknown message type: INVALID"
}
}
Complete Example
const WebSocket = require('ws');
const ws = new WebSocket('wss://polymarket.sandbox.playbatman.com/ws?apiKey=your-api-key');
ws.on('open', () => {
console.log('Connected');
});
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.type) {
case 'AUTHENTICATED':
console.log('Authenticated as', message.payload.operatorId);
// Subscribe to a market
ws.send(JSON.stringify({
type: 'SUBSCRIBE',
payload: { marketIds: ['market-uuid'] }
}));
break;
case 'SUBSCRIBED':
console.log('Subscribed to', message.payload.marketIds);
break;
case 'PRICE_UPDATE':
console.log('Price update:', message.payload.prices);
break;
case 'TRADE':
console.log('Trade:', message.payload);
break;
case 'ORDER_UPDATE':
console.log('Order update:', message.payload.order);
break;
case 'POSITION_UPDATE':
console.log('Position update:', message.payload.position);
break;
case 'MARKET_STATUS':
console.log('Market status:', message.payload.status);
break;
case 'PONG':
break;
}
});
// Keep-alive ping every 30 seconds
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'PING' }));
}
}, 30000);
Event Visibility
| Event | Visibility |
|---|---|
PRICE_UPDATE | All subscribers of the market |
ORDERBOOK_UPDATE | All subscribers of the market |
TRADE | All subscribers of the market |
MARKET_STATUS | All subscribers of the market |
POSITION_UPDATE | Only the operator that owns the position |
ORDER_UPDATE | Only the operator that owns the order |
Alternatives for Backend Integration
WebSocket is ideal for browser-based real-time UIs. For backend-to-backend integration, consider these alternatives:
- Webhooks — Receive HTTP POST notifications when events occur. More reliable than maintaining a persistent WebSocket from your server. Includes HMAC-SHA256 signing and automatic retries.
- Event Replay — Fetch missed events via REST. Events are retained for 24 hours with monotonically increasing sequence numbers for reliable pagination.
A recommended pattern for backend integration is to use webhooks for real-time delivery and event replay as a safety net to catch any events missed during webhook downtime.