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 pull historical prices from the Polymarket CLOB

Most empty responses from Polymarket's price history endpoint come from one cause: the parameter named `market` doesn't want a market ID. The second most common is a query shape that doesn't suit the range you're asking for.

API & data · Automation August 9, 2026 3 min read Alex Young Alex Young Technical specialist
Key takeawaysThe market parameter on /prices-history wants the asset (CLOB token) ID, not a market ID or condition ID.
  • The market parameter on /prices-history wants the asset (CLOB token) ID, not a market ID or condition ID.
  • Distinguish the failure modes: invalid filters are rejected with 400, while a well-formed query for a token that never traded returns {"history": []} with a 200.
  • Each market has two tokens with two separate series; get them from Gamma's clobTokenIds, which arrives as a JSON-encoded string and must be decoded first.
  • interval and startTs/endTs are mutually exclusive — for backfills use the explicit range and verify coverage, rather than falling back to coarse fidelity when a query comes back thin.
  • fidelity on /prices-history is an integer in minutes; a separate GET /ohlc with a 1m1w fidelity enum appears in the error reference but isn't yet in the main API index, so verify its contract before relying on it.
  • The price series is {t, p} only — no volume, no OHLCV — so candles built from it describe the observed series, not the trade tape; trade-level data comes from the Data API's /trades at 200 requests per 10 seconds.
  • Depth history is unreliable: the legacy /orderbook-history sits outside the supported API surface and has been reported returning nothing for windows after February 2026 — record the WebSocket feed or use an archival provider.
  • For bulk work use POST /batch-prices-history with up to 20 asset IDs per call; /prices-history allows 1,000 requests per 10 seconds and Gamma's /markets only 300, so batch at both ends.

This summary was created with AI.

What the prices-history endpoint accepts

The price history endpoint is GET https://clob.polymarket.com/prices-history, public and unauthenticated like the rest of the CLOB's read surface. It takes four parameters, and the first one is where nearly everything goes wrong:

Parameter Type Meaning
market string, required The asset ID — the CLOB token, not the market
startTs number Unix timestamp, items after this point
endTs number Unix timestamp, items before this point
interval enum max, all, 1m, 1w, 1d, 6h, 1h
fidelity integer Resolution in minutes

Read the first row again, because the naming is genuinely misleading and the prices-history documentation says it outright: the parameter called market wants the asset id. In Polymarket's data model a market has two outcome tokens — Yes and No — each with its own CLOB token ID, and price history belongs to a token, not to the market. There is no single price series for a market, because the market has two sides.

The response is deliberately minimal:

JSON
{"history": [{"t": 1754400000, "p": 0.63}, {"t": 1754403600, "p": 0.65}]}

A Unix timestamp and a price between 0 and 1, where the price reads as an implied probability. Note what isn't there: no volume, no open, high or low, no trade count. This is a series of price observations, not OHLCV — a distinction that shapes the whole next section.

Two more mechanics worth knowing before you write a loop. interval and the timestamp pair are alternatives rather than companions: use interval for a relative window ending now, or startTs/endTs for an absolute range. And fidelity is expressed in minutes, so fidelity=60 means hourly points and fidelity=1440 daily — not "number of points", which is how several third-party wrappers mislabel it.

Two reasons the response comes back empty

The historical prices endpoint can fail in two different ways, and telling them apart saves time. Malformed or invalid filters are rejected: the error reference documents validation errors for market, startTs, endTs and fidelity, returned as 400 Bad Request. But a request that is syntactically fine and simply names a token that never traded — or a window with nothing in it — comes back as {"history": []} with a 200. So an empty array isn't a guaranteed signal of a wrong identifier; it's the answer to a well-formed question that has no data behind it.

Cause one: you passed a market ID instead of a token ID. This is the naming trap from the previous section. Gamma's market object gives you both: id (a short numeric string like 15345), conditionId (a 0x… hash), and clobTokenIds — a JSON-encoded string holding the two token IDs. Decode that field before use, then pass one of the two long numeric IDs:

Python
import json, requests

GAMMA = "https://gamma-api.polymarket.com"
CLOB = "https://clob.polymarket.com"

def token_ids(slug):
    r = requests.get(f"{GAMMA}/markets", params={"slug": slug}, timeout=15)
    r.raise_for_status()
    market = r.json()[0]
    outcomes = json.loads(market["outcomes"])          # e.g. ["Yes", "No"]
    tokens = json.loads(market["clobTokenIds"])        # JSON-encoded, not a list
    return dict(zip(outcomes, tokens)), market["closed"]

def price_history(token_id, start_ts=None, end_ts=None, fidelity=60, interval=None):
    params = {"market": token_id, "fidelity": fidelity}
    if interval:                                        # relative window ending now
        params["interval"] = interval
    else:                                               # absolute range — preferred for backfill
        params["startTs"], params["endTs"] = start_ts, end_ts
    r = requests.get(f"{CLOB}/prices-history", params=params, timeout=30)
    r.raise_for_status()                                # 400 on invalid filters
    return r.json().get("history", [])

Cause two: the query shape doesn't fit the range. interval and the timestamp pair are mutually exclusive, and for historical backfills the absolute range is the one to reach for. There's a reported case in Polymarket's client repository where a resolved market queried with interval=max returned data at coarse fidelity and an empty array at finer fidelity — with the reporter's own follow-up being to switch to explicit startTs/endTs. Treat that as a query-shape lesson rather than a retention rule: for closed markets, bound the range explicitly around the period the market was actually trading, then verify coverage by counting the points you got against the span you asked for. Don't hardcode a coarse fidelity as a blanket fallback — you'd be discarding detail that may well be available.

