Products
Datacenter proxies from $19/mo Rotating proxies from $49/mo ISP proxies from $33/mo Dedicated proxies from $3.50/mo UDP proxies from $5/mo Try proxies
Use cases
Data & scraping AI services Social media & messaging E-commerce & finance Media & entertainment Marketing & ads Automation & tools All use cases →
Pricing
Full pricing table All 20 locations Money-back guarantee
Resources
Blog Proxy API MCP server Setup guides FAQ For business About us Affiliate program
English
English Русский
← PapaProxy.net Blog

How to subscribe to the Polymarket WebSocket channels

Polymarket uses two WebSocket systems: CLOB streams order books and authenticated order activity, while RTDS streams external prices and comments. CLOB expects text `PING` every 10 seconds; RTDS expects it every 5 seconds.

API & data · Automation August 7, 2026 3 min read Alex Young Alex Young Technical specialist
Key takeawaysPolymarket has separate CLOB and RTDS WebSockets: CLOB market/user connections use 10-second text PINGs, while RTDS requires them every 5 seconds.
  • Polymarket has separate CLOB and RTDS WebSockets: CLOB market/user connections use 10-second text PINGs, while RTDS requires them every 5 seconds.
  • The public CLOB market channel subscribes by outcome-token assets_ids; the authenticated user channel filters by condition IDs and uses L2 API credentials.
  • book is a full order-book snapshot, price_change changes individual levels, and size: "0" removes a level; last_trade_price is not a replacement book snapshot.
  • After reconnecting, invalidate the old book and wait for a fresh book snapshot before applying deltas because the documented feed does not provide a universal monotonic sequence number.
  • Monitor data freshness separately from heartbeat health: an open socket and successful PONGs do not prove that the application feed is still delivering useful events.

This summary was created with AI.

Two hosts that are easy to confuse

There is no single WebSocket API endpoint for every live Polymarket event. The CLOB socket belongs to the trading stack; RTDS is a separate real-time data feed for external prices and comments.

Stream Endpoint Subscription key Auth Heartbeat
CLOB market wss://ws-subscriptions-clob.polymarket.com/ws/market token / asset IDs No PING every 10s
CLOB user wss://ws-subscriptions-clob.polymarket.com/ws/user condition IDs L2 API credentials PING every 10s
RTDS wss://ws-live-data.polymarket.com topic + type + filters Usually public; selected streams may use gamma_auth PING every 5s

The current WebSocket documentation describes CLOB market as the public Level 2 feed for book, price and trade updates. The user channel carries activity tied to your own API credentials. RTDS uses a different message model and currently documents crypto prices, Chainlink crypto prices, equity prices and comments.

This distinction became more important after CLOB V2 went live on April 28, 2026. The production CLOB still uses clob.polymarket.com, but legacy V1 SDKs and V1-signed orders are no longer supported. Old 2024 or 2025 examples may therefore show a recognizable WebSocket host while surrounding it with obsolete trading or authentication code.

The identifiers are another common source of mistakes. The public market channel subscribes with assets_ids, meaning the individual outcome-token IDs. The authenticated user channel filters with markets, which are condition IDs. Sending a condition ID where the market socket expects a token ID does not describe the same object.

Subscribing to the market channel

Use the public market channel when you need the order book over WebSocket, best prices, price-level changes or executions for specific outcome tokens. No wallet or API key is required.

A minimal subscription is:

JSON
{
  "type": "market",
  "assets_ids": [
    "TOKEN_ID_YES",
    "TOKEN_ID_NO"
  ],
  "custom_feature_enabled": true
}

custom_feature_enabled is optional. Turning it on adds best_bid_ask, new_market and market_resolved; the core book events do not depend on it. You can also add or remove asset IDs on an existing connection with operation: "subscribe" and operation: "unsubscribe" instead of reconnecting.

The event types solve different parts of a local-book implementation:

Event Meaning What your client should do
book Full aggregated snapshot Replace local state for that asset
price_change Order placement or cancellation changed levels Apply each level update
last_trade_price A trade was executed Record the execution; do not treat it as a full book
tick_size_change Minimum tick changed Update order-price validation
best_bid_ask Top of book changed Optional shortcut when custom features are enabled

A price_change with size: "0" removes that price level. That detail matters more than array ordering: a safe local book stores levels by price and recomputes the best bid and ask instead of assuming that every incoming array happens to be sorted exactly how your code expects.

