# How the three Polymarket APIs divide the work

> Polymarket runs three REST APIs on three hosts, and picking the wrong one is the most common reason a first integration stalls. Gamma describes what exists, the CLOB prices and trades it, the Data API answers for a wallet.

- Source: https://papaproxy.net/blog/polymarket-three-apis.php
- Published: 2026-08-08
- Author: Alex Young
- Category: API & data · PapaProxy.net Blog

---

## Key takeaways

- Three hosts, three jobs: `gamma-api` for market and event metadata, `clob` for books, prices and trading, `data-api` for anything keyed by a wallet address.
- In Gamma market objects, `outcomes`, `outcomePrices` and `clobTokenIds` are typed as strings and arrive JSON-encoded — decode before indexing, or you'll be comparing characters.
- Join keys hold the pipeline together: `conditionId` links Gamma to the Data API, and the decoded `clobTokenIds` are the CLOB's `token_id` and the Data API's `asset`.
- Query events rather than markets where you can — an event carries its markets nested, halving the round trips.
- Page large sweeps with `/markets/keyset` and `/events/keyset` — cursor-based, `limit` max 100, `offset` rejected with 422, `next_cursor` absent on the last page; the offset endpoints still work but are being deprecated.
- In a CLOB order book, `bids` are sorted price descending and `asks` ascending, so the best quote on each side is index `0`.
- Budgets are separate and IP-based: Gamma 4,000 req/10s, CLOB 9,000, Data API 1,000 (with `/positions` at 150), under a 15,000 global ceiling — so metadata enrichment doesn't compete with position polling.
- Only trading and your own ledger need credentials: L1 signature to derive an API key, then L2 HMAC headers, with the signature type and funder address matched to the account type.

## What Gamma covers

The Gamma API base URL is `https://gamma-api.polymarket.com`, and its job is the catalogue: everything the front end needs to display a market before anyone trades it. The OpenAPI spec marks its routes as public, so no key, wallet or signature is involved.

Two endpoints carry most of the traffic. The Gamma API markets endpoint, `GET /markets`, lists markets with filters that cover nearly every discovery need: `slug`, `condition_ids`, `clob_token_ids`, `id` and `question_ids` for direct lookups; `tag_id` with `related_tags` for category browsing; `liquidity_num_min`/`max` and `volume_num_min`/`max` for size thresholds; `start_date_min`/`max` and `end_date_min`/`max` for time windows; plus `limit`, `offset`, `order` (a comma-separated list of fields) and `ascending` for paging and sorting. The `closed` parameter defaults to `false`, so resolved markets are already excluded unless you ask for them.

The Gamma API events endpoint, `GET /events`, is the one to reach for first in practice. An event is the container — "Fed decision in October" — and it arrives with its `markets` array nested inside, so one event call replaces a market call plus a loop. Polymarket's own guidance says the same: work backwards from events to reduce the number of calls. For direct lookups by URL, `GET /events/slug/{slug}` takes the slug straight out of a Polymarket link.

One field-level trap deserves naming before you write a parser, because it's in the schema rather than in the prose. In the market object, `outcomes`, `outcomePrices` and `clobTokenIds` are typed as **strings**, not arrays — they arrive JSON-encoded, so `market["outcomePrices"][0]` gives you the character `[` rather than a price. Decode them first. Everything else in the object is what you'd expect from a catalogue: `conditionId`, `question`, `slug`, `bestBid`, `bestAsk`, `lastTradePrice`, `spread`, `volumeNum`, `liquidityNum`, `volume24hr`, price-change fields by horizon, plus operational flags like `enableOrderBook`, `acceptingOrders`, `orderPriceMinTickSize` and `orderMinSize`.

## What CLOB covers

The CLOB API base URL is `https://clob.polymarket.com`, and this is the trading surface — the live order book plus everything you do against it. Market data here is free to read and needs no credentials: `/book` and `/books` for order books, `/price` and `/prices` for a single side's price, `/midpoint` and `/midpoints`, `/prices-history` for a price series, and a tick-size lookup per market.

