Engineering

Polymarket API for Bot Development: Developer Guide

The Polymarket API has two halves and the docs do not really explain when to use which. A developer-first walkthrough of CLOB, Gamma, auth, rate limits, and how a bot should consume each one.

Last reviewed · Jamal Okafor, Poly Syncer

The Polymarket API is not one API; it is two, and the docs do not really tell you when to use which. A bot developer who treats the platform as a single REST surface will spend the first week tripping over the split: CLOB for orders and books, Gamma for market discovery and metadata, with overlapping but non-identical schemas. This guide is a developer-first walkthrough of both halves, with code that demonstrates the actual endpoints, an authentication flow that explains the EIP-712 plus L2 API key dance, rate-limit numbers worth respecting, and a capability map that shows which surface answers which question. The aim is a working mental model that lets a competent engineer ship a bot against the Polymarket API in a week, not three.

The two halves of the Polymarket API

Polymarket exposes two production HTTP surfaces and one WebSocket stream. CLOB, at clob.polymarket.com, is the order book and trading surface. It handles order placement, order cancellation, order book snapshots, trade history, and per-token state. Gamma, at gamma-api.polymarket.com, is the metadata and discovery surface. It handles events, markets, categories, tags, search, and the human-readable copy that the website renders. A bot that needs to discover which markets exist asks Gamma. A bot that needs to read or move an order book talks to CLOB. The split feels arbitrary at first and then becomes obvious: discovery is read-mostly and cacheable, trading is write-heavy and authenticated.

The two surfaces share the underlying token IDs but model them differently. Gamma returns markets with rich nested objects (event, condition, outcomes, prices, volume aggregates) and is the right place to ask “what markets exist for category X.” CLOB returns markets as flat token-level entries optimised for the order book. A bot that loads a watchlist from Gamma and then subscribes to per-token book updates on CLOB is following the intended pattern; the other direction (using CLOB to enumerate markets) is slower and missing fields.

There is also a third minor surface, the data API at data-api.polymarket.com, which exposes historical price and volume time series. It is read-only, unauthenticated, and rate-limited more aggressively than the other two. Most bots can ignore it on day one and add it later for backtesting.

Authentication: EIP-712 and the L2 API key dance

Read endpoints on both Gamma and CLOB are unauthenticated. Anyone can curl the books and the market lists. Write endpoints — placing orders, cancelling orders, reading per-user trade history with PII attached — require authentication, and the auth model is more involved than a typical bearer-token API. There are two layers.

Layer one is an EIP-712 typed-data signature from the trader's Polygon wallet. This proves the order originated from the holder of the private key. The signature covers the order parameters (token ID, side, price, size, expiration, nonce, fee rate, signature type) and is the same primitive used by the official py-clob-client and clob-client TypeScript libraries. Layer two is an L2 API key, which the trader derives once from the wallet by signing a deterministic message; the resulting key plus a per-request HMAC are sent in the headers of every authenticated CLOB call. The L2 layer exists so that the bot does not have to round-trip a fresh wallet signature for every order; the wallet signs the order body, the L2 key authenticates the HTTP request.

The derivation looks roughly like this in Python:

# auth_setup.py — one-time L2 API key derivation
from py_clob_client.client import ClobClient
from py_clob_client.constants import POLYGON

HOST    = "https://clob.polymarket.com"
KEY     = "0x..."                       # private key, never log this
CHAIN   = POLYGON                       # Polygon mainnet

client = ClobClient(HOST, key=KEY, chain_id=CHAIN)
creds  = client.create_or_derive_api_creds()
# creds.api_key, creds.api_secret, creds.api_passphrase
# Store these. The bot uses them on every authenticated request.

Once the L2 credentials exist, every authenticated CLOB request carries four headers: POLY_ADDRESS (the trader address), POLY_SIGNATURE (HMAC of the canonical request), POLY_TIMESTAMP, and POLY_API_KEY. The HMAC body is the request method, path, timestamp, and JSON body joined in a fixed order. The official client builds these headers; a hand-rolled implementation is roughly forty lines of Python and is worth doing once to understand what is being signed, then throwing away in favour of the maintained library.

