Kalshi is the first federally regulated (CFTC) prediction market exchange in the US, and unlike a lot of trading venues it ships a genuinely usable public API. If you want to pull live market prices, build a trading bot, run a PolymarketβKalshi arbitrage scanner, or just log odds over time, this is the guide. Everything below is based on the current v2 Trade API β with working Python you can paste and run.
What the Kalshi API gives you
There are two halves. Reading market data is public β no account, no keys, no auth. You can hit the markets, events, and order-book endpoints straight away. Trading is authenticated β placing or cancelling orders and reading your balance/positions requires an API key pair and a signature on every request. Kalshi exposes three interfaces: a REST API (what most people use), a WebSocket feed for real-time order-book and ticker updates, and FIX 4.4 for high-throughput institutional flow.
Base URLs and environments
The current production REST base is:
https://external-api.kalshi.com/trade-api/v2
You'll also see https://api.elections.kalshi.com/trade-api/v2 in older code β it still resolves and, despite the "elections" name, serves every market, not just political ones. The WebSocket host is wss://external-api-ws.kalshi.com/trade-api/ws/v2.
Kalshi also runs a demo/sandbox environment with fake money β build and test your bot there before pointing it at real funds. Kalshi has relocated these hosts more than once, so grab the current demo base URL from Kalshi's API Environments doc rather than hard-coding an old one.
Reading market data (no auth needed)
The fastest way to confirm you're connected β list open markets with plain curl:
curl "https://external-api.kalshi.com/trade-api/v2/markets?limit=5&status=open"
The same in Python, printing each market's ticker and current yes/no bid:
import requestsBASE = "https://external-api.kalshi.com/trade-api/v2"
r = requests.get(f"{BASE}/markets", params={"limit": 20, "status": "open"})
for m in r.json()["markets"]:
print(m["ticker"], "|", m["title"], "| yes", m.get("yes_bid"), "no", m.get("no_bid"))
The public, key-free endpoints you'll use most:
GET /seriesβ top-level market series (e.g. an ongoing recurring market)GET /eventsβ events, each grouping related marketsGET /marketsβ individual yes/no contracts, filterable bystatus,event_ticker, etc.GET /markets/{ticker}/orderbookβ full bid/ask depth for one market
Prices come back in cents (1β99): a yes_bid of 62 means the market implies a ~62% chance and a Yes share costs $0.62.
Authentication: RSA-PSS request signing
This is the part that trips people up, so here it is precisely. In your Kalshi account settings you create an API key pair β Kalshi shows you an Access Key ID and lets you download a private key (a .pem file). Kalshi keeps the public half; you keep the private key and sign with it.
Every authenticated request needs three headers:
KALSHI-ACCESS-KEYβ your Access Key IDKALSHI-ACCESS-TIMESTAMPβ current time in millisecondsKALSHI-ACCESS-SIGNATUREβ a base64 RSA-PSS signature
You sign the string timestamp + METHOD + path, where the path includes /trade-api/v2 and excludes any query string. For example the message might be 1703123456789GET/trade-api/v2/portfolio/balance. The algorithm is RSA-PSS with SHA-256 (MGF1-SHA256, salt length equal to the digest length). Here's a complete, working signer:
import time, base64, requestsfrom cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
HOST = "https://external-api.kalshi.com"
ACCESS_KEY = "your-access-key-id" # from Kalshi -> Settings -> API Keys
with open("kalshi_private_key.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
def signed_headers(method, path): # path includes /trade-api/v2, no query string
ts = str(int(time.time() * 1000))
message = (ts + method.upper() + path).encode()
signature = private_key.sign(
message,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH),
hashes.SHA256(),
)
return {
"KALSHI-ACCESS-KEY": ACCESS_KEY,
"KALSHI-ACCESS-TIMESTAMP": ts,
"KALSHI-ACCESS-SIGNATURE": base64.b64encode(signature).decode(),
}
Read your balance
path = "/trade-api/v2/portfolio/balance"
resp = requests.get(HOST + path, headers=signed_headers("GET", path))
print(resp.json())
Two things that cause 401s: signing the wrong path (drop the query string, keep /trade-api/v2), and a clock more than a few seconds out of sync β the timestamp you sign must be the same one you send.
Placing and managing orders
Everything under /portfolio is authenticated:
GET /portfolio/balanceβ cash balanceGET /portfolio/positionsβ open positionsGET /portfolio/ordersβ your resting ordersPOST /portfolio/ordersβ place an orderDELETE /portfolio/orders/{order_id}β cancel
A limit order to buy 10 Yes shares at 60Β’:
import jsonorder = {
"ticker": "SOME-MARKET-TICKER",
"client_order_id": "my-unique-id-001", # your idempotency key
"action": "buy", # buy | sell
"side": "yes", # yes | no
"count": 10,
"type": "limit", # limit | market
"yes_price": 60, # price in cents, 1-99
}
path = "/trade-api/v2/portfolio/orders"
body = json.dumps(order)
resp = requests.post(HOST + path, data=body,
headers={**signed_headers("POST", path),
"Content-Type": "application/json"})
print(resp.json())
Always send a unique client_order_id β it's your idempotency key, so a retried request can't accidentally double-fill.
Real-time data over WebSocket
For a live order book or ticker you don't want to poll REST. Connect to wss://external-api-ws.kalshi.com/trade-api/ws/v2 (authenticated the same way, via headers on the upgrade request) and subscribe to channels like orderbook_delta or ticker for the tickers you care about. This is how most serious bots keep prices fresh with sub-second latency.
Rate limits, SDKs, and gotchas
- Rate limits are tiered (Kalshi offers higher tiers for active/institutional traders). Design your bot to back off on
429s and lean on WebSocket for real-time data instead of hammering REST. - Official SDK: Kalshi's docs demonstrate raw
requests+cryptography(as above), and there are solid community Python clients β but knowing the raw signing flow means you're never blocked waiting on a wrapper. - Cents, not dollars. Prices and P&L are in cents; a "$100 position" is 100-count at some cents price. Convert carefully.
Kalshi API vs Polymarket API
If you're building cross-platform, the two are different beasts. Polymarket is on-chain (Polygon) β you authenticate with a wallet signature and settle in USDC; see our Polymarket API complete guide. Kalshi is a centralized, CFTC-regulated exchange β you authenticate with an API key + RSA signature and settle in USD from a funded account. The data models rhyme (events β markets β yes/no contracts, prices in cents), which is exactly why cross-market arbitrage between them is a thing. We break down the economics in Polymarket vs Kalshi and the PolymarketβKalshi arbitrage guide.
Don't want to build it yourself?
Plenty of tools already wrap both APIs so you get unified data, alerts, and arbitrage signals without writing a signer:
- Aggregators & data tools β one feed across Polymarket and Kalshi (e.g. Dome, Predexon)
- Arbitrage scanners β auto-detect cross-market price gaps in real time
- The full directory β 100+ prediction-market tools, ranked weekly by real traders
Whether you build on the raw API or buy a tool off the shelf, the Kalshi v2 API is one of the cleanest in the space β start on the demo environment, get a public /markets call working, then layer in signing.