Note the plural forms. Batch variants exist precisely so you don't loop: `/books` and `/prices` take a set of tokens in one request. Their request-rate allowance is lower — 500 requests per 10 seconds against 1,500 for the single-item versions — but each request covers many tokens, so for any multi-token workload they cut the number of requests you need rather than the other way round. How much you save depends on how many tokens you pack per call.

The CLOB API endpoints for account state and trading sit behind CLOB API authentication, which runs on two levels. L1 is a signature from your private key — an EIP-712 signature — used once to create or derive an API key. L2 is that key, sent as HMAC headers on subsequent requests. Two account details decide whether this works: the signature type matching how the account was created (a plain EOA, or the `POLY_PROXY`, `GNOSIS_SAFE`, `POLY_1271` types), and the funder address that actually holds the money, which for an EOA account is the EOA itself. Authenticated routes then cover order placement and cancellation plus your own ledger through `/data/orders` and `/data/trades` — your orders and your fills, not an arbitrary wallet's.

Two rate-limit details matter here more than elsewhere. The API key endpoints have their own tight limit of 100 requests per 10 seconds, and orders and cancellations are governed by both IP-based Cloudflare limits and separate per-signer token buckets — so a single account can't burst through by spreading across addresses, and shouldn't try.

## What the Data API covers

The Data API at `https://data-api.polymarket.com` answers questions keyed by address. `/positions` and `/closed-positions` for holdings, `/activity` for the chronological on-chain feed, `/trades` for fills, `/value` for a portfolio total, `/holders` for the largest holders of a given market, plus leaderboard surfaces.

Worth stating plainly because it's a common search: there is no Data API markets endpoint in the sense of a market catalogue. Market metadata lives on Gamma, and the Data API's market-shaped surface is `/holders` plus the `market` and `eventId` filters on the address-scoped endpoints. If you're looking for a market's question, slug or token IDs, you're on the wrong host — and if you're looking for who holds it, you're on the right one.

## Choosing an API for the job

The decision collapses to one question: what is your key? Each API is indexed by a different identifier, and knowing how to use the Gamma API mostly means knowing how to translate between them.

| You have | You want | Host | Endpoint |
| --- | --- | --- | --- |
| A Polymarket URL | Market metadata | Gamma | `/events/slug/{slug}` |
| A `conditionId` | The market's question, dates, flags | Gamma | `/markets?condition_ids=...` |
| A token ID | Live order book or price | CLOB | `/book`, `/price` |
| A wallet address | Positions, activity, fills | Data | `/positions`, `/activity`, `/trades` |
| A market | Its largest holders | Data | `/holders` |

The join keys are the practical core. `conditionId` links Gamma to the Data API — it appears in both a market object and a position object. `clobTokenIds` on the Gamma market decodes to the two outcome token IDs, which are what the CLOB calls `token_id` and what the Data API returns as `asset` on a position. So the canonical pipeline is: discover on Gamma, price on the CLOB, attribute on Data — and carry `conditionId` and the token IDs along as you go.

