The Polymarket WebSocket API is the streaming layer that pushes market data (order books, prices, trades) and private account data (orders, positions, balances) to a connected client instead of requiring repeated REST polling. It is organized around separate market and private endpoints, each with its own subscription types and authentication requirements. This article explains the mental model, shows runnable client code, describes how to verify that a stream is actually live, and covers the failure modes that matter in production. Concrete endpoint and provider choices belong in RPC Assistant; this page focuses on how the streaming integration works and how to measure it.
What the Polymarket WebSocket API Is For
The Polymarket WebSocket API provides real-time streaming data for market information and private user data. Instead of polling REST endpoints on a timer, a client opens a persistent connection and subscribes to the streams it cares about. The server then pushes updates as they occur. This is the right model when you need order book changes, trade notifications, or account state changes with low latency.
The API is split into two logical surfaces. A markets stream carries public data such as full order book snapshots, lightweight price data, and trades. A private stream carries account-scoped data such as order updates, position changes, and balance updates. The private stream requires authentication; the markets stream is public in the documented US API model but may still require API key headers depending on the deployment you are integrating with.
This article assumes you already have a Polymarket API key or SDK credentials, know the market slugs or token identifiers you want to track, and can run a WebSocket client in Node.js or Python. It does not assume any specific hosting provider. If you are still choosing infrastructure for a latency-sensitive bot, the RPC endpoint service overview and dedicated node options are the relevant starting points.
- Use WebSocket when you need continuous order book or trade updates.
- Use REST for one-off lookups, historical data, and reconciliation.
- Keep private and public subscriptions on separate connections.
Connection Model and Endpoint Structure
Documented Polymarket WebSocket deployments expose separate paths for private and market streams. The US documentation describes endpoints under wss://api.polymarket.us/v1/ws/private and wss://api.polymarket.us/v1/ws/markets. Other documentation and community references describe a CLOB subscription endpoint at wss://ws-subscriptions-clob.polymarket.com/ws. Treat the exact host and path as deployment-specific and confirm them against the documentation for the environment you are targeting before writing production code.
The important architectural point is not the literal URL but the separation of concerns. Private streams are authenticated per user and carry order, position, and balance events. Market streams are shared and carry book, price, and trade events. Mixing them on one connection complicates authentication, error handling, and reconnection logic. Keep them separate.
# Conceptual connection layout (confirm exact hosts in current docs)
# Private stream: orders, positions, balances
wss://api.polymarket.us/v1/ws/private
# Market stream: order book, prices, trades
wss://api.polymarket.us/v1/ws/markets
# Handshake headers for authenticated streams
X-PM-Access-Key: <your-api-key-id>
X-PM-Timestamp: <timestamp-in-milliseconds>
X-PM-Signature: <base64-encoded-signature>Authentication and the Signed Handshake
Private WebSocket connections use the same API key authentication model as the REST API. The documented approach passes three headers during the WebSocket handshake: an access key ID, a millisecond timestamp, and a base64-encoded signature. The signature is constructed from the timestamp, the HTTP method, and the request path. For the private stream the method is GET and the path is /v1/ws/private; for the markets stream the path is /v1/ws/markets.
Because the signature includes a timestamp, clock skew is a real failure mode. If your host clock drifts, the server may reject the handshake even though your key is valid. Keep the machine synchronized with NTP and log the timestamp you sent alongside any handshake failure. The signature must be computed over the exact path you connect to, so a trailing slash or a different host path will produce a mismatch.
- Sign
timestamp + "GET" + path, not the full URL. - Use millisecond timestamps and keep the host clock synchronized.
- Never log the secret key or the full signature in production logs.
Subscription Types and Message Format
All WebSocket messages are JSON with snake_case field names. A subscription request includes a unique request_id, a numeric subscription_type, and the market slugs or identifiers you want to track. The documented private subscription types are order updates, position changes, and account balance updates. The documented market subscription types are full market data (order book and stats), lightweight price data, and real-time trade notifications.
Responses echo the request_id and subscription_type so you can correlate a response with the request that produced it. Snapshot responses include an eof flag to indicate the end of the initial snapshot. If a subscription fails, the response includes an error field. Unsubscribing uses the original request_id, which is why unique IDs matter: they are the only reliable way to cancel a specific stream without tearing down the connection.
{
"subscribe": {
"request_id": "book-btc-100k-001",
"subscription_type": 1,
"market_slugs": ["btc-100k-2025"]
}
}A Minimal Node.js Client That Verifies the Stream
The following client connects to the market stream, subscribes to full market data, and prints the first few messages. It is intentionally small so you can confirm connectivity and message shape before building a full ingestion pipeline. Replace the host and path with the values from the documentation for your environment, and replace the slug with a market you actually want to track.
Run it and watch for a response that echoes your request_id. If you see a heartbeat message but no subscription response, your subscription payload is likely malformed. If the connection closes immediately, check the handshake headers and the host path.
import WebSocket from "ws";
const WS_URL = process.env.POLYMARKET_WS_MARKETS; // e.g. wss://api.polymarket.us/v1/ws/markets
const MARKET_SLUG = process.env.MARKET_SLUG || "btc-100k-2025";
const ws = new WebSocket(WS_URL, {
headers: {
"X-PM-Access-Key": process.env.POLYMARKET_KEY_ID,
"X-PM-Timestamp": String(Date.now()),
"X-PM-Signature": process.env.POLYMARKET_SIGNATURE,
},
});
let received = 0;
ws.on("open", () => {
console.log("connected");
ws.send(JSON.stringify({
subscribe: {
request_id: "book-" + MARKET_SLUG,
subscription_type: 1,
market_slugs: [MARKET_SLUG],
},
}));
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.heartbeat) {
console.log("heartbeat");
return;
}
received += 1;
console.log("message", received, Object.keys(msg));
if (received >= 5) ws.close();
});
ws.on("error", (err) => console.error("ws error", err.message));
ws.on("close", (code, reason) => console.log("closed", code, reason.toString()));A Python Client for Order Book and Trade Events
Python clients follow the same pattern: connect, send a subscription payload, and handle messages by type. The example below uses the websockets library and demonstrates the two things most implementations get wrong: responding to heartbeats and distinguishing snapshot messages from incremental updates.
The eof flag in a snapshot response tells you when the initial state has finished loading. Only after eof should you treat subsequent messages as incremental deltas. If you apply deltas before the snapshot completes, your local order book will be inconsistent.
import asyncio, json, os
import websockets
WS_URL = os.environ["POLYMARKET_WS_MARKETS"]
SLUG = os.environ.get("MARKET_SLUG", "btc-100k-2025")
async def main():
headers = {
"X-PM-Access-Key": os.environ["POLYMARKET_KEY_ID"],
"X-PM-Timestamp": str(int(asyncio.get_event_loop().time() * 1000)),
"X-PM-Signature": os.environ["POLYMARKET_SIGNATURE"],
}
async with websockets.connect(WS_URL, extra_headers=headers) as ws:
await ws.send(json.dumps({
"subscribe": {
"request_id": f"book-{SLUG}",
"subscription_type": 1,
"market_slugs": [SLUG],
}
}))
async for raw in ws:
msg = json.loads(raw)
if "heartbeat" in msg:
continue
if "error" in msg:
print("subscription error:", msg["error"])
break
print("keys:", list(msg.keys()))
asyncio.run(main())Verifying That the Stream Is Actually Live
A connection that stays open is not proof that data is flowing. To verify a stream, record a small set of observable signals over a fixed window and compare them against expectations. The table below is a procedure, not a set of measured results: fill in the right-hand column from your own run. Do not publish numbers you have not measured.
| Signal | How to capture it | Expected behavior | Your result |
|---|---|---|---|
| Handshake success | Log connection open event | Opens without error | (fill in) |
| Subscription ack | Match request_id in response | Echoed with same type | (fill in) |
| Snapshot completion | Watch for eof: true | Arrives before deltas | (fill in) |
| Heartbeat cadence | Timestamp each heartbeat | Regular interval | (fill in) |
| Message rate | Count messages per minute | Non-zero during active market | (fill in) |
| Reconnect recovery | Kill connection, observe | Resubscribes and resumes | (fill in) |
Run this procedure against a market that is actively trading. A quiet market can produce long gaps between messages, which looks like a dead stream but is not. If you need a baseline for comparison, run the same procedure at two different times of day and record both.
Reconnection, Heartbeats, and State Recovery
The server sends periodic heartbeat messages to keep the connection alive. Clients should respond to heartbeats or implement their own keep-alive mechanism. In practice, the safest design is to treat every disconnect as a full state reset: reconnect, resubscribe, wait for a fresh snapshot, and only then resume applying deltas. Attempting to patch a stale local order book after a reconnect is a common source of phantom liquidity.
Reconnection should use exponential backoff with jitter. A tight reconnect loop against a rate-limited endpoint will make an outage worse. Log the close code and reason for every disconnect so you can distinguish network failures from authentication failures and subscription errors.
- Reset local state on every reconnect.
- Wait for
eofbefore applying deltas. - Back off with jitter; cap the maximum delay.
- Alert on repeated authentication failures separately from network failures.
Troubleshooting Common Failures
Most integration problems fall into a small number of categories. A handshake that fails immediately usually points to a signature mismatch, a stale timestamp, or a wrong path. A connection that opens but never receives a subscription response usually points to a malformed payload or an unsupported subscription type. A stream that delivers a snapshot but no updates usually means the market is inactive or the subscription is scoped to the wrong identifier.
If you are running the client behind a proxy or load balancer, confirm that WebSocket upgrade headers are forwarded and that idle timeouts are longer than the heartbeat interval. Many production incidents that look like API failures are actually infrastructure timeouts. For broader guidance on node and endpoint reliability, see the RPC endpoints guide and the multi-chain RPC endpoints guide.
- Signature mismatch: verify method, path, and timestamp.
- No subscription response: validate JSON shape and type value.
- Silent stream: confirm the market is active and the slug is correct.
- Frequent disconnects: check proxy idle timeout and heartbeat handling.
Limitations and Tradeoffs
WebSocket streaming is not a replacement for REST. Snapshots can be large, and a full order book subscription for many markets at once will consume memory and bandwidth. Lightweight price subscriptions exist precisely to reduce that cost when you only need top-of-book or last price. Choose the subscription type that matches your decision logic rather than defaulting to the heaviest stream.
Streaming also introduces ordering and consistency questions that polling avoids. You must decide how to handle out-of-order messages, duplicate updates, and gaps after a reconnect. A common pattern is to maintain a local book from the stream and periodically reconcile against a REST snapshot. That reconciliation step is cheap and catches drift before it affects trading decisions.
Finally, the exact endpoint host, path, and authentication details vary by deployment. Do not hardcode a host you found in a blog post. Read the current documentation for the environment you are integrating with, and keep the host in configuration so you can change it without a code deploy.
Next Steps for a Production Integration
Once the minimal client works, the next steps are operational: add structured logging for connection lifecycle events, add metrics for message rate and reconnect count, and add alerts for authentication failures. Keep private and market streams on separate connections with separate retry policies. Persist raw messages for a short window so you can replay a failure.
If your bot also needs on-chain data from Polygon or another network, pair the streaming integration with a reliable RPC layer. The RPC pricing page and the supported networks list describe the options, and the blockchain node hosting guide covers the infrastructure side. For a concrete endpoint recommendation rather than a methodology, use RPC Assistant.
- Add metrics before you add features.
- Reconcile stream state against REST periodically.
- Keep credentials and hosts in configuration, not code.