API · Reference
Quick answer (updated 2026-10-25)
For order books, connect to wss://ws-subscriptions-clob.polymarket.com/ws/market and send {"assets_ids": ["<token_id>"], "type": "market"} (the key is assets_ids, with an s). For your own orders and fills, connect to /ws/user on the same host and send your CLOB API key, secret and passphrase with "type": "user". On both, send the text PING every 10 seconds; the server answers PONG. RTDS (wss://ws-live-data.polymarket.com) is now legacy: crypto and equity prices moved to PolyBolt (wss://ws-live-v2.polymarket.com/ws, which needs CLOB API credentials), while RTDS still carries the public activity trade stream.
Polymarket runs several WebSocket services on different hosts, and mixing them up is the most common mistake. This page lists every endpoint with its subscribe message and heartbeat, then shows working Python and TypeScript code for the market channel, the user channel, RTDS and the new PolyBolt price feed. Polymarkets.co.il is an independent guide, not Polymarket. Every host, field and event name below was checked on 25 October 2026 against Polymarket's developer documentation and its published AsyncAPI specs.
What this page answers
- The Polymarket WebSocket URLs and which one to use
- The exact JSON to subscribe to the market channel (
assets_ids) and the user channel - Every market and user event type, with the fields that matter
- What RTDS is now, what moved to PolyBolt, and the
activitytopic - Heartbeats, reconnects and keeping a local order book correct
Polymarket WebSocket endpoints (October 2026)
| Service | URL | Data | Auth | Heartbeat |
|---|---|---|---|---|
| CLOB market channel | wss://ws-subscriptions-clob.polymarket.com/ws/market | Order books, price changes, trades, tick size, new and resolved markets | None | Client sends PING every 10 s |
| CLOB user channel | wss://ws-subscriptions-clob.polymarket.com/ws/user | Your orders and trades | CLOB API key, secret, passphrase | Client sends PING every 10 s |
| Sports | wss://sports-api.polymarket.com/ws | Live scores, periods and game status | None | Server sends ping every 5 s; reply pong within 10 s |
| RTDS (legacy) | wss://ws-live-data.polymarket.com | Public trade activity; comments (being retired); old price topics | None for activity | Client sends PING every 5 s |
| PolyBolt | wss://ws-live-v2.polymarket.com/ws | Crypto, equity and TWAP reference prices | CLOB API credentials | Server pings every 25 s |
| Combos RFQ quoter gateway | wss://combos-rfq-gateway-quoter.polymarket.com/ws/rfq | Quote requests for market makers | Required | See the RFQ docs |
The CLOB heartbeat is an application-level text frame: the literal string PING, not a WebSocket protocol ping. A library's built-in ping setting does not replace it.
Market channel: the subscribe message and its JSON format
The market channel is public. Right after connecting, send one subscription frame listing the outcome token IDs (asset IDs) you want. Get token IDs from the Gamma API (clobTokenIds) or from the SDK's market object.
{
"assets_ids": ["<token_id_yes>", "<token_id_no>"],
"type": "market",
"initial_dump": true,
"custom_feature_enabled": true
}
| Field | Required | Meaning |
|---|---|---|
assets_ids | Yes | Array of token IDs (strings). Spelled assets_ids; asset_ids is the field name inside events, not in this request. |
type | Yes | Must be "market". |
initial_dump | No | Send a full book snapshot on subscribe. Default true. |
level | No | 1, 2 or 3, default 2. The docs list the values but do not describe them. |
custom_feature_enabled | No | Adds best_bid_ask, new_market and market_resolved events. Default false. |
To change the set without reconnecting, send {"operation": "subscribe", "assets_ids": ["<new_id>"]} or {"operation": "unsubscribe", "assets_ids": ["<old_id>"]} on the open connection.
Market channel events
event_type | When | Key fields |
|---|---|---|
book | On subscribe, and when the book is resent | asset_id, market (condition ID), bids, asks (price and total size per level), hash, timestamp (ms) |
price_change | A price level changes | price_changes[]: asset_id, price, size (the new total at that level; 0 removes it), side, best_bid, best_ask |
last_trade_price | A trade executes | price, size, side, fee_rate_bps, transaction_hash |
tick_size_change | The minimum price step changes | old_tick_size, new_tick_size |
best_bid_ask | Top of book moves (opt-in) | best_bid, best_ask, spread |
new_market | A market is created (opt-in) | question, slug, assets_ids, outcomes |
market_resolved | A market resolves (opt-in) | winning_asset_id, winning_outcome |
All prices and sizes arrive as strings; parse them as decimals. Treat price_change sizes as replacements, not increments.
Python: market channel with the websockets library
# pip install websockets
import asyncio, json, websockets
URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
async def heartbeat(ws):
while True:
await asyncio.sleep(10)
await ws.send("PING") # text frame, not a protocol ping
async def stream(asset_ids):
async with websockets.connect(URL) as ws:
await ws.send(json.dumps({"assets_ids": asset_ids, "type": "market"}))
hb = asyncio.create_task(heartbeat(ws))
try:
async for raw in ws:
if raw == "PONG":
continue
msg = json.loads(raw)
for evt in msg if isinstance(msg, list) else [msg]:
print(evt.get("event_type"), evt.get("asset_id") or evt.get("market"))
finally:
hb.cancel()
asyncio.run(stream(["<token_id>"]))
TypeScript: market channel with ws
// npm install ws
import WebSocket from "ws";
const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market");
let hb: ReturnType<typeof setInterval>;
ws.on("open", () => {
ws.send(JSON.stringify({ assets_ids: ["<token_id>"], type: "market" }));
hb = setInterval(() => ws.send("PING"), 10_000);
});
ws.on("message", (data) => {
const text = data.toString();
if (text === "PONG") return;
const msg = JSON.parse(text);
for (const evt of Array.isArray(msg) ? msg : [msg]) console.log(evt.event_type, evt.asset_id);
});
ws.on("close", () => clearInterval(hb));
Same stream through the official SDKs (0.14.0)
The unified SDKs, polymarket-client (Python) and @polymarket/client (TypeScript), wrap the same feeds and handle the heartbeat for you. In Python, streams are available only on the async clients.
# pip install polymarket-client
import asyncio
from polymarket import AsyncPublicClient
from polymarket.streams import MarketSpec
async def main():
async with AsyncPublicClient() as client:
async with await client.subscribe([MarketSpec(asset_ids=["<token_id>"])]) as stream:
async for event in stream:
if event.type == "book":
print("snapshot", event.payload.bids[-1:], event.payload.asks[-1:])
elif event.type == "price_change":
print("delta", event.payload)
asyncio.run(main())
// npm install @polymarket/client@latest
import { createPublicClient } from "@polymarket/client";
const client = createPublicClient();
const stream = await client.subscribe([{ topic: "market", assetIds: ["<token_id>"] }]);
for await (const event of stream) {
if (event.type === "book") console.log("snapshot", event.payload.assetId);
else if (event.type === "price_change") console.log("delta", event.payload);
}
User channel: your orders and fills
The user channel streams order and trade updates for the account behind your CLOB API credentials (create or derive them as shown in authentication). Connect to wss://ws-subscriptions-clob.polymarket.com/ws/user and send the subscription immediately; the server may close a connection that stays unsubscribed.
{
"auth": { "apiKey": "<key>", "secret": "<secret>", "passphrase": "<passphrase>" },
"type": "user",
"markets": ["<condition_id>"]
}
markets is optional: leave it out to receive every market for the account. It takes condition IDs, not token IDs, which is the opposite of the market channel. Add or remove markets later with {"operation": "subscribe", "markets": [...]} or "unsubscribe". Keep these credentials on a server; never ship them to a browser.
event_type | Sub-type | Key fields |
|---|---|---|
order | type: PLACEMENT, UPDATE (some or all matched), CANCELLATION | id, asset_id, side, price, original_size, size_matched, order_type, status |
trade | status: MATCHED, MINED, CONFIRMED, RETRYING, FAILED | id, taker_order_id, size, price, trader_side (TAKER or MAKER), maker_orders[], transaction_hash |
A trade is final only at CONFIRMED; FAILED is the other terminal state. With the SDKs: client.subscribe([{ topic: "user" }]) on a TypeScript SecureClient, or client.subscribe(UserSpec()) on a Python AsyncSecureClient, with an optional market filter (UserSpec(markets=["<condition_id>"])).
RTDS (ws-live-data.polymarket.com): what it carries now
RTDS, the Real-Time Data Socket, was Polymarket's public stream for crypto and equity prices, comments and trade activity. Since 15 September 2026 Polymarket's changelog calls it legacy for reference prices: crypto_prices, crypto_prices_chainlink and equity_prices map to PolyBolt channels (next section), and the SDKs' deprecated RTDS price topics are scheduled for removal. Two things stay:
activity: the migration guide says to keep using RTDS for this topic, which has no PolyBolt channel. It streams public trades as they happen, with message typestradesandorders_matched.comments: still on RTDS for now, but the live comments stream is being retired with no streaming replacement. List comments over the Gamma API (GET /comments) instead.
RTDS uses its own frame format. Connect to wss://ws-live-data.polymarket.com, send the text PING every 5 seconds, and subscribe like this:
{
"action": "subscribe",
"subscriptions": [
{ "topic": "activity", "type": "trades", "filters": "{\"event_slug\":\"<event-slug>\"}" }
]
}
filters is a JSON string with either event_slug or market_slug; an empty filter means no filtering. Each trade message carries topic, type, timestamp and a payload with asset, conditionId, slug, eventSlug, outcome, side, price, size, proxyWallet and transactionHash. Note: the current documentation pages no longer describe the activity topic in detail; these topic, filter and field names come from the README of Polymarket's official real-time-data-client, which is deprecated in favour of the unified SDK (checked 25 October 2026). For a durable trade history, page through the Data API /v2/trades feed.
PolyBolt: the new reference-price feed
PolyBolt (wss://ws-live-v2.polymarket.com/ws, live since 15 September 2026) streams the reference prices that crypto and equity up-or-down markets resolve on. Unlike RTDS it requires your CLOB API credentials, sends a two-minute history snapshot on every subscribe, and numbers every message.
| Channel | Data | Filter |
|---|---|---|
price.crypto | Crypto prices in USD (Chainlink by default, Pyth on some symbols) | {"symbol": "btcusd"} |
price.crypto.twap | 60-second Chainlink TWAP | {"symbol": "btcusd", "window_seconds": 60} |
price.equity | Stocks, ETFs, forex, metals, commodities | {"symbol": "aapl"} |
price.equity.twap | 60-second forex TWAP (eurusd, usdjpy) | {"symbol": "eurusd", "window_seconds": 60} |
Symbols are lowercase and crypto pairs end in usd, so RTDS's btcusdt or btc/usd becomes btcusd (and Binance USDT quotes become USD quotes). The 30-second TWAP has no PolyBolt replacement.
# pip install websockets
import asyncio, json, os, websockets
async def prices():
async with websockets.connect("wss://ws-live-v2.polymarket.com/ws") as ws:
await ws.send(json.dumps({"op": "auth", "rid": "a1", "auth": {
"apiKey": os.environ["CLOB_API_KEY"],
"secret": os.environ["CLOB_SECRET"],
"passphrase": os.environ["CLOB_PASS_PHRASE"]}}))
while True: # wait for the auth acknowledgement
ack = json.loads(await ws.recv())
if ack.get("op") == "authed":
break
if ack.get("op") == "error":
raise RuntimeError(ack)
await ws.send(json.dumps({"op": "subscribe", "rid": "s1", "subscriptions": [
{"channel": "price.crypto", "filter": {"symbol": "btcusd"}}]}))
async for raw in ws:
msg = json.loads(raw)
if "payload" in msg:
print(msg["channel"], msg.get("seq"), msg["payload"])
asyncio.run(prices())
Every data frame has the envelope {"v": 1, "channel", "seq", "ts", "snapshot"?, "dropped"?, "payload"}. Prefer the exact decimal string full_accuracy_value over the float value, and re-read source after each reconnect. Limits per connection: 64 subscriptions, 20 subscribe frames per second, 64 KB frames and 8 auth frames; breaking them closes the socket with code 4008. Other close codes: 4001 auth, 4002 slow consumer, 4003 server draining (reconnect once with jitter). With the SDK (0.11.0 or later), subscribe on an authenticated client: { topic: "prices.crypto", symbols: ["btcusd"] } in TypeScript or CryptoPriceSpec(symbols=["btcusd"]) in Python.
Sports WebSocket
wss://sports-api.polymarket.com/ws needs no subscription message: the server streams every game update after you connect. It sends the text ping every 5 seconds and closes the connection unless you answer pong within 10 seconds. Each update carries gameId, leagueAbbreviation, teams, status, score (as "home-away"), period and live / ended flags. Polymarket labels this data informational and possibly delayed.
Heartbeat, reconnect and a correct local order book
- Heartbeat: send
PINGevery 10 seconds on the market and user channels and ignore thePONGreplies in your parser. - Change subscriptions live: use the
operationframes instead of reconnecting. - Nothing is replayed. Polymarket's docs say real-time updates do not replay changes missed while disconnected. On the market channel, resubscribe after every reconnect: with
initial_dumpleft attrueyou get a freshbooksnapshot to rebuild from. You can also fetchGET https://clob.polymarket.com/book?token_id=...and compare itshash. - User channel recovery: after a reconnect, fetch your open orders and recent trades over REST, then apply new events from that state.
- Back off: reconnect with exponential backoff and jitter so a restart does not turn into a reconnect storm.
- Order safety is separate: a WebSocket heartbeat does not keep resting orders alive or cancel them. For a dead man's switch on orders, use the CLOB order heartbeat (
POST /heartbeats), described in the CLOB API guide.
Is the WebSocket faster than polling the REST API?
For staying current, yes in practice: the server pushes each book change as it happens, while polling only sees the book at the moment you ask and spends rate limit on every request (/book is limited to 1,500 requests per 10 seconds per IP). Polymarket publishes no latency figures comparing the two, so measure on your own setup. For one-off reads, or after a reconnect, REST remains the simplest source of truth.
docs.polymarket.com), the AsyncAPI specs they publish, and the README of the official real-time-data-client repository.Next: CLOB REST API · Data API · Build: stream the book in a bot · Fix a CLOB error.



