Pull Polymarket historical order books, trades, and settlements with AymarData REST, Python, cURL, and bulk files — with working examples.
How to get Polymarket historical data
Teams need Polymarket history for backtests, academic work, compliance audits, and AI agent workflows. AymarData exposes historical L2 books, trades, and settlements through REST, optional bulk files, and MCP. This guide shows practical ways to download and query that history without scraping the live site.
Start from the Polymarket data hub to see which families are covered, then authenticate with a Bearer key (ad_live_…).
Choose a delivery path
| Path | Best for | Entry point |
|---|---|---|
| REST point-in-time | Interactive research, agents | /v1/book, /v1/trades |
| REST deltas | High-frequency replay | /v1/book/deltas |
| Bulk files | Multi-day offline jobs | /v1/files, Bulk |
| MCP | Cursor / Claude tools | MCP platform |
Builder and Pro plans differ on concurrency and file access — check Pricing. Bulk listing via /v1/files is available on Pro and Enterprise.
Decision rule of thumb
- Need one book at one timestamp → REST
/v1/book?at= - Need a path every few milliseconds → snapshot +
/v1/book/deltas - Need weeks of many markets offline → list/download files, then DuckDB/Polars
- Exploring interactively in an IDE → MCP, then export a REST script for CI
Authenticate
export AYMARDATA_API_KEY=ad_live_YOUR_KEY
All requests use:
Authorization: Bearer ad_live_YOUR_KEY
Details: Authentication. Rotate keys if they appear in shared notebooks; prefer environment variables over hardcoded secrets.
Discover markets and windows
curl -s "https://api.aymardata.com/v1/markets" \
-H "Authorization: Bearer $AYMARDATA_API_KEY" \
-G \
--data-urlencode "source=polymarket" \
--data-urlencode "q=btc" \
--data-urlencode "interval=15m" \
--data-urlencode "status=all" \
--data-urlencode "limit=50"
Each row includes identifiers, interval, status, and history_from when available. Use those ids for subsequent calls. Family pages like btc-updown-15m map to the same q / slug vocabulary.
Interpreting discovery fields
market— Pass this string into book/trades/settlement calls.history_from— Earliest retained reconstruction time for that window; skip rows without it when building historical grids.interval— Confirms you filtered 15m vs 1h families correctly.- Status — Use resolved windows for labeled P&L; open windows for live monitoring only.
Cache market lists locally. Discovery changes slowly compared with books, and repeated discovery calls burn rate-limit budget needlessly (Rate limits).
Historical books
Pass at= as ISO-8601 UTC:
import os
import requests
from datetime import datetime, timedelta, timezone
API = "https://api.aymardata.com/v1"
H = {"Authorization": f"Bearer {os.environ['AYMARDATA_API_KEY']}"}
def historical_books(market: str, outcome: str, start: datetime, minutes: int = 15, step_s: int = 60):
out = []
t = start
end = start + timedelta(minutes=minutes)
while t <= end:
at = t.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
r = requests.get(
f"{API}/book",
headers=H,
params={
"source": "polymarket",
"market": market,
"outcome": outcome,
"at": at,
"depth": 10,
},
timeout=30,
)
r.raise_for_status()
out.append(r.json())
t += timedelta(seconds=step_s)
return out
# Example: sample every 60s across a known window open
series = historical_books(
"btc-updown-15m",
"up",
datetime(2026, 10, 22, 14, 0, tzinfo=timezone.utc),
)
print(len(series), "snapshots")
For denser paths, fetch one snapshot then apply deltas (see deltas vs snapshots) instead of polling every second. Always pass outcome for two-sided Polymarket books — Up and Down are separate ladders.
Field notes on book rows
Store ts_exchange and ts_recv alongside mid/spread. Exchange time is what you want for strategy clocks; receive time helps latency audits. spread is best ask minus best bid when both sides exist; empty sides should skip mid calculation rather than coerce to zero.
Historical trades
curl -s "https://api.aymardata.com/v1/trades" \
-H "Authorization: Bearer $AYMARDATA_API_KEY" \
-G \
--data-urlencode "source=polymarket" \
--data-urlencode "market=btc-updown-15m" \
--data-urlencode "outcome=up" \
--data-urlencode "start=2026-10-22T14:00:00Z" \
--data-urlencode "end=2026-10-22T14:15:00Z" \
--data-urlencode "limit=200"
Paginate with cursor / next_cursor when present. Aggregate volume, VWAP, and aggressor side in your own store if you need daily rollups.
Trade interpretation tips
Trades validate whether quoted mids ever traded and help calibrate fill models. Do not substitute last trade for top-of-book when simulating entries on a two-sided market. For supervised learning on resolution, join trades to settlements rather than treating volume alone as a label.
Settlements
def settlement(market: str):
r = requests.get(
f"{API}/settlement",
headers=H,
params={"source": "polymarket", "market": market},
timeout=30,
)
r.raise_for_status()
return r.json()
Resolved windows are essential for labeled datasets. Combine history_from, trade prints, and settlement outcome for supervised models of resolution. Normalize outcome strings before joins. Missing settlement means the window is still open or not ingested — exclude from P&L tables.
Bulk files
When you need days or weeks offline, list available objects then download:
curl -s "https://api.aymardata.com/v1/files" \
-H "Authorization: Bearer $AYMARDATA_API_KEY" \
-G \
--data-urlencode "source=polymarket" \
--data-urlencode "prefix=btc-updown-15m"
File layouts (Parquet/CSV partitions) are described under Bulk delivery and GET /v1/files. Prefer files for DuckDB / Polars jobs; prefer REST for interactive at= lookups. Bulk downloads do not count against the REST budget the same way as chatty point-in-time polling — see plan notes in Rate limits.
def list_files(source: str, prefix: str = "", date: str | None = None, fmt: str = "parquet"):
params = {"source": source, "prefix": prefix, "format": fmt}
if date:
params["date"] = date
r = requests.get(f"{API}/files", headers=H, params=params, timeout=60)
r.raise_for_status()
return r.json()
REST vs files edge cases
- Point-in-time reconstruction at arbitrary
at=is a REST strength; files excel at sequential scans. - Empty file listings usually mean the date/prefix is wrong, the plan lacks bulk access, or partitions are not published yet — not that the market never existed.
- Keep REST as a spot-check against a file row when schemas drift.
Storing a local research lake
A practical layout:
data/
polymarket/
markets.jsonl
books/{market}/{date}.parquet
trades/{market}/{date}.parquet
settlements.jsonl
Write idempotently keyed by (market, outcome, ts_exchange). Keep raw API payloads when debugging schema drift. Align crypto reference series from Binance or Hyperliquid into the same partition scheme if you train cross-venue models. Related event contracts may also appear under Kalshi.
Minimal writer sketch
import json
from pathlib import Path
def append_jsonl(path: Path, row: dict):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a") as f:
f.write(json.dumps(row, default=str) + "\n")
def ingest_settlements(markets: list[str], out: Path):
for market in markets:
try:
row = settlement(market)
row["market"] = market
append_jsonl(out, row)
except requests.HTTPError as e:
append_jsonl(out.with_suffix(".errors.jsonl"), {"market": market, "error": str(e)})
Agents and MCP
If you work in Cursor or Claude Code, point the MCP server at your key and ask the agent to fetch books for a date range. Pair with Backtesting with an AI agent via MCP and Using with AI coding tools. Require the agent to emit REST scripts so CI can rerun without MCP.
Rate limits and etiquette
- Burst carefully; respect rate limits
- Cache market lists; they change slowly compared to books
- Prefer deltas for fine-grained replay
- Do not share live keys in notebooks published publicly
- On HTTP 429, honor
Retry-Afterrather than tight spin loops
Building a labeled dataset without leakage
Historical Polymarket work often ends in a supervised table: features known at decision time, label equal to settlement outcome. Leakage creeps in when settlement timestamps are earlier than you think, when you sample books after resolution, or when you use the full-window VWAP as a “feature” for an early-window decision. Keep a hard rule: every feature row carries an asof timestamp, and every join to settlement or crypto references must use only observations at or before asof.
A practical pipeline is: discover windows → filter to those with history_from and a settlement → sample books on a fixed relative schedule (for example +60s, +5m, +10m inside a 15m window) → attach trades aggregates that end at each sample → attach settlement once → write Parquet. That structure ports cleanly from REST to bulk files later without changing labels.
When coverage gaps appear (HTTP errors, empty books), keep them as explicit rows with a status column rather than dropping silently. Attrition that correlates with volatility will bias any edge estimate if you only keep “easy” windows. The backtest guide shows how skip counters surface that problem early.
Quality checks that catch bad pulls
Before you schedule a week-long backfill, run a one-day canary:
- List markets for one family and assert
history_fromis present on most resolved rows. - Fetch a book at
history_from + 60sfor Up and Down; assert both sides or record one-sidedness. - Fetch trades for the same window; assert timestamps fall inside the interval.
- Fetch settlement; assert outcome is
upordown(or the family’s vocabulary). - Reconstruct via deltas for one minute and compare to
/v1/book?at=at the end of that minute. - List
/v1/filesfor the same source/prefix if your plan includes bulk; note whether partitions exist yet.
If step 5 fails, fix apply logic or seed depth before trusting any path-dependent model. If step 6 is empty while REST works, keep using REST for that range and revisit bulk later. These checks are boring; they are also how teams avoid discovering broken joins after training overnight.
Naming and versioning your extracts
Historical extracts rot when filenames omit source, outcome, and schema version. Prefer paths like polymarket/btc-updown-15m/outcome=up/dt=2026-10-22/ and keep a small manifest.json that records the API base URL, pull time, and code git SHA. When AymarData adds fields, manifests tell you which partitions need a refresh instead of forcing a full re-download. The same habit makes MCP-generated scripts safer: the agent can append to a known layout instead of inventing a new folder each session.
Related reading
- How to backtest Polymarket Up/Down
- Polymarket order book data explained
- Polymarket API vs AymarData
- Docs quickstart
FAQ
Is scraping Polymarket allowed / enough?
Scraping is brittle, incomplete for L2 history, and often against venue terms. AymarData is built for authorized historical access under your Data License.
How far back does history go?
See each family’s data page and history_from on /v1/markets. Coverage expands as collectors run; paid plans retain stored history.
Can I export CSV only?
Use bulk files where offered, or convert REST JSON to CSV/Parquet in Python. REST remains the source of truth for point-in-time reconstruction.
Do I need outcome on every call?
For two-sided Polymarket books and trades, yes — pass up/down (or yes/no) explicitly to avoid ambiguous responses.
Educational content, not trading or investment advice. Past data doesn't predict future results.