A third, simpler cause: a market that never traded has no history to return. Check volumeNum on the Gamma object before concluding something is broken.

Building candles from a price series

Because the response carries only timestamps and prices, historical price data here isn't OHLCV and can't be turned into it faithfully. You can build candles, but you should know exactly what they mean.

Before building your own, check whether you still need to. Polymarket's error reference documents a GET /ohlc endpoint taking asset_id, startTs, limit and a fidelity enum of 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w — a different parameter style from /prices-history, where fidelity is an integer in minutes. The caveat is that it isn't yet carried in the main API reference index the way the other market-data endpoints are, so treat its contract as unconfirmed: probe it, compare a window against /prices-history, and don't build production code on it until you've verified the response shape yourself.

If you do build candles from the price series, know exactly what they mean. With fidelity set fine enough you can group consecutive observations into buckets and take first, max, min and last within each. That yields open, high, low and close of the observed series, not of the trade tape: if two observations bracket a spike that happened between them, the spike never existed as far as your candles are concerned. Volume simply doesn't exist in this data; anything that needs it has to come from the trade level, covered next.

Two practical notes. Prices are probabilities in the 0–1 range, so returns and volatility computed on them behave differently from asset prices — a move from 0.02 to 0.04 is a doubling, and near the bounds the series compresses. And the two tokens of a market are near-complements: the Yes and No series should roughly sum to 1, and the gap between them is a spread artefact rather than a signal. Pulling both and checking that sum is a cheap sanity check on your identifiers — if it lands nowhere near 1, you're probably holding tokens from two different markets.

Going below prices to individual trades

When aggregated prices aren't enough, historical trades are available — from a different host. The Data API's /trades endpoint returns executed trades and accepts filters including the market's condition ID, so the natural path is Gamma for the condition ID, then Data for the fills. Its budget is tighter than the CLOB's — 200 requests per 10 seconds — so trade-level backfills are the slowest part of any pipeline and belong in a background job. The subgraph layer is the other route: the orderbook subgraph indexes on-chain fill events, which suits aggregate analysis better than per-market queries. One caveat there — the public subgraph manifest still lists the original CTF Exchange contracts, while Polymarket moved to CLOB V2 on new exchange contracts in 2026, so an older deployment may not carry complete recent fill history. Check which contracts a deployment indexes before treating it as authoritative.

Historical order book depth is the murkiest corner. A legacy GET /orderbook-history endpoint exists on the CLOB — it appears in Polymarket's error reference and accepts asset_id, startTs, endTs, limit and offset — but it isn't part of the currently supported main API surface, and a February 2026 bug report against a trading framework documents it returning {"count": 0, "data": []} for any window after roughly 20 February 2026, while older history still comes back normally. So it may serve archival depth, and it should not be relied on for anything recent. For dependable new depth history the options are unchanged: record the WebSocket feed yourself going forward, or buy from a provider that has been recording. Discovering this after building a backtest that assumes historical depth is a costly lesson; knowing it in week one is a design constraint like any other.

Bulk pulls across many markets

Historical data for backtesting means thousands of markets, and there the single-token endpoint is the wrong tool. The CLOB documents a batch variant: POST https://clob.polymarket.com/batch-prices-history with a JSON body of markets (a list of asset IDs, maximum 20), plus start_ts, end_ts and fidelity — note the snake_case here against the camelCase of the single endpoint, and that fidelity defaults to 1 minute.

Python
import requests

def batch_history(token_ids, start_ts, end_ts, fidelity=1440):
    assert len(token_ids) <= 20                    # documented ceiling
    r = requests.post(f"{CLOB}/batch-prices-history",
                      json={"markets": token_ids, "start_ts": start_ts,
                            "end_ts": end_ts, "fidelity": fidelity},
                      timeout=60)
    r.raise_for_status()
    return r.json()

That's a 20× reduction in requests for the same data, and it's the single most effective thing you can do for a historical data download at scale. A sweep of 5,000 markets — 10,000 tokens, since each market has two — drops from 10,000 requests to 500.

Sizing the rest of the job: /prices-history has its own line in Polymarket's rate-limit table at 1,000 requests per 10 seconds — not the CLOB host's general 9,000 — and limits are enforced per IP with Cloudflare throttling rather than immediate rejection. The token IDs themselves come from Gamma, which is stricter still: /markets allows 300 requests per 10 seconds. So both ends of the pipeline are constrained, and batching matters at both. Worth noting for a common search: price history on Gamma doesn't exist as an endpoint. Gamma carries current outcomePrices and change-over-horizon fields on the market object, but the series itself lives only on the CLOB.

The practical order of operations, then: page Gamma once with the keyset endpoints to build a local table of markets and their token IDs, cache it (those IDs never change), then walk the batch history endpoint 20 tokens at a time at whatever fidelity your research actually needs — daily candles for a broad study, finer only where it matters. Where the remaining constraint is request volume across a large universe, budgets are counted per IP address, and our proxies for crypto projects are dedicated IPv4 addresses, static for the full plan term, each subject to its own documented allowance. Verify scaling against your own workload rather than assuming it stays linear.