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 activity topic
  • Heartbeats, reconnects and keeping a local order book correct

Polymarket WebSocket endpoints (October 2026)

ServiceURLDataAuthHeartbeat
CLOB market channelwss://ws-subscriptions-clob.polymarket.com/ws/marketOrder books, price changes, trades, tick size, new and resolved marketsNoneClient sends PING every 10 s
CLOB user channelwss://ws-subscriptions-clob.polymarket.com/ws/userYour orders and tradesCLOB API key, secret, passphraseClient sends PING every 10 s
Sportswss://sports-api.polymarket.com/wsLive scores, periods and game statusNoneServer sends ping every 5 s; reply pong within 10 s
RTDS (legacy)wss://ws-live-data.polymarket.comPublic trade activity; comments (being retired); old price topicsNone for activityClient sends PING every 5 s
PolyBoltwss://ws-live-v2.polymarket.com/wsCrypto, equity and TWAP reference pricesCLOB API credentialsServer pings every 25 s
Combos RFQ quoter gatewaywss://combos-rfq-gateway-quoter.polymarket.com/ws/rfqQuote requests for market makersRequiredSee 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
}
FieldRequiredMeaning
assets_idsYesArray of token IDs (strings). Spelled assets_ids; asset_ids is the field name inside events, not in this request.
typeYesMust be "market".
initial_dumpNoSend a full book snapshot on subscribe. Default true.
levelNo1, 2 or 3, default 2. The docs list the values but do not describe them.
custom_feature_enabledNoAdds 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_typeWhenKey fields
bookOn subscribe, and when the book is resentasset_id, market (condition ID), bids, asks (price and total size per level), hash, timestamp (ms)
price_changeA price level changesprice_changes[]: asset_id, price, size (the new total at that level; 0 removes it), side, best_bid, best_ask
last_trade_priceA trade executesprice, size, side, fee_rate_bps, transaction_hash
tick_size_changeThe minimum price step changesold_tick_size, new_tick_size
best_bid_askTop of book moves (opt-in)best_bid, best_ask, spread
new_marketA market is created (opt-in)question, slug, assets_ids, outcomes
market_resolvedA 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_typeSub-typeKey fields
ordertype: PLACEMENT, UPDATE (some or all matched), CANCELLATIONid, asset_id, side, price, original_size, size_matched, order_type, status
tradestatus: MATCHED, MINED, CONFIRMED, RETRYING, FAILEDid, 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 types trades and orders_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.

ChannelDataFilter
price.cryptoCrypto prices in USD (Chainlink by default, Pyth on some symbols){"symbol": "btcusd"}
price.crypto.twap60-second Chainlink TWAP{"symbol": "btcusd", "window_seconds": 60}
price.equityStocks, ETFs, forex, metals, commodities{"symbol": "aapl"}
price.equity.twap60-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 PING every 10 seconds on the market and user channels and ignore the PONG replies in your parser.
  • Change subscriptions live: use the operation frames 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_dump left at true you get a fresh book snapshot to rebuild from. You can also fetch GET https://clob.polymarket.com/book?token_id=... and compare its hash.
  • 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.

Sources checked on 25 October 2026 (named, not linked): Polymarket docs pages Market Channel, User Channel, Sports Channel, Quoter Gateway, Real-Time Data, Real-Time Order Updates, PolyBolt WebSocket, Migrate from RTDS to PolyBolt, API (getting started), Rate Limits and the Predictions changelog (all under 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.