Reading the order book (CLOB endpoints)

The single most useful CLOB endpoint is GET /markets, which returns paginated market entries with their token IDs, condition IDs, current minimum order size, tick size, and resolution status. A bot uses this once at startup to build the token-to-market map, then subscribes to the WebSocket for live updates. The plain unauthenticated read:

# curl — first 500 active CLOB markets
curl -s "https://clob.polymarket.com/markets?next_cursor=" \
  | jq '.data[] | {condition_id, question, tokens: [.tokens[].token_id]}' \
  | head -40

For a specific market's current book, GET /book?token_id={id} returns bids and asks as arrays of price-size pairs. The book endpoint is the right surface for a one-shot snapshot or for reconciliation against the WebSocket cache. Bots should not poll the book endpoint at high frequency; the rate limit is generous but not unlimited, and the snapshot will always be slightly behind the WebSocket. Use it on startup and on reconnect.

For midprice and last-trade data without parsing the full book, GET /midpoint?token_id={id} and GET /last-trade-price?token_id={id} are convenience endpoints that return a single number each. They are useful for thin-client integrations (a Telegram alert bot, for example) where the full book is overkill. A scanner that compares hundreds of markets simultaneously should still use the book endpoint and compute the midpoint locally.

The trade history endpoint, GET /trades?market={condition_id}, returns recent fills with timestamps, prices, and sizes. It is unauthenticated for public market data; the per-user variant requires the L2 headers. The full mechanics of how the book itself works (price-time priority, partial fills, the role of the tick size) are explained in the companion order book deep-dive; this guide stays at the API layer.

Placing and cancelling orders

An order on Polymarket is a JSON object with the trade parameters plus an EIP-712 signature. The bot constructs the order body, signs it with the wallet's private key, attaches the L2 headers, and POSTs to /order. The shape, abbreviated:

# place_order.py — signed order via py-clob-client
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY

args = OrderArgs(
    token_id   = "71321045679252212594626385532...",  # outcome token
    price      = 0.42,
    size       = 100.0,
    side       = BUY,
    fee_rate_bps = 0,
)

signed = client.create_order(args)
resp   = client.post_order(signed, OrderType.GTC)
# resp -> {"success": true, "orderID": "0x..", "transactionsHashes": []}
print(resp)

Order types worth knowing: GTC (good-till-cancelled, resting on the book), GTD (good-till-date, with an explicit expiration), FOK (fill-or-kill, taker only, all-or-nothing), and FAK (fill-and-kill, taker only, fill what is available and cancel the rest). FOK and FAK are the right choices for arbitrage and momentum bots that want to avoid resting risk. GTC is the right choice for market-making bots that intentionally rest quotes.

Cancelling is a separate signed call. DELETE /order?orderID={id} with the L2 headers cancels a single order; POST /cancel-all cancels every open order for the authenticated trader. Cancels are not instantaneous — the request has to be processed and the on-book state updated — so a bot that needs to flatten quickly should send the cancel and not assume it landed for at least 200 milliseconds.

A practical note: the order builder needs the market's tick size and minimum order size to construct a valid order. These come from the /markets response or, more reliably for a single market, GET /markets/{condition_id}. Submitting an order at a price that does not align with the tick size returns a 400 and burns a round-trip.

Market discovery via Gamma

Gamma is where a bot finds what to trade. The two endpoints that matter are GET /events, which returns event-level records (each event groups one or more related markets), and GET /markets, which returns individual markets with prices and volume. Both support filtering by status, category, tag, and a few other fields, and both paginate with cursors.

# curl — active events in the politics category
curl -s "https://gamma-api.polymarket.com/events?active=true&closed=false&tag=politics&limit=50" \
  | jq '.[] | {slug, title, volume, markets: [.markets[].question]}'

