Polymarket API: Trace 28 NFL Tokens From Gamma to CLOB

Polymarket API Deep Dive - OddsPapi API Blog
How To Guides April 16, 2026

Polymarket Has Two APIs, and the Order Book Reads Upside Down

Search polymarket api and you land on two subdomains and a docs site that assumes you know what a condition ID is. Polymarket splits its public data across two services. Gamma is the catalogue. CLOB is the order book. They do not share a schema, and you need both.

This post maps the two services with payloads captured on 15 October 2026. It then traces one token from Gamma to CLOB to a decimal-odds feed. We ran that trace on all 14 NFL Week 7 fixtures, 28 outcomes in total, and every top-of-book price and size matched to the share. You will learn:

  • What Gamma and CLOB each return, and which one holds the number you want.
  • Three parser traps we hit on live data: the stringified arrays, the worst-first order book, and a /price endpoint whose BUY side returns the bid.
  • How to convert share prices (0 to 1) to decimal odds and back.
  • How to pull Polymarket next to Pinnacle and bet365 in one call, then check that feed against CLOB yourself.

Reads need no key and no wallet. You only need requests.

The Landscape: Gamma vs CLOB

Polymarket settles on Polygon, but you do not query the chain. Two off-chain APIs wrap the market state:

Service Base URL Purpose Auth
Gamma https://gamma-api.polymarket.com Discovery: events, markets, metadata, volume, mid price. The catalogue. None
CLOB https://clob.polymarket.com Order book depth, best bid and ask, midpoint, price history, order placement. None for reads. Signed EIP-712 payloads plus L2 headers for writes.

Rule of thumb: Gamma tells you what exists. CLOB tells you what you can trade at. Most developers start at CLOB and then stall, because every CLOB endpoint wants a 77-digit token_id. Gamma hands you the token IDs.

The four ID types

ID Example (Chargers v Chiefs) Where it comes from What it refers to
Event slug nfl-lac-kc-2026-10-18 Gamma /events The fixture. This event holds 308 markets: moneyline, spreads, totals.
Market ID 4329891 Gamma /markets One question, here “Chargers vs. Chiefs”.
Condition ID 0xa70da744…b546 Gamma markets[].conditionId The on-chain market hash. CLOB returns it as market.
Token ID (asset_id) 58167714289864… (77 digits) Gamma markets[].clobTokenIds One outcome. Every CLOB endpoint wants this.

A two-way market has one condition ID and two token IDs. On an NFL moneyline the tokens are the two participants, not Yes and No.

Gamma API: The Catalogue

Gamma is plain REST. You will use /markets and /events most of the time.

Listing active markets

import requests, json

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

r = requests.get(f"{GAMMA}/markets", params={
    "limit": 3,
    "active": "true",
    "closed": "false",
})
for m in r.json():
    # outcomes / outcomePrices / clobTokenIds are stringified JSON arrays
    outcomes = json.loads(m["outcomes"])
    prices   = json.loads(m["outcomePrices"])
    tokens   = json.loads(m["clobTokenIds"])
    print(m["question"])
    for o, p, t in zip(outcomes, prices, tokens):
        print(f"  {o}: {p}  (token {t[:12]}...)")

Output on 15 October 2026:

Xi Jinping out before 2027?
  Yes: 0.0285  (token 323382201900...)
  No: 0.9715  (token 256593106749...)
Will Gavin Newsom win the 2028 Democratic presidential nomination?
  Yes: 0.1545  (token 545330438199...)
  No: 0.8455  (token 878541741480...)
Will Alexandria Ocasio-Cortez win the 2028 Democratic presidential nomination?
  Yes: 0.1685  (token 107064985435...)
  No: 0.8315  (token 651760722613...)

