Kalshi API vs Polymarket API: A Python Developer’s Comparison
If you’re building anything on prediction-market data, you eventually hit the same fork in the road: Kalshi API vs Polymarket API. They look interchangeable from the outside — two venues quoting probabilities on the same events — but the moment you open an editor they could not be more different. Different auth models, different identifiers, different price formats, even a different number of APIs. Get the wrong mental model and your parser breaks on the first request.
This is a developer-to-developer breakdown: how each API is actually shaped, the gotchas that bite, and a shortcut that gives you both venues — already converted to decimal odds — from a single HTTP call.
The 30-Second Answer
| Dimension | Kalshi API | Polymarket API |
|---|---|---|
| Venue type | CFTC-regulated US exchange | Onchain (Polygon), crypto-settled |
| Number of APIs | One REST API | Two: Gamma (catalog) + CLOB (order book) |
| Read auth | None (public market data) | None (public reads) |
| Market identifier | ticker (human-readable) |
token_id (77-digit ERC-1155) |
| Price format | USD strings, 0–1 (_dollars fields) |
Share-price strings, 0–1 |
| Decimal odds? | No — compute 1 / price |
No — compute 1 / price |
| Trading auth | API key + RSA signature | EIP-712 wallet sig + L2 headers |
Neither API hands you decimal odds, and neither lets you pull both venues in one request. If all you want is to read prices side by side, skip to the OddsPapi section — both venues come back pre-converted from one endpoint. If you’re going deeper (trading, raw depth, settlement), read on.
Kalshi API: One REST API, Ticker-Based
Kalshi is a CFTC-regulated exchange, so the API feels like a traditional financial data feed. There’s a single base URL, everything is keyed off a human-readable ticker, and market data reads need no authentication.
Base URL: https://api.elections.kalshi.com/trade-api/v2
The ID hierarchy
Kalshi nests three levels, all string tickers:
- Series — a recurring template (e.g. a weekly question type)
- Event (
event_ticker) — a single real-world question - Market (
ticker) — one yes/no contract inside an event
Fetching markets and prices
import requests
KALSHI = "https://api.elections.kalshi.com/trade-api/v2"
# List open markets (no auth required for reads)
r = requests.get(f"{KALSHI}/markets", params={"status": "open", "limit": 100})
markets = r.json()["markets"]
m = markets[0]
print(m["ticker"]) # e.g. KXBTCD-25JUN06-T100000
print(m["title"])
print(m["yes_bid_dollars"]) # "0.6550" -> implied probability of YES
print(m["yes_ask_dollars"]) # "0.6600"
print(m["no_bid_dollars"], m["no_ask_dollars"])
print(m["last_price_dollars"]) # last trade
Current-API gotcha: Kalshi moved its price fields to a _dollars suffix. The values are strings in the 0–1 range (so "0.6550" = a 65.5% implied probability). Older tutorials reference integer-cent fields like yes_bid; on the live v2 API you want yes_bid_dollars, yes_ask_dollars, no_bid_dollars, no_ask_dollars, and last_price_dollars. Cast before doing math.
The order book
ticker = m["ticker"]
ob = requests.get(f"{KALSHI}/markets/{ticker}/orderbook", params={"depth": 5}).json()
# Shape: {"orderbook_fp": {"yes_dollars": [[price, size], ...],
# "no_dollars": [[price, size], ...]}}
book = ob["orderbook_fp"]
for price, size in book["yes_dollars"]:
print(float(price), float(size)) # both are strings -> cast
Each level is a [price, size] pair, both strings. To turn a Kalshi share price into decimal odds: decimal = 1 / float(price). A YES contract at 0.655 is decimal 1.527.
Polymarket API: Two Services, Token-Based
Polymarket splits its read API in two — and confusing them is the #1 reason people’s first script fails. We covered this in depth in the Polymarket API deep dive; here’s the comparison-relevant version.
| Service | Base URL | Purpose |
|---|---|---|
| Gamma | https://gamma-api.polymarket.com |
Catalog: /events, /markets, metadata, discovery |
| CLOB | https://clob.polymarket.com |
Order book: /book, /price, /midpoint |
Step 1 — discover via Gamma
import requests, json
GAMMA = "https://gamma-api.polymarket.com"
r = requests.get(f"{GAMMA}/markets",
params={"active": "true", "closed": "false", "limit": 5})
m = r.json()[0]
print(m["question"])
print(m["conditionId"]) # 0x... on-chain market hash
# GOTCHA: these arrive as STRINGIFIED JSON, not native arrays
token_ids = json.loads(m["clobTokenIds"]) # double-parse required
outcomes = json.loads(m["outcomes"]) # ["Yes", "No"]
prices = json.loads(m["outcomePrices"]) # ["0.505", "0.495"]
The classic Polymarket trap: clobTokenIds, outcomes, and outcomePrices are strings containing JSON. If you iterate them directly you’ll loop over individual characters. Always json.loads() a second time.
Step 2 — get the book via CLOB
CLOB = "https://clob.polymarket.com"
token_id = token_ids[0] # the 77-digit ERC-1155 position id
book = requests.get(f"{CLOB}/book", params={"token_id": token_id}).json()
# {"bids": [{"price": "0.50", "size": "..."}], "asks": [...], ...}
mid = requests.get(f"{CLOB}/midpoint", params={"token_id": token_id}).json() # {"mid": "0.505"}
price = requests.get(f"{CLOB}/price", params={"token_id": token_id, "side": "buy"}).json()
Every CLOB endpoint wants the token_id (a.k.a. asset_id), not the human-readable slug or the conditionId. Prices and sizes are strings in the 0–1 range; decimal odds = 1 / float(price).
Head-to-Head: The Differences That Actually Bite
| You want to… | Kalshi | Polymarket |
|---|---|---|
| Find a market | GET /markets → ticker |
GET /markets (Gamma) → clobTokenIds |
| Get best bid/ask | Read yes_bid_dollars / yes_ask_dollars off the market |
GET /price (CLOB) per token |
| Get full depth | GET /markets/{ticker}/orderbook |
GET /book (CLOB) per token |
| Parse prices | String 0–1 in _dollars fields |
String 0–1, double-json.loads the arrays |
| Both outcomes | One market = one YES/NO pair | One token per outcome (separate ids) |
| Decimal odds | Compute 1/price |
Compute 1/price |
The summary: Kalshi is one tidy REST API with readable tickers and prices attached to the market object. Polymarket is two APIs, opaque token IDs, and stringified JSON you have to parse twice. Neither gives you decimal odds, and — critically — neither lets you compare the two venues in a single call. You’re maintaining two clients, two ID schemes, two price-parsing paths, and writing your own odds converter, just to read a number both venues already know.
The Shortcut: Both Venues, Decimal Odds, One Call
This is where OddsPapi earns its place in the stack. It aggregates 350+ bookmakers and exchanges — including both Polymarket and Kalshi — behind one schema, and pre-converts every price into decimal, American, and fractional. One request returns both prediction markets plus the sharp sportsbook line (Pinnacle) for the same event, so you can compare apples to apples instead of stitching three feeds together.
Authentication
import requests
API_KEY = "YOUR_API_KEY" # grab a free one at oddspapi.io
BASE = "https://api.oddspapi.io/v4"
# apiKey is a QUERY PARAMETER, never a header
params = {"apiKey": API_KEY}
print(requests.get(f"{BASE}/sports", params=params).status_code) # 200
Pull both prediction markets for one fixture
Here’s a real, live example: the 2026 FIFA World Cup match Brazil vs Morocco. One call returns Kalshi, Polymarket, and Pinnacle on the Full Time Result market.
fixture_id = "id1000001666456928" # Brazil vs Morocco, World Cup 2026
r = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmakers": "kalshi,polymarket,pinnacle",
})
books = r.json()["bookmakerOdds"]
# 1X2 = market 101; outcomes 101=Home, 102=Draw, 103=Away
LABELS = {"101": "Brazil", "102": "Draw", "103": "Morocco"}
for slug in ("kalshi", "polymarket", "pinnacle"):
outcomes = books[slug]["markets"]["101"]["outcomes"]
quote = {LABELS[oid]: o["players"]["0"]["price"] for oid, o in outcomes.items()}
print(f"{slug:11} {quote}")
Live output (captured June 2026):
| Venue | Brazil | Draw | Morocco | Overround |
|---|---|---|---|---|
| Kalshi | 1.639 | 4.167 | 5.882 | 2.01% |
| Polymarket | 1.639 | 4.000 | 5.882 | 3.01% |
| Pinnacle (sharp benchmark) | 1.641 | 3.840 | 5.950 | 3.79% |
Already decimal, no 1/price needed. The interesting read: on this early World Cup line, both prediction markets quoted a tighter margin than Pinnacle — Kalshi’s 2.01% overround was the lowest of the three. (Early lines tighten by matchday; this is a snapshot of available prices, not a value claim.) The point is you got all three, normalized, from one request — no token IDs, no double-parsing, no converter.
Depth-of-book is there too
Because Kalshi and Polymarket are exchanges, OddsPapi exposes their ladder under exchangeMeta — the same depth you’d otherwise hit two separate APIs for:
brazil = books["polymarket"]["markets"]["101"]["outcomes"]["101"]["players"]["0"]
ladder = brazil["exchangeMeta"] # null on sportsbooks; a ladder on exchanges
for level in ladder["back"][:3]:
print(level["price"], "odds |", level["size"], "liquidity")
# 1.639 odds | 35456.27 liquidity
# 1.613 odds | 12432.34 liquidity
# 1.587 odds | 21985.0 liquidity
Parse defensively: exchangeMeta is null on sportsbooks and a {back: [...], lay: [...]} ladder on exchanges. Each level is {cents, price, size, limit}, best price first. Polymarket also ships a bookmakerLayOutcomeId (the opposite-side token, which drops straight into Polymarket’s CLOB); Kalshi omits it. Always check the field is truthy before indexing into it.
When to Use Which
| If you’re… | Use |
|---|---|
| Reading/comparing prices across both venues | OddsPapi (one call, decimal odds, + Pinnacle anchor) |
| Backtesting prediction-market accuracy | OddsPapi free historical odds |
| Placing trades on Kalshi | Kalshi API directly (RSA-signed key) |
| Placing trades on Polymarket | Polymarket CLOB directly (EIP-712 + L2 auth) |
| Raw onchain settlement / token mechanics | Polymarket CLOB + conditionId |
For trading you’ll always go direct — each venue’s order-placement layer is venue-specific and wallet/key bound. But for the 90% of work that’s reading — comparison, alerts, dashboards, modeling, arbitrage scanning — one OddsPapi call beats maintaining two clients and a converter.
FAQ
Is the Kalshi API free to use?
Yes for market data. Reads against /markets, /events, and /markets/{ticker}/orderbook need no authentication. Placing trades requires an account and an RSA-signed API key.
Is the Polymarket API free to use?
Yes for reads. Both Gamma (gamma-api.polymarket.com) and CLOB (clob.polymarket.com) serve catalog and order-book data unauthenticated. Trading requires an EIP-712 wallet signature plus L2 auth headers.
Do Kalshi and Polymarket return decimal odds?
No. Both return share/contract prices in the 0–1 range as strings (a 0.65 price = 65% implied probability). Convert with decimal = 1 / float(price). OddsPapi pre-converts to decimal, American, and fractional so you skip the math.
What’s the biggest difference between the two APIs?
Structure. Kalshi is one REST API keyed off readable ticker strings with prices on the market object. Polymarket is two services (Gamma for catalog, CLOB for books), keyed off 77-digit token IDs, and ships its arrays as stringified JSON you must parse twice.
Can I get both Kalshi and Polymarket from one API?
Yes — OddsPapi aggregates both (plus 350+ sportsbooks and exchanges) behind one schema. A single /v4/odds call returns both venues for the same event, already converted to decimal odds, with full depth-of-book under exchangeMeta.
Stop Maintaining Two Clients
Kalshi and Polymarket are great venues with awkward, mismatched APIs. If you’re trading, go direct. If you’re reading, comparing, or modeling, get your free OddsPapi key and pull both — plus the sharp line — from one endpoint.