Gamma's market entries include the condition ID and the token IDs, which is the bridge to CLOB. A typical discovery flow: query Gamma for events matching the bot's category filter, extract the token IDs, register those tokens in the bot's watchlist, and open a CLOB WebSocket subscription for each. Gamma is also where the human-readable fields live (the market question, the description, the image, the resolution source URL), so any bot that surfaces markets to a user — a Telegram alert bot, a dashboard — pulls strings from Gamma.

One source of confusion: Gamma's active, closed, and archived flags are not mutually exclusive in the way a developer might expect. A market can be active=false and closed=false simultaneously while it is in a pre-launch state. Filter explicitly on both flags rather than assuming one implies the other. The reference list of valid filter combinations is in the official documentation, and the source of truth for the schemas is the clob-client repository on GitHub.

Rate limits and websocket streams

The public CLOB read endpoints are rate-limited per IP, roughly in the range of 10 to 20 requests per second sustained with a small burst allowance above that. Gamma is similar. Authenticated CLOB endpoints are rate-limited per API key, with a noticeably more generous budget for order placement and cancellation than for catalogue reads. The exact numbers shift; a bot should treat the rate limiter as a black box and back off on 429 responses with an exponential retry capped at one minute.

The WebSocket stream at wss://ws-subscriptions-clob.polymarket.com/ws/market is where any serious bot spends most of its time. The subscription is per token ID; a single connection can carry hundreds of token subscriptions. The stream emits book updates, last-trade events, and a periodic heartbeat. The connection drops occasionally — once or twice an hour is typical — and the bot needs a reconnect loop with deduplication on the resume, because the stream does not replay missed events. On reconnect, fetch a fresh snapshot from the REST /book endpoint for every subscribed token before re-arming the live cache.

A small comparison table for the three transport choices a bot has to make:

TransportLatencyUse forCost / quota
REST GET (CLOB)80–250 msSnapshots, reconnect, low-frequency reads10–20 req/s per IP
REST GET (Gamma)120–400 msDiscovery, metadata, search10–20 req/s per IP
WebSocket (CLOB)40–120 msLive book and trade updatesPer-token subscription, soft cap ~1000
REST POST (CLOB)150–350 msOrder placement and cancellationPer-API-key, generous

The numbers are representative, not guaranteed. A bot deployed in the same AWS region as the gateway sees the lower end of each range; a bot on a home network sees the upper end. For latency-sensitive work (the kind covered in the arbitrage bot guide), region matters as much as code quality.

The capability map

The mental model that pays off fastest is a simple two-column map: which capability is available on which surface, and where the overlap is.

Polymarket API — CLOB versus Gamma capability map

Capability map for the Polymarket API showing CLOB and Gamma coverage A two-column matrix mapping six capabilities to the CLOB and Gamma API surfaces. Order placement is CLOB only. Order cancellation is CLOB only. Market discovery is Gamma primary, CLOB partial. Order book read is CLOB only. Trade history is CLOB primary, Gamma partial for aggregates. WebSocket live updates are CLOB only. The chart uses green ticks for primary coverage, amber for partial, and dashes for not available. Capability CLOB API Gamma API Order placement primary Order cancellation primary Market discovery partial primary Order book read primary Trade history primary aggregates Live WebSocket primary Rule of thumb: write paths and live books are CLOB; metadata and discovery are Gamma. A bot that calls Gamma for orders or CLOB for discovery is fighting the platform.
Capability split between the two Polymarket API surfaces. Treat CLOB as the trading and live-data surface and Gamma as the catalogue. The partial entry for market discovery on CLOB reflects that the CLOB /markets endpoint does list markets, but without the rich metadata Gamma carries; the aggregates entry under trade history on Gamma reflects that volume and turnover roll-ups live there even though raw fills do not.

The capability map collapses a lot of confused first-week questions into a single glance. “Where do I get the question text for a market?” Gamma. “Where do I subscribe to live trades?” CLOB WebSocket. “Where do I look up my own fills?” CLOB authenticated. “Where do I search by tag?” Gamma. Once the split is internalised the rest of the API surface is small.

