Polymarket API Deep Dive: Gamma vs CLOB JSON Format Explained
Polymarket Has Two APIs. Most Devs Hit the Wrong One.
If you’ve ever typed polymarket api into a search bar, you’ve probably been bounced between three GitHub repos, two subdomains, and a docs.polymarket.com page that assumes you already know what a condition ID is. It’s not you — Polymarket’s public surface is genuinely split into two different services that do different things, and they don’t share a schema.
This post is the map. By the end you’ll know:
- What Gamma is, what CLOB is, and which one you actually want.
- The exact JSON shape each one returns (captured live, not from memory).
- Why Polymarket returns share prices (0.00–1.00) while every sportsbook on earth returns decimal odds, and how to convert between them.
- How to skip all of it and pull Polymarket alongside Pinnacle and Bet365 in a single HTTP call.
No enterprise account. No wallet signing. Just requests.
The Landscape: Gamma vs CLOB
Polymarket runs on Polygon as an on-chain CLOB (Central Limit Order Book), but on-chain data is painful to query directly. So Polymarket exposes two off-chain APIs that wrap different slices of the same market state:
| Service | Base URL | Purpose | Auth |
|---|---|---|---|
| Gamma | https://gamma-api.polymarket.com |
Read-only discovery: events, markets, metadata, volume, last trade. The “catalogue.” | None |
| CLOB | https://clob.polymarket.com |
Order book depth, live price, historical prices, trade placement. | None for reads; EIP-712 + L2 headers for writes. |
Rule of thumb: if you want to know what exists, hit Gamma. If you want the live book or historical price curve for a specific outcome, hit CLOB. Most devs start at CLOB because that’s where the “trading” verbs live, then get stuck because CLOB wants a 77-digit token_id they don’t have. You get token IDs from Gamma.
The four ID types (and why you need all of them)
Polymarket’s schema uses four different identifiers and you will confuse them at some point:
| ID | Example | Where it comes from | What it refers to |
|---|---|---|---|
| Event ID | 16167 |
Gamma /events |
A group of related markets (e.g. “US Election 2028”) |
| Market slug | russia-ukraine-ceasefire-before-gta-vi-554 |
Gamma /markets |
Human-readable market handle (used in URLs) |
| Condition ID | 0x9c1a953f…5cb5763 |
Gamma markets[].conditionId |
On-chain market hash (used to group outcome tokens) |
Token ID (a.k.a. asset_id) |
85014971590839487… (77 digits) |
Gamma markets[].clobTokenIds |
ERC-1155 position for one specific outcome (Yes or No). This is what CLOB endpoints want. |
A typical binary market (“Will X happen by Y?”) has one condition ID and two token IDs — one for Yes, one for No. Multi-outcome markets (“Who wins the 2028 election?”) are usually modelled as a bundle of linked binary markets, each with its own condition ID and token pair.
Gamma API: The Catalogue
Gamma is unauthenticated, REST, and returns plain JSON. The two endpoints you’ll use 90% of the time are /events and /markets.
Listing active markets
import requests, json
GAMMA = "https://gamma-api.polymarket.com"
r = requests.get(f"{GAMMA}/markets", params={
"limit": 5,
"active": "true",
"closed": "false",
})
markets = r.json()
for m in markets:
# outcomes / outcomePrices / clobTokenIds are stringified JSON arrays
outcomes = json.loads(m["outcomes"])
prices = json.loads(m["outcomePrices"])
tokens = json.loads(m["clobTokenIds"])
print(f"{m['question']}")
for o, p, t in zip(outcomes, prices, tokens):
print(f" {o}: {p} (token {t[:12]}…)")
The stringified-array quirk is real. Gamma ships outcomes, outcomePrices, and clobTokenIds as JSON inside strings, not as native arrays. You must json.loads them or you’ll be indexing into a character string and wondering why “Yes” is the letter Y.
A real market object (captured April 11, 2026, trimmed):
{
"id": "540816",
"question": "Russia-Ukraine Ceasefire before GTA VI?",
"conditionId": "0x9c1a953fe92c8357f1b646ba25d983aa83e90c525992db14fb726fa895cb5763",
"slug": "russia-ukraine-ceasefire-before-gta-vi-554",
"outcomes": "[\"Yes\", \"No\"]",
"outcomePrices": "[\"0.535\", \"0.465\"]",
"clobTokenIds": "[\"8501497159083948713316135768103773293754490207922884688769443031624417212426\", \"2527312495175492857904889758552137141356236738032676480522356889996545113869\"]",
"volume": "1480024.8395290007",
"liquidity": "74814.5449",
"lastTradePrice": 0.54,
"bestBid": 0.53,
"bestAsk": 0.54,
"active": true,
"closed": false
}
Every price in that payload is a share price in the range [0, 1]. Think of it as “what the market says the probability is.” 0.535 means the market is pricing a Yes outcome at 53.5%. We’ll convert it to decimal odds in a minute.
Listing events and drilling down
Events wrap one or more markets. Use them when you want the full context for a topic:
r = requests.get(f"{GAMMA}/events", params={
"limit": 3,
"active": "true",
"closed": "false",
})
for ev in r.json():
print(f"{ev['title']} (vol ${float(ev['volume']):,.0f})")
for m in ev.get("markets", []):
print(f" - {m['question']} [condition {m['conditionId'][:10]}…]")
Gamma quirks worth knowing:
active=true&closed=falseis the correct filter for “tradeable right now.”activealone will still include resolved markets.- Volume is a string. Cast to
floatbefore sorting. - Gamma doesn’t have a stable pagination cursor; use
limit+offsetand don’t assume stable ordering between calls.
CLOB API: The Order Book
Once you have a token_id from Gamma, CLOB gives you the depth and price history for that specific outcome. All read endpoints are unauthenticated.
Fetching the order book
CLOB = "https://clob.polymarket.com"
# Yes token from the market above
YES_TOKEN = "8501497159083948713316135768103773293754490207922884688769443031624417212426"
book = requests.get(f"{CLOB}/book", params={"token_id": YES_TOKEN}).json()
print(f"tick_size: {book['tick_size']} last trade: {book['last_trade_price']}")
print("top 3 bids:", book["bids"][:3])
print("top 3 asks:", book["asks"][:3])
Real response (captured April 11, 2026, trimmed):
{
"market": "0x9c1a953fe92c8357f1b646ba25d983aa83e90c525992db14fb726fa895cb5763",
"asset_id": "8501497159083948713316135768103773293754490207922884688769443031624417212426",
"timestamp": "1775923619253",
"tick_size": "0.01",
"neg_risk": false,
"last_trade_price": "0.540",
"bids": [
{"price": "0.01", "size": "1041530.39"},
{"price": "0.02", "size": "110"},
{"price": "0.03", "size": "23200"}
],
"asks": [
{"price": "0.99", "size": "530924.17"},
{"price": "0.98", "size": "792.33"},
{"price": "0.97", "size": "7250"}
]
}
Two things to notice. First, every number is a string. CLOB does this so clients don’t lose precision on big integers. Cast to Decimal or float before doing math. Second, the bids and asks you see at the edges (0.01 and 0.99) are deep-book liquidity parked far from the mid — the real spread is found by iterating inward until cumulative size crosses whatever fill size you care about. CLOB does not give you a “touch” endpoint; you compute the touch yourself from the book.
Price, midpoint, and history
# Best buy / sell
buy = requests.get(f"{CLOB}/price", params={"token_id": YES_TOKEN, "side": "BUY"}).json()
sell = requests.get(f"{CLOB}/price", params={"token_id": YES_TOKEN, "side": "SELL"}).json()
mid = requests.get(f"{CLOB}/midpoint", params={"token_id": YES_TOKEN}).json()
print(f"buy={buy['price']} sell={sell['price']} mid={mid['mid']}")
# Historical price curve
hist = requests.get(f"{CLOB}/prices-history", params={
"market": YES_TOKEN, # note: this param is named "market" but it takes a token_id
"interval": "1d",
"fidelity": "60", # 60-minute buckets
}).json()
print(f"{len(hist['history'])} price points")
print("first point:", hist["history"][0])
Real response from /prices-history:
{
"history": [
{"t": 1775840401, "p": 0.535},
{"t": 1775847653, "p": 0.535},
{"t": 1775869248, "p": 0.535}
]
}
Each point is a Unix timestamp + a share price. interval accepts 1h, 6h, 1d, 1w, 1m, max. fidelity is the bucket size in minutes and is advisory — if you ask for fidelity=1 on interval=max, CLOB will happily downsample for you.
Out of scope for this post: placing orders. That requires an EIP-712 signed payload + an L2 auth header derived from a funded Polygon wallet, plus proxy-wallet approvals. The official Polymarket docs cover that flow; skip it unless you actually plan to trade.
JSON Format Head-to-Head
Here is the same concept — “what’s the price of this outcome right now?” — answered by Gamma, CLOB, and OddsPapi. The takeaway is that the first two make you do work the third one has already done.
| Source | Endpoint | Path to price | Units |
|---|---|---|---|
| Gamma | /markets?limit=1&slug=… |
json.loads(m["outcomePrices"])[0] |
Share (0–1) |
| CLOB | /price?token_id=…&side=BUY |
json["price"] (string) |
Share (0–1) |
| OddsPapi | /v4/odds?fixtureId=… |
bookmakerOdds["polymarket"]["markets"][mid]["outcomes"][oid]["players"]["0"]["price"] |
Decimal (≥ 1) |
Three endpoints, three shapes, three unit conventions. If you want to compare Polymarket’s implied probability against Pinnacle’s sharp line, options 1 and 2 force you to do the conversion yourself for every call. Option 3 does it for you before the JSON ever leaves the server.
The Normalization Problem
Polymarket prices are share prices. Sportsbooks quote decimal odds. If you want to say “Polymarket thinks Trump is 62% to win, DraftKings thinks he’s 65%,” you have to unify the units first.
The math is one line in each direction:
def share_to_decimal(share: float) -> float:
"""0.535 -> 1.8691..."""
return 1 / share
def decimal_to_share(decimal: float) -> float:
"""1.87 -> 0.5347..."""
return 1 / decimal
def implied_pct(price, is_decimal=True):
"""Return implied probability as a percentage."""
return round((1 / price if is_decimal else price) * 100, 2)
# Gamma gives share: 0.535
print(implied_pct(0.535, is_decimal=False)) # -> 53.5
# Decimal odds equivalent:
print(share_to_decimal(0.535)) # -> 1.8691588...
# Sportsbook gives decimal: 1.87
print(implied_pct(1.87)) # -> 53.48
This is trivial until you need to do it for every outcome of every market, every 30 seconds, while also stripping overround, handling stale quotes, and matching Polymarket market slugs to sportsbook fixtures that spell team names differently. At that point you’re building an aggregation service, which is the part you probably wanted to skip.
Or: Skip Gamma and CLOB Entirely
OddsPapi aggregates Polymarket alongside 350+ sportsbooks and normalizes every price to decimal odds before you see it. The same endpoint that serves Pinnacle, Bet365, and DraftKings serves polymarket:
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
r = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY,
"fixtureId": "id1100168067058948",
"bookmakers": "polymarket,pinnacle,bet365",
})
bo = r.json()["bookmakerOdds"]
# Moneyline market is 111 in basketball. Outcome 111 = home, 112 = away.
for slug in ("polymarket", "pinnacle", "bet365"):
if slug not in bo:
continue
market = bo[slug]["markets"].get("111", {})
for oid in ("111", "112"):
price = market.get("outcomes", {}).get(oid, {}).get("players", {}).get("0", {}).get("price")
print(f"{slug:12s} outcome {oid}: {price}")
Eight lines of business logic, one HTTP call, already in decimal odds, already keyed by the same market ID as every other book in the response. No token IDs, no stringified arrays, no casting prices from string to Decimal, no second endpoint for history (/v4/historical-odds is the same shape).
Polymarket-specific data lives in exchangeMeta
Because Polymarket is an exchange, OddsPapi keeps the lay price and the opposite-side token on the outcome object under exchangeMeta:
{
"price": 1.342,
"active": false,
"bookmakerOutcomeId": "221285127206339595430079217709898417...",
"changedAt": "2026-04-11T11:24:10.509988+00:00",
"exchangeMeta": {
"lay": 3.922,
"bookmakerLayOutcomeId": "788357230734469575963529682307211026..."
}
}
Sharp bettors can read this as “back Yes at 1.342, lay Yes at 3.922” — which is exactly how you’d read a Betfair quote, except the counterparty is a pool of Polymarket LPs instead of another punter. The bookmakerOutcomeId and bookmakerLayOutcomeId fields are Polymarket token IDs — drop them straight into CLOB if you need deep-book context.
A quick honesty note on sports coverage
Polymarket is a prediction market, not a sportsbook. Its depth on day-to-day sports fixtures (mid-table soccer, pre-playoff NBA, MLB regular season) is thin and often inactive — the volume concentrates on headline games, majors, elections, crypto, and culture markets. If you query /v4/odds for an average soccer fixture and expect polymarket in bookmakerOdds, you’ll be disappointed most of the time. For cross-book sharp-vs-soft analysis, pin your Polymarket usage to markets where the platform actually has liquidity.
Free Historical Data (Sidebar)
CLOB’s /prices-history gives you share-price candles for a single token_id at a time, which is fine for one market but painful when you want to backtest a model against, say, every NBA playoff moneyline next to the matching Pinnacle line. You’d have to reconcile Polymarket slugs against sportsbook fixture IDs by hand.
OddsPapi’s /v4/historical-odds endpoint returns the full price history for a fixture across any set of books in one shape. Every tier — including the free tier — gets historical access. Pull the same market from Polymarket and Pinnacle side by side, with a shared timestamp axis and shared market IDs, in one loop. That’s the part most competitor APIs charge for and the part that makes calibration analysis actually tractable. We wrote this up in detail in the prediction market accuracy backtest if you want to see the pattern end-to-end.
Where to Go From Here
- Building a live tracker? See Prediction Market Dashboard: Build a Live Tracker with Python & Streamlit.
- Want the CLI version? Prediction Market Terminal: Build a CLI Trading Monitor with Python.
- Cross-market arbitrage? Polymarket Arbitrage: Find Arbs Between Prediction Markets and Your Sportsbook.
- Market-making niche Polymarket books with sportsbook edge? Market Making on Polymarket.
- Background on Polymarket + Kalshi together? Polymarket API & Kalshi API: Python Guide to Prediction Market Data.
- Pricing context? Odds API Pricing in 2026: 4 Providers Compared.
Stop Stitching Two APIs
Gamma + CLOB is a fine stack if you’re building inside Polymarket’s ecosystem. But if your job is “compare prediction market prices against 350+ sportsbooks, in decimal, with one schema, and backtest it for free” — that’s a different product, and that’s what OddsPapi is for.
Get your free API key and query polymarket alongside Pinnacle, Bet365, DraftKings, FanDuel, and 350+ more in a single call.
FAQ
What’s the difference between Gamma and CLOB?
Gamma is Polymarket’s read-only catalogue API — events, markets, volume, last trade. CLOB is the order book and trading API — depth, best bid/ask, historical prices, and order placement. You almost always need both: Gamma to discover which markets exist and fetch their clobTokenIds, then CLOB for live depth and history on specific outcomes.
Is the Polymarket API free?
Yes. Both Gamma and CLOB read endpoints are unauthenticated and free. Placing orders requires a funded Polygon wallet and EIP-712 signed payloads, but reading data has no key, no plan, and no paywall — rate limits are undocumented but in practice generous for polling.
Can I place Polymarket trades via OddsPapi?
No. OddsPapi is strictly read-only and data-aggregation focused. For order placement you need Polymarket’s CLOB directly with an EIP-712 signer and a funded Polygon wallet. OddsPapi gives you the data layer; execution stays with you.
Where do I get historical Polymarket odds?
Two options. Directly: Polymarket CLOB’s /prices-history endpoint accepts a token_id and returns share-price candles for that one outcome. Via OddsPapi: /v4/historical-odds?fixtureId=…&bookmakers=polymarket,pinnacle,bet365 returns the full price history for every requested book in a single response, already normalized to decimal odds, on a shared timestamp axis. The latter is free-tier accessible.
What format are Polymarket prices in?
Gamma and CLOB return share prices in the range [0, 1], which you can read as implied probability. 0.535 means the market is pricing that outcome at 53.5%. OddsPapi converts these to decimal odds (1 / share_price, so 0.535 → 1.869) so Polymarket can be compared directly to sportsbook quotes. Both Polymarket endpoints return prices as JSON strings, not numbers — cast before doing math.
Are Polymarket outcomes and prices returned as arrays?
Not exactly. Gamma’s /markets endpoint ships outcomes, outcomePrices, and clobTokenIds as stringified JSON arrays — i.e. strings that contain JSON that you then have to json.loads a second time. Miss this and you’ll end up iterating over characters instead of values.