Polymarket API: retrieve public events without a trading key
Call the Polymarket events keyset endpoint with plain Python, read event versus market IDs, walk the cursor, and check closed, acceptingOrders and price timestamps.
In this guide
What the public read-only call gives you
Polymarket's market data endpoints answer plain GET requests, so you can list live events without any wallet, API key or signature. The one you want for browsing is the Gamma events keyset route: you pass active=true, closed=false, a limit, and you keep following a cursor. The payload comes back with an events array and a next_cursor value, and you repeat the request with that cursor until the field stops handing you more pages. This is observation only: getting a record back tells you the event exists and is open, it does not authorize you to place anything.
The easiest mistake is assuming every id is the same kind of id. An event groups one or more markets; the event has its own id, each market carries a separate market/condition id, and the outcome tokens you would actually trade inside a market have yet another identifier, clobTokenIds. Those 3 id spaces are not interchangeable, so keep them apart in your head before writing any join logic.
Run the keyless call and stamp the moment you saw it
The snippet below is the actual executed example: standard library only, no install, no secret, one request to events/keyset with active=true, closed=false and limit=2. It returned 2 event records and the server flagged a further page because next_cursor was non-empty. Because everything here is a read-only observation, write the observation time into the payload: prices, volume and open/closed status keep moving after your process exits, and a snapshot without a timestamp is hard to trust later.
import json
from datetime import datetime, timezone
from urllib.request import Request, urlopen
url = "https://gamma-api.polymarket.com/events/keyset?active=true&closed=false&limit=2"
request = Request(url, headers={"User-Agent": "PolyZenoResearch/1.0"})
with urlopen(request, timeout=20) as response:
data = json.load(response)
print(json.dumps({
"observed_at": datetime.now(timezone.utc).isoformat(),
"events": [{"id": e["id"], "slug": e["slug"], "title": e["title"]}
for e in data["events"]],
"has_next_page": bool(data.get("next_cursor"))
}, ensure_ascii=False, indent=2))
The fields that change what you call next
Market detail payloads carry the question, the outcomes list, outcomePrices, clobTokenIds, a closure flag, an order acceptance flag, liquidity and volume. Some of those arrays arrive serialized as strings, so check whether outcomePrices and clobTokenIds are native lists or JSON text before you index into them. Whenever you surface a price, carry its observation date with it.
Volume and liquidity are distinct fields. Keep their observation times, and treat a missing or stale value as unknown. Check closed and acceptingOrders independently rather than deriving order acceptance from the parent event. Study market-level liquidity concepts before deriving a signal from metadata.
When you move from polling public metadata to sending or cancelling orders, a different authentication flow applies and it sits outside this keyless guide. If you are weighing whether to automate ordering at all, the trading bot guide covers that decision.
| Field | Where it appears | What to check |
|---|---|---|
| id | Event object | Event identifier, not a market id |
| next_cursor | Top level response | Empty means no further keyset page |
| closed | Event or market metadata | Separate from order acceptance |
| acceptingOrders | Market metadata | Whether the market accepts orders now |
| outcomePrices | Market metadata | May require parsing; inspect the response before use |
| clobTokenIds | Market metadata | Outcome token ids, distinct from event and condition ids |
| volume / liquidity | Market metadata | Stale or missing is unknown, not zero |
1 cursor loop, checked against the executed snapshot
The executed run above returns 2 events from a keyset page and reports has_next_page true. Walk the sequence with next_cursor, not an offset: request a page, process the records, store observed_at, then continue only while next_cursor is non-empty. Always stop explicitly and log the time, because a paused cursor can masquerade as a finished dataset. Notice the id 16183 on the Kraken IPO record: that is an event identifier, not a market id and not a clobTokenId.
Illustrative 2-page walkthrough
The steps below are an illustrative protocol, not an empirical study, and they are not a performance claim. They show how cursor state and field checks combine in a single pass. Inputs: limit=2, active=true, closed=false, no authentication, stop once next_cursor is empty. The outcomes describe what a builder would record, and each one is conditional on what the previous response actually contained.
| Step | Input | Outcome |
|---|---|---|
| 1 | GET events/keyset?active=true&closed=false&limit=2 | 2 events returned; next_cursor present |
| 2 | Store observed_at and next_cursor | has_next_page true recorded |
| 3 | GET same route with after_cursor | Next page of events returned |
| 4 | next_cursor empty | Stop; do not assume more data exists |
| 5 | Check closed and acceptingOrders per market | Continue or stop a planned order flow |
Limits and the losing case
These API examples are limited read-only snapshots rather than a monitoring guarantee. Endpoint availability varies, serialized arrays stay ambiguous until you parse them, and stale or missing values should stay marked unknown. The executed record covers a read-only scope: 2 public event metadata records, no orders, no account. A losing case to keep in mind: a bot reads a market with an empty liquidity value, treats it as tradeable depth, and then acts on a price that may have changed before the next poll. That sequence is an illustrative hypothetical, not a documented detection pattern; the safe response is to log the payload and stop when closed or acceptingOrders do not match what you assumed.
Before wiring the loop into anything that spends money, confirm on a fresh read that the cursor advanced, that closed=false still holds, and that acceptingOrders matches the specific market you intend to touch. If any of those checks fails, keep the snapshot and stop. For the wider picture of Polymarket data surfaces, start from the main topic guide to Polymarket data.
Sources & verification
Sources checked
Sources checked
PolyZeno. Automated review with DeepSeek V4.1 Flash.