Trap 1: the stringified arrays. Gamma ships outcomes, outcomePrices and clobTokenIds as JSON inside strings. Skip the json.loads and you iterate over characters, so the first outcome becomes the letter [.

The Chiefs moneyline as Gamma returns it (captured 15 Oct 2026, 13:13 UTC, trimmed from 90+ keys):

{
  "id": "4329891",
  "question": "Chargers vs. Chiefs",
  "conditionId": "0xa70da744210533416edb26fb4ee19ea7a7acdaef6daabb5077c4dec89494b546",
  "slug": "nfl-lac-kc-2026-10-18",
  "outcomes": "[\"Chargers\", \"Chiefs\"]",
  "outcomePrices": "[\"0.215\", \"0.785\"]",
  "clobTokenIds": "[\"25975613657668487250673927756557799100593582472155421789800705293576427824049\", \"58167714289864717825087991545150213770744040987591942526721915426213133665357\"]",
  "volume": "3512.5369280000004",
  "liquidity": "149784.2895",
  "lastTradePrice": 0.21,
  "bestBid": 0.21,
  "bestAsk": 0.22,
  "spread": 0.01,
  "negRisk": false,
  "active": true,
  "closed": false,
  "orderPriceMinTickSize": 0.01,
  "orderMinSize": 5
}

Three details matter here. outcomePrices holds the midpoint (0.785 for the Chiefs), not a price you can fill. bestBid and bestAsk belong to the first outcome only, so 0.21 / 0.22 is the Chargers. And Gamma lists the away participant first: “Chargers vs. Chiefs” for a Chiefs home fixture. Gamma did this on 14 of 14 Week 7 fixtures. If you zip Gamma outcomes against a feed that puts the home side first, you flip every price. Match on the token ID, never on position.

Finding an event from its slug

import requests

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

# fixturePath from OddsPapi ends in the Gamma event slug
ev = requests.get(f"{GAMMA}/events", params={"slug": "nfl-lac-kc-2026-10-18"}).json()[0]
print(ev["title"], "|", len(ev["markets"]), "markets")
for m in ev["markets"][:4]:
    print("  -", m["question"], "| condition", m["conditionId"][:10] + "...")
Chargers vs. Chiefs | 308 markets
  - Chargers vs. Chiefs | condition 0xa70da744...
  - Spread: Chiefs (-0.5) | condition 0x97aa81e2...
  - Spread: Chiefs (-1.5) | condition 0x1f349328...
  - Spread: Chiefs (-2.5) | condition 0xd0071583...

Gamma notes:

  • active=true&closed=false filters to tradeable markets. active alone still returns resolved ones.
  • Market volume is a string, but event volume is a number. Wrap both in float() before you sort.
  • /markets?clob_token_ids=<token> takes you from a token back to its market. It resolved 28 of 28 NFL tokens in our run.
  • Paginate with limit and offset, and do not assume a stable order between calls.

CLOB API: The Order Book

CLOB serves depth and history for one token at a time. Read endpoints need no auth.

import requests

CLOB = "https://clob.polymarket.com"
CHIEFS = "58167714289864717825087991545150213770744040987591942526721915426213133665357"

book = requests.get(f"{CLOB}/book", params={"token_id": CHIEFS}).json()
# both sides arrive worst-first: the touch is the LAST element
best_bid, best_ask = book["bids"][-1], book["asks"][-1]
print("tick", book["tick_size"], "| min order", book["min_order_size"], "| neg_risk", book["neg_risk"])
print("best bid", best_bid, "| best ask", best_ask)

# side=BUY returns the best BID, side=SELL the best ASK
buy  = requests.get(f"{CLOB}/price", params={"token_id": CHIEFS, "side": "BUY"}).json()
sell = requests.get(f"{CLOB}/price", params={"token_id": CHIEFS, "side": "SELL"}).json()
mid  = requests.get(f"{CLOB}/midpoint", params={"token_id": CHIEFS}).json()
print("BUY", buy["price"], "| SELL", sell["price"], "| mid", mid["mid"])

hist = requests.get(f"{CLOB}/prices-history", params={
    "market": CHIEFS,     # named "market", takes a token_id
    "interval": "1d",
    "fidelity": "60",     # 60-minute buckets
}).json()["history"]
print(len(hist), "points, first", hist[0])
tick 0.01 | min order 5 | neg_risk False
best bid {'price': '0.78', 'size': '39472.87'} | best ask {'price': '0.79', 'size': '2495.56'}
BUY 0.78 | SELL 0.79 | mid 0.785
24 points, first {'t': 1791986416, 'p': 0.785}

Trap 2: the book reads worst-first. bids run from 0.01 up to 0.78, and asks run from 0.99 down to 0.79. The first rows you print are deep, far-from-the-money orders. The touch sits at index -1 on both sides. Every number is a string, so cast before you do math.

Trap 3: side=BUY returns the bid. The side names the resting orders, not your trade. If you want to buy the Chiefs now, you pay the SELL price, 0.79. A bot that reads BUY as “my cost to buy” quotes one tick too cheap on every market.

/prices-history returns {t, p} points, where p is the midpoint. interval accepts 1h, 6h, 1d, 1w, 1m and max. fidelity is the bucket size in minutes.

Order placement is out of scope. It needs a funded Polygon wallet, an EIP-712 signature and L2 headers; Polymarket documents the flow in Place Orders. Baskets with neg_risk: true (multi-outcome events) behave differently again; our negative-risk post sizes one at the bid.

One Price, Three Shapes

Source Endpoint Path to the price you can fill Units
Gamma /markets?slug=… None. outcomePrices is the mid; bestAsk covers outcome 0 only. Share (0 to 1)
CLOB /book?token_id=… book["asks"][-1]["price"] (string) Share (0 to 1)
OddsPapi /v4/odds?fixtureId=… bookmakerOdds["polymarket"]["markets"]["141"]["outcomes"][oid]["players"]["0"]["price"] Decimal (1 or more)

Share Prices to Decimal Odds

Polymarket quotes shares. Sportsbooks quote decimal odds. One line converts each way:

def share_to_decimal(share: float) -> float:
    """0.79 -> 1.2658..."""
    return 1 / share

def decimal_to_share(decimal: float) -> float:
    """1.266 -> 0.7899..."""
    return 1 / decimal

def implied_pct(price, is_decimal=True):
    """Implied probability as a percentage."""
    return round((1 / price if is_decimal else price) * 100, 2)

print(implied_pct(0.79, is_decimal=False))   # -> 79.0
print(round(share_to_decimal(0.79), 3))      # -> 1.266
print(implied_pct(1.244))                    # -> 80.39 (Pinnacle, same outcome)

The math is the easy part. The work is doing it for every outcome on every fixture, matching Polymarket slugs to sportsbook fixtures, and keeping the home and away sides straight when Gamma lists them in reverse. For a converter that also covers American and fractional odds, see Odds Converter in Python.

Skip the Plumbing: Polymarket Next to 350+ Books

OddsPapi carries polymarket as a bookmaker slug among 350+ books and converts every price to decimal odds. It keys Polymarket on the same market IDs as Pinnacle and bet365:

import requests

API_KEY = "YOUR_API_KEY"
BASE    = "https://api.oddspapi.io/v4"

r = requests.get(f"{BASE}/odds", params={
    "apiKey": API_KEY,
    "fixtureId": "id1400003171515918",          # Chiefs v Chargers, NFL Week 7
    "bookmakers": "polymarket,pinnacle,bet365",
})
bo = r.json()["bookmakerOdds"]

# NFL moneyline is market 141. Outcome 141 = participant 1 (Chiefs), 142 = Chargers.
for slug in ("polymarket", "pinnacle", "bet365"):
    outcomes = bo.get(slug, {}).get("markets", {}).get("141", {}).get("outcomes", {})
    for oid in ("141", "142"):
        p = outcomes.get(oid, {}).get("players", {}).get("0", {})
        print(f"{slug:11s} {oid}: {p.get('price')}  limit={p.get('limit')}")
polymarket  141: 1.266  limit=1971.49
polymarket  142: 4.545  limit=8684.03
pinnacle    141: 1.244  limit=18442.0
pinnacle    142: 4.28  limit=4500.0
bet365      141: 1.18  limit=None
bet365      142: 4.6  limit=None

On that board Polymarket ran the moneyline at a 0.99% margin, Pinnacle at 3.75% and bet365 at 6.48%. Polymarket held the best Chiefs price and bet365 the best Chargers price. limit on Polymarket is the stake you can place at the top price; bet365 publishes none. For margins and depth across 88 fixtures and three prediction venues, read Polymarket API & Kalshi API: Compare 1% Markets to Pinnacle.

The CLOB book lives in exchangeMeta

Each Polymarket outcome carries a three-level ladder:

"exchangeMeta": {
  "back": [ {"cents": 0.79, "price": 1.266, "size": 2495.56, "limit": 1971.49}, ... x3 ],
  "lay":  [ {"cents": 0.22, "price": 4.545, "size": 39472.87, "limit": 8684.03}, ... x3 ],
  "bookmakerLayOutcomeId": "25975613657668487250673927756557799100593582472155421789800705293576427824049"
}

Read it like this. cents is the native share price and price is 1 / cents. size is shares, which is also the payout in dollars. limit is the stake: size × cents. The outcome’s bookmakerOutcomeId is its CLOB token, and bookmakerLayOutcomeId is the other participant’s token. Other exchanges on OddsPapi use different ladder shapes, so parse each venue on its own terms.

Check the feed against CLOB yourself

The token IDs let you audit the decimal feed against the source book:

import requests

API_KEY = "YOUR_API_KEY"
CLOB    = "https://clob.polymarket.com"

odds = requests.get("https://api.oddspapi.io/v4/odds", params={
    "apiKey": API_KEY,
    "fixtureId": "id1400003171515918",
    "bookmakers": "polymarket",
}).json()
pm = odds["bookmakerOdds"]["polymarket"]
print("event:", pm["fixturePath"])

for oid, oc in pm["markets"]["141"]["outcomes"].items():
    p    = oc["players"]["0"]
    meta = p["exchangeMeta"]
    back, lay = meta["back"][0], meta["lay"][0]
    book = requests.get(f"{CLOB}/book", params={"token_id": p["bookmakerOutcomeId"]}).json()
    ask, bid = book["asks"][-1], book["bids"][-1]
    print(f"outcome {oid}: back {back['cents']} x {back['size']:,.2f} | CLOB ask {ask['price']} x {ask['size']}")
    print(f"             lay  {lay['cents']} x {lay['size']:,.2f} | CLOB bid {bid['price']} x {bid['size']}"
          f"  (1 - bid = {1 - float(bid['price']):.2f})")
    print(f"             limit {back['limit']:,.2f} = size x cents {back['size'] * back['cents']:,.2f}")
event: https://polymarket.com/event/nfl-lac-kc-2026-10-18
outcome 141: back 0.79 x 2,495.56 | CLOB ask 0.79 x 2495.56
             lay  0.22 x 39,472.87 | CLOB bid 0.78 x 39472.87  (1 - bid = 0.22)
             limit 1,971.49 = size x cents 1,971.49
outcome 142: back 0.22 x 39,472.87 | CLOB ask 0.22 x 39472.87
             lay  0.79 x 2,495.56 | CLOB bid 0.21 x 2495.56  (1 - bid = 0.79)
             limit 8,684.03 = size x cents 8,684.03

The output shows how Polymarket builds a two-way market. A bid for the Chiefs at 0.78 is the same order as an ask for the Chargers at 0.22, so the two tokens share one book. OddsPapi’s back ladder is the token’s ask side. Its lay ladder is the token’s bid side, quoted as the other participant’s price.

We ran this check on all 14 NFL Week 7 fixtures on 15 October 2026, 13:09 UTC, with about one second between the OddsPapi call and each CLOB call:

Check Matched
bookmakerOutcomeId answers on CLOB /book 28 of 28 outcomes
Top back cents and size = CLOB best ask 28 of 28
Top lay cents = 1 minus CLOB best bid, size = bid size 28 of 28
Back levels 2 and 3 = CLOB asks 2 and 3 54 of 56 (two sizes moved inside the one-second gap)
limit = size × cents 168 of 168 ladder levels
price = 1 / cents, three decimals 28 of 28
bookmakerLayOutcomeId = the other outcome’s token 28 of 28
Gamma /markets?clob_token_ids= finds the market 28 of 28

For ladder depth across seven prediction venues on the same sport, see NFL Prediction Market Liquidity. If you want those ladders pushed to you instead of polled, the WebSocket odds API streams them.

Free Historical Polymarket Odds

CLOB’s /prices-history gives you midpoints for one token. OddsPapi’s /v4/historical-odds gives you every price change with the stake behind it, on the free tier. Polymarket has one rule: pass one bookmaker and exactly one outcomeId per call. A call with bookmakers=polymarket,pinnacle returns HTTP 400 INVALID_PARAMETER, so pull Pinnacle in a second call.

import requests

API_KEY = "YOUR_API_KEY"

r = requests.get("https://api.oddspapi.io/v4/historical-odds", params={
    "apiKey": API_KEY,
    "fixtureId": "id1400003171515892",   # Packers v Bears, 11 Oct 2026, 31-7
    "bookmakers": "polymarket",          # polymarket: one book per call...
    "outcomeId": 141,                    # ...and exactly one outcome
})
snaps = r.json()["bookmakers"]["polymarket"]["markets"]["141"]["outcomes"]["141"]["players"]["0"]
snaps.sort(key=lambda s: s["createdAt"])

live  = [s for s in snaps if s["active"] and s["price"] > 1]
close = [s for s in live if s["createdAt"] < "2026-10-11T17:00:00"][-1]
print(len(snaps), "snapshots,", len(live), "active")
print("first active:", live[0])
print("close:       ", close)
18191 snapshots, 17337 active
first active: {'createdAt': '2026-08-31T12:42:37.650Z', 'price': 1.031, 'limit': 21.06, 'active': True, 'exchangeMeta': None}
close:        {'createdAt': '2026-10-11T16:59:57.490Z', 'price': 2.0, 'limit': 80312.12, 'active': True, 'exchangeMeta': None}

The Packers book opened 41 days out at 1.031 with $21 behind it, a placeholder price. It closed three seconds before kick-off at 2.0 with $80,312. Pinnacle closed the same outcome at 2.0 with a $56,250 limit (a second call, bookmakers=pinnacle). Two things to know: exchangeMeta is null on every historical snapshot, so use the snapshot limit for depth, and one Polymarket outcome on one NFL fixture is about 1.9 MB. To score closing prices against results, follow the prediction market accuracy backtest.

Where to Go From Here

One Schema for Polymarket and the Sportsbooks

Gamma plus CLOB is the right stack if you trade inside Polymarket. If you compare Polymarket against sportsbooks, you want the prices in decimal, keyed on shared market IDs, with history you can backtest for free. That is what OddsPapi gives you, and the token IDs let you audit every Polymarket price against CLOB.

Get your free API key and pull polymarket next to Pinnacle, bet365, DraftKings, FanDuel and 350+ more books in one call.

FAQ

What is the difference between Gamma and CLOB?

Gamma is Polymarket's catalogue API: events, markets, volume, midpoint prices and the clobTokenIds for each outcome. CLOB is the order book API: depth, best bid and ask, midpoint, price history and order placement. You use Gamma to find a market's token IDs, then CLOB to read the book for each token.

Is the Polymarket API free?

Yes, for reads. Gamma and the CLOB read endpoints need no key. Placing orders needs a funded Polygon wallet and EIP-712 signed payloads.

Why does CLOB /price with side=BUY return a lower price than I can buy at?

The side parameter names the resting orders. side=BUY returns the best bid and side=SELL returns the best ask. On the Chiefs token on 15 October 2026, BUY returned 0.78 and SELL returned 0.79. To buy now, you pay the SELL price.

Can I place Polymarket trades through OddsPapi?

No. OddsPapi is a read-only data API. For execution you sign orders against Polymarket's CLOB yourself. OddsPapi gives you the bookmakerOutcomeId token, which is the ID CLOB expects.

Where do I get historical Polymarket odds?

Polymarket's CLOB /prices-history returns midpoint points for one token. OddsPapi's /v4/historical-odds returns every Polymarket price change with its stake limit, in decimal odds, on the free tier. For Polymarket, pass one bookmaker and one outcomeId per call; a Packers outcome from 11 October 2026 returned 18,191 snapshots.

What format are Polymarket prices in?

Share prices from 0 to 1, read as implied probability. A Chiefs ask of 0.79 means 79%, or decimal odds of 1 / 0.79 = 1.266. Gamma and CLOB send prices as strings, so cast them before you do math. OddsPapi sends the decimal price and keeps the share price as cents on each ladder level.

Why are Gamma outcomes in a different order from my sportsbook feed?

Gamma lists the away participant first on NFL markets ("Chargers vs. Chiefs" for a Chiefs home fixture); it did so on all 14 Week 7 fixtures. OddsPapi puts participant 1 (the home side) in outcome 141. Match outcomes on the token ID, never on array position.