Guides / API Guide
All four historical endpoints — /v1/historical/{kalshi,polymarket}/{orderbook,trades}
— take the same time-range shape. Getting it right is mostly about two things: the units, and
never using time as your pagination cursor.
| Parameter | Type | Notes |
|---|---|---|
market_id | string | Required. From /v1/search/markets. |
start_time | Unix seconds | Inclusive lower bound. |
end_time | Unix seconds | Upper bound. |
granularity | 1m | 5m | Orderbook endpoints only. |
limit | integer | Rows per page. |
pagination_key | string | Continues a previous response. |
asset_id | string | Polymarket: narrows to one outcome token. |
Unix seconds, not milliseconds and not ISO 8601. Passing milliseconds yields a range far in the future and an empty result rather than an error, which is why "the API returned nothing" is usually a units bug.
# Python
start_time = int(datetime(2025, 1, 20, tzinfo=timezone.utc).timestamp())
# JavaScript — Date.getTime() is milliseconds, so divide
const startTime = Math.floor(Date.parse("2025-01-20T00:00:00Z") / 1000);
The tempting pattern — take the last row's timestamp, use it as the next
start_time — is wrong. Multiple records can share the same second, so an
inclusive bound re-reads them and an exclusive one drops them. Follow the cursor instead:
params = {"market_id": mid, "start_time": start, "end_time": end, "limit": 1000}
while True:
res = get("/v1/historical/polymarket/orderbook", params).json()
yield from res["snapshots"]
if not res["pagination"]["has_more"]:
break
params["pagination_key"] = res["pagination"]["pagination_key"]
Windows trade off request count against response size. A day at 1m is ~1,440
snapshots — a handful of pages at limit=1000. Prefer one wide range with proper
paging over many narrow ranges: it is fewer round trips and cannot develop gaps at the seams.
Bear in mind the retention on your plan (Builder covers 60 days) —
a start_time before your window returns nothing for the uncovered part.
Data from Kalshi and Polymarket. Search live API docs Pricing