The public trades payload structure is the last_trade_price event. It contains fields such as asset_id, market, price, size, side, timestamp and, in the current API reference, transaction_hash. It is not the same thing as the authenticated trade event on the user socket, which tracks the lifecycle of your own matched trade.

Send the text message:

Text
PING

every 10 seconds. The CLOB server responds with PONG. This is an application-level heartbeat; a WebSocket library's protocol-level ping frames are a different mechanism.

The authenticated user channel

The user socket is for your own order and trade updates. WebSocket authentication uses the L2 CLOB credentials apiKey, secret and passphrase, not the wallet private key itself.

Those L2 credentials are created or derived through L1 authentication, where the wallet signs an EIP-712 message. Polymarket then uses the API key, secret and passphrase for L2 authentication. Never put them into frontend JavaScript or a public repository.

A subscription looks like this:

JSON
{
  "type": "user",
  "markets": [
    "0xCONDITION_ID"
  ],
  "auth": {
    "apiKey": "YOUR_API_KEY",
    "secret": "YOUR_API_SECRET",
    "passphrase": "YOUR_PASSPHRASE"
  }
}

The identifiers differ from the public channel deliberately: assets_ids on market are outcome-token IDs, while markets on user are condition IDs. The user connection can also subscribe or unsubscribe from markets without reconnecting.

There are two main event families.

order reports placement, partial matching/update and cancellation. The event's type distinguishes states such as PLACEMENT, UPDATE and CANCELLATION.

trade follows execution farther than a public last-price event. The documented lifecycle includes:

Text
MATCHED → MINED → CONFIRMED
    ↓        ↑
RETRYING ───┘
    ↓
  FAILED

MATCHED therefore should not automatically be treated as final settlement. CONFIRMED and FAILED are terminal in the documented trade-state model.

Authentication failures deserve explicit logging. In a February 2026 GitHub issue, a Polymarket maintainer noted that failed authentication could cause the server to close a user WebSocket without first sending a useful error message. That is field evidence rather than an API contract, but it gives you a practical first check when a user socket closes immediately: validate freshly derived credentials before rebuilding the rest of the client.

RTDS is a different socket with different rules

RTDS connects to:

Text
wss://ws-live-data.polymarket.com

Its subscription message does not use CLOB token or condition IDs. You send an action plus one or more topic subscriptions:

JSON
{
  "action": "subscribe",
  "subscriptions": [
    {
      "topic": "crypto_prices",
      "type": "update",
      "filters": "btcusdt,ethusdt"
    }
  ]
}

Current RTDS documentation describes crypto_prices from Binance, crypto_prices_chainlink from Chainlink, equity_prices and comments. Binance symbols use lowercase concatenated names such as btcusdt; Chainlink uses forms such as btc/usd, and its symbol filter is encoded differently.

Messages share an envelope:

JSON
{
  "topic": "crypto_prices",
  "type": "update",
  "timestamp": 1753314088421,
  "payload": {
    "symbol": "btcusdt",
    "timestamp": 1753314088395,
    "value": 67234.50
  }
}

RTDS requires a text PING every 5 seconds, not every 10. Subscriptions can be changed without disconnecting.

Heartbeat success is not the same as data freshness. A socket can remain technically open while the data your application expects becomes stale. An issue against Polymarket's official RTDS client describes exactly that failure shape: ping/pong continued while application messages stopped. Treat that report as a reason to monitor last_data_age separately from last_pong_age, not as a guarantee that every connection will fail the same way.

A useful monitor therefore has at least:

Text
connection_age_seconds
last_pong_age_seconds
last_data_age_seconds
messages_received_total{topic,type}
reconnect_total{reason}
parse_error_total{topic,type}

The stale-data threshold must match the feed. A quiet comments subscription and a high-frequency BTC price subscription cannot use the same timeout.

Streaming or polling: choosing between them

Use REST and WebSocket endpoints for different jobs instead of replacing every HTTP call with a socket.

For continuous order-book state, WebSocket is the natural path. The current order book documentation exposes GET /book for one token and POST /books for multiple token IDs; these REST calls are useful for one-off snapshots, startup checks and diagnostics. CLOB read endpoints are public.

A practical split looks like this:

Need Better source
Discover markets and obtain metadata Gamma / market REST APIs
One current book snapshot CLOB REST /book or /books
Continuous book changes CLOB market WebSocket
Your order/fill lifecycle CLOB user WebSocket
External crypto/equity reference prices RTDS
Comments RTDS
Historical prices CLOB REST price-history endpoint