Gotchas worth knowing before you ship

Five gotchas are worth surfacing because every developer hits them and the docs do not flag them.

Token ID versus condition ID. A market has one condition ID (the on-chain identifier of the resolution condition) and multiple token IDs (one per outcome). Orders go against token IDs; market metadata uses condition IDs. The pair is documented but easy to confuse, and a bot that submits an order with a condition ID where a token ID is expected gets an unhelpful 400. Always log both when constructing an order.

Tick size and minimum size vary per market. Most markets use a 0.01 tick and a 5 USDC minimum, but a small fraction of markets use 0.001 tick or larger minimums. Read the tick size from /markets/{condition_id} at order construction time rather than hard-coding. Submitting a 0.425 price on a 0.01-tick market is rejected; submitting it on a 0.001-tick market is accepted.

Order expiration is required for GTD. The expiration field is a Unix timestamp in seconds, not milliseconds. Passing a millisecond timestamp creates an order that has already expired and is rejected silently as cancelled. This costs about half a day of debugging the first time it happens.

The WebSocket does not replay. When the connection drops, the bot does not get the missed updates on reconnect; it has to re-fetch snapshots from REST and rebuild the cache. A bot that assumes durable streaming will silently diverge from the real book and produce phantom signals. The reconnect path is not optional.

Nonces matter. The order builder includes a nonce that, combined with the wallet address, must be unique. The official client generates this for you; a hand-rolled signer that reuses nonces (for example by reading from a database that has not yet been flushed) will see orders rejected as duplicates. If the bot maintains its own nonce, persist it to disk synchronously before signing.

None of these are showstoppers and all of them are obvious in hindsight. They are listed because they are the difference between a bot that works in a week and a bot that limps for three. The companion trading bot tutorial covers the full pipeline from API client to strategy to deployment; this guide is intentionally scoped to the API surface itself.

Frequently asked questions

Is the Polymarket API free to use?

Yes. The Polymarket API has no subscription fee. Read endpoints are unauthenticated and open to anyone, and authenticated trading endpoints cost only the standard taker or maker fees on filled orders plus Polygon gas at settlement. Rate limits apply per IP for unauthenticated reads and per API key for authenticated calls, but the budgets are generous enough for any reasonable bot workload.

What is the difference between the CLOB and Gamma APIs?

CLOB at clob.polymarket.com is the trading surface: order placement, cancellation, order book reads, trade history, and the live WebSocket. Gamma at gamma-api.polymarket.com is the metadata surface: events, markets with rich descriptions, tags, categories, and search. A typical bot uses Gamma for discovery and watchlist construction, then talks to CLOB for everything to do with the actual order book and trades.

Do I need to sign every order with my wallet?

Yes, every order body is signed with the wallet private key using an EIP-712 typed-data signature. The HTTP request itself is authenticated separately with an L2 API key plus HMAC, which the trader derives once from the wallet. The two layers exist so that the bot does not round-trip a fresh wallet signature for the HTTP transport on every call; the wallet signature covers the order parameters, and the L2 key covers the HTTP envelope.

How do I handle WebSocket disconnects?

The Polymarket WebSocket does not replay missed events on reconnect. The correct pattern is a reconnect loop that, on a successful resume, fetches a fresh order book snapshot from the REST /book endpoint for every subscribed token before re-arming the live cache. A bot that skips this step will silently diverge from the real book and produce phantom signals; reconnect handling is not optional for any production deployment.

What rate limits should I expect?

Public read endpoints on CLOB and Gamma are rate-limited per IP, roughly in the range of 10 to 20 requests per second sustained with a small burst allowance. Authenticated CLOB endpoints are rate-limited per API key, with a noticeably more generous budget for order placement and cancellation than for catalogue reads. Exact numbers shift over time; treat the limiter as a black box and back off on 429 responses with an exponential retry capped at about one minute.