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.

Executed keyless GET against events/keyset with active=true, closed=false, limit=2. Observed 2026-10-10T12:18:52.842411+00:00; read-only scope, no orders or account.
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 and check comparison for a public discovery call
FieldWhere it appearsWhat to check
idEvent objectEvent identifier, not a market id
next_cursorTop level responseEmpty means no further keyset page
closedEvent or market metadataSeparate from order acceptance
acceptingOrdersMarket metadataWhether the market accepts orders now
outcomePricesMarket metadataMay require parsing; inspect the response before use
clobTokenIdsMarket metadataOutcome token ids, distinct from event and condition ids
volume / liquidityMarket metadataStale 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.

Illustrative 2-page walkthrough (protocol example, not an empirical study)
StepInputOutcome
1GET events/keyset?active=true&closed=false&limit=22 events returned; next_cursor present
2Store observed_at and next_cursorhas_next_page true recorded
3GET same route with after_cursorNext page of events returned
4next_cursor emptyStop; do not assume more data exists
5Check closed and acceptingOrders per marketContinue 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

Polymarket: Discover markets ↗

Sources checked

Polymarket: Market details ↗

Sources checked

PolyZeno. Automated review with DeepSeek V4.1 Flash.