Polling /book continuously throws away one of the main advantages of the stream: after the initial snapshot, most order placements and cancellations can be represented as level changes rather than another complete HTTP response.

The reverse mistake is forcing one-off discovery through a persistent socket. Market metadata and static identifiers do not need a long-lived connection simply because your execution process already has one.

Reconnecting without losing the book

Good WebSocket support is not just while True: reconnect(). The important question is what state your process trusts after the connection comes back.

Current Polymarket market messages document timestamps and hashes, but they do not expose a monotonic sequence number that a client can use as a universal gap detector. Do not invent one from timestamps: multiple events can share close timestamps, and network arrival order is not a replacement for a documented sequence.

For the CLOB book, use this recovery rule:

  1. The moment the socket is lost, mark every book owned by that connection as unsynchronized.
  2. Reconnect with backoff and jitter.
  3. Resubscribe to the same token IDs.
  4. Ignore price_change events for an asset until a fresh book event has established its new full snapshot.
  5. Replace the old local book with that snapshot, then resume applying deltas.
  6. Treat REST /book as a diagnostic or fallback snapshot, not as permission to merge an unknown WebSocket gap blindly.

Polymarket documents book as a full snapshot emitted when first subscribing, which makes that event the clean handoff point after a reconnect.

Here is a production-oriented skeleton for the public market channel. It handles application-level heartbeats, invalidates state across reconnects and refuses to apply deltas before a fresh snapshot:

Python
import asyncio
import json
import random
from decimal import Decimal

from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed

URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
ASSET_IDS = ["TOKEN_ID_YES", "TOKEN_ID_NO"]


class Books:
    def __init__(self):
        self.data = {}
        self.synced = set()

    def invalidate_all(self):
        self.synced.clear()

    def apply(self, msg):
        event = msg.get("event_type")

        if event == "book":
            asset = msg["asset_id"]
            self.data[asset] = {
                "bids": {
                    Decimal(x["price"]): Decimal(x["size"])
                    for x in msg["bids"]
                },
                "asks": {
                    Decimal(x["price"]): Decimal(x["size"])
                    for x in msg["asks"]
                },
            }
            self.synced.add(asset)
            return

        if event != "price_change":
            return

        for change in msg["price_changes"]:
            asset = change["asset_id"]
            if asset not in self.synced:
                continue

            side = "bids" if change["side"] == "BUY" else "asks"
            price = Decimal(change["price"])
            size = Decimal(change["size"])
            levels = self.data[asset][side]

            if size == 0:
                levels.pop(price, None)
            else:
                levels[price] = size


async def heartbeat(ws):
    while True:
        await asyncio.sleep(10)
        await ws.send("PING")


async def stream():
    books = Books()
    backoff = 1.0

    while True:
        try:
            async with connect(
                URL,
                ping_interval=None,   # Polymarket uses text PING/PONG here
                open_timeout=10,
                close_timeout=5,
            ) as ws:
                books.invalidate_all()
                await ws.send(json.dumps({
                    "type": "market",
                    "assets_ids": ASSET_IDS,
                }))

                ping_task = asyncio.create_task(heartbeat(ws))

                try:
                    async for raw in ws:
                        if raw == "PONG":
                            continue
                        books.apply(json.loads(raw))
                finally:
                    ping_task.cancel()

            backoff = 1.0

        except (ConnectionClosed, OSError, TimeoutError, ValueError):
            books.invalidate_all()
            await asyncio.sleep(random.uniform(0, backoff))
            backoff = min(backoff * 2, 30)


asyncio.run(stream())

What to watch: an asset enters synced only after a new full book snapshot. A price_change received before that point is discarded rather than applied to stale state. Decimal avoids introducing binary-float rounding into price-level keys.

What this example deliberately leaves out: persistent storage, metrics export, graceful process shutdown, RTDS handling, authenticated user events and REST snapshot fallback. It also assumes that ASSET_IDS were already discovered and validated before the process starts.

For RTDS, use the same reconnect principles but restore topic subscriptions instead of token IDs, send the heartbeat every five seconds, and monitor application-data age independently from PONG.

The network path comes after those software controls. A proxy does not repair a stale local book, bad credentials or a missing heartbeat. When several independent long-running collectors need fixed, reproducible egress addresses or separate network failure domains, crypto proxies can provide that stable egress layer without changing Polymarket's application rules.