There's a budget argument for this split too, and it works in your favor. The three hosts have separate allowances: Gamma is 4,000 requests per 10 seconds, the CLOB 9,000, the Data API 1,000 — under a global ceiling of 15,000. Polymarket's documentation says Cloudflare delays and queues excess requests, but [our August 2026 test found immediate 429 responses with no gradual slowdown](/blog/polymarket-api-rate-limits.php#our-test-what-the-limits-actually-do). Because the budgets are separate, enriching positions with market metadata doesn't spend the allowance your position polling needs. The Data API is the tightest of the three and the one to design around: `/positions` allows 150 requests per 10 seconds and `/trades` 200.

The Gamma API markets endpoint parameters are what keep you inside those budgets. Filtering server-side with `tag_id`, date bounds and volume thresholds beats fetching broadly and filtering in your own code, and asking for events instead of markets collapses two round trips into one. Cache aggressively while you're at it: a market's question, slug and token IDs don't change, so they belong in a local store rather than in a polling loop.

For sweeps across large result sets, new code should page with cursors rather than offsets. Polymarket added `/markets/keyset` and `/events/keyset` as the cursor-based replacement for the offset endpoints: you read `next_cursor` from each response and pass it back as `after_cursor`, with `limit` capped at 100 and a default of 20. The contract is strict in a helpful way — `offset` is explicitly rejected with a 422 on these routes, and `next_cursor` is present only while more pages remain, so its absence is your stop condition rather than a guess. The offset-based endpoints still work today, but they're on a deprecation path; anything built now belongs on keyset.

## Calling them from Python

Here's the whole pipeline in one CLOB API Python example, starting from a URL slug and ending with a live book — no credentials anywhere, since every read below is public:

PythonCopy code

```python
import json, requests

GAMMA = "https://gamma-api.polymarket.com"
CLOB  = "https://clob.polymarket.com"
DATA  = "https://data-api.polymarket.com"
HEAD  = {"User-Agent": "market-tracker/1.0"}

def event_by_slug(slug):                      # 1. discovery: one call, markets nested
    r = requests.get(f"{GAMMA}/events/slug/{slug}", headers=HEAD, timeout=15)
    r.raise_for_status()
    return r.json()

def book(token_id):                           # 2. pricing: CLOB is keyed by token, not market
    r = requests.get(f"{CLOB}/book", params={"token_id": token_id}, headers=HEAD, timeout=15)
    r.raise_for_status()
    return r.json()

def holders(condition_id):                    # 3. attribution: Data is keyed by address/market
    r = requests.get(f"{DATA}/holders", params={"market": condition_id}, headers=HEAD, timeout=15)
    r.raise_for_status()
    return r.json()

event = event_by_slug("fed-decision-in-october")     # replace with any live slug
for market in event.get("markets", []):
    outcomes  = json.loads(market["outcomes"])       # JSON-encoded strings, not arrays
    token_ids = json.loads(market["clobTokenIds"])
    prices    = json.loads(market["outcomePrices"])
    print(market["question"], "|", market["conditionId"])

    for name, token, cached in zip(outcomes, token_ids, prices):
        b = book(token)
        # bids are sorted price DESCENDING, asks ASCENDING — best quote is index 0
        best_bid = b["bids"][0]["price"] if b.get("bids") else None
        best_ask = b["asks"][0]["price"] if b.get("asks") else None
        print(f"  {name:<6} gamma={cached:<8} bid={best_bid} ask={best_ask}")

    top = holders(market["conditionId"])              # 3rd host: same conditionId as the key
    print(f"  holders returned: {len(top)}")
```

What to look at, and what this deliberately doesn't do. The three `json.loads` calls are the point of the example — skip them and every downstream comparison silently operates on characters. Book ordering is the second trap: the schema documents `bids` as sorted by price descending and `asks` ascending, so the best quote on each side is index `0` — reaching for `[-1]` gets you the worst resting order in the book, which looks plausible enough in a log to survive review. The `User-Agent` header is set because requests without a recognisable one have been reported to hit Cloudflare more often. Prices from Gamma are catalogue values and can lag the book, which is why the loop prints both side by side: use Gamma's number for a listing page, the CLOB's for anything you'll trade on. The example has no retry policy, no batching (`/books` would replace the per-token loop in a real tracker), no WebSocket for live updates, and no pagination.

One last operational note that costs people an afternoon: Gamma and the docs don't fully agree on filters. Polymarket's guide recommends `active=true&closed=false` for live markets, while the current `/markets` OpenAPI card lists `closed` (defaulting to `false`) among its documented parameters without `active`. Send what the card documents, verify the rest against live responses, and don't assume a filter works because a tutorial used it.
