// PREDICTION-MARKET APIS
Kalshi API guide: place your first order
2026-07-16 · Mithril
Kalshi is the CFTC-regulated US prediction-market exchange, and its trade API is the most conventional of the major venues: REST endpoints, an API key, and signed requests. If you have integrated any traditional exchange API before, you will feel at home. This guide walks from zero to a filled order: auth, market discovery, reading the book, placing and managing an order, and the gotchas that catch almost everyone the first time.
Everything below targets the trade API at
https://api.elections.kalshi.com/trade-api/v2. Kalshi also runs a demo
environment — start there, and only point at production once your order
lifecycle handling works.
Step 1: create an API key
In your Kalshi account settings, generate an API key. You get two things:
- an access key ID (a UUID that identifies the key), and
- an RSA private key that you download once and store yourself.
Kalshi never sees your private key again, so treat it like any other trading credential: keep it out of source control, load it from an environment variable or a secrets manager, and rotate it if it ever touches a log line.
Step 2: sign every request
Kalshi's auth is per-request signing, not a bearer token. Each request carries three headers:
KALSHI-ACCESS-KEY— your access key IDKALSHI-ACCESS-TIMESTAMP— current time in millisecondsKALSHI-ACCESS-SIGNATURE— an RSA-PSS signature overtimestamp + method + path
In Python:
import base64
import time
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
BASE = "https://api.elections.kalshi.com"
ACCESS_KEY = "your-access-key-id"
with open("kalshi_private_key.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
def kalshi_headers(method: str, path: str) -> dict:
ts = str(int(time.time() * 1000))
msg = (ts + method + path).encode()
sig = private_key.sign(
msg,
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(sig).decode(),
}
Two details bite people here. The signed path is the API path (for
example /trade-api/v2/portfolio/orders) — check the docs for exactly what
string to sign, because a mismatch produces an opaque auth error. And the
timestamp must be close to server time; a skewed clock on your box means
every request fails signature validation even though your code is correct.
Step 3: find a market
Kalshi organizes contracts as series → events → markets. A series is the recurring template ("Fed decision"), an event is one instance ("September 2026 meeting"), and a market is one binary contract inside it. Market discovery is unauthenticated-friendly and paginated:
resp = requests.get(
f"{BASE}/trade-api/v2/markets",
params={"status": "open", "limit": 100},
headers=kalshi_headers("GET", "/trade-api/v2/markets"),
)
markets = resp.json()["markets"]
Each market has a human-readable ticker like FED-26SEP-T4.00, which
is also its API identifier. Prices are integers in cents, 1–99. A yes
price of 42 means the market implies roughly a 42% probability, and one
contract pays $1.00 if the market resolves yes.
Step 4: read the order book
path = "/trade-api/v2/markets/FED-26SEP-T4.00/orderbook"
book = requests.get(BASE + path, headers=kalshi_headers("GET", path)).json()
The book is returned as arrays of [price, quantity] levels for the yes
and no sides. One structural quirk: because every contract is binary, a bid
on no is economically an offer on yes at 100 − price. If the book looks
one-sided at first glance, you are probably forgetting to mirror the other
side. For polling, the REST book is fine; for anything latency-sensitive,
use the WebSocket orderbook channel instead (covered in
rate limits, order types, and websockets).
Step 5: place your first order
Orders go to POST /trade-api/v2/portfolio/orders:
import uuid
order = {
"ticker": "FED-26SEP-T4.00",
"client_order_id": str(uuid.uuid4()),
"action": "buy",
"side": "yes",
"type": "limit",
"count": 10,
"yes_price": 42,
}
path = "/trade-api/v2/portfolio/orders"
resp = requests.post(
BASE + path, json=order, headers=kalshi_headers("POST", path)
)
print(resp.json())
Field by field:
| Field | Meaning |
|---|---|
ticker | The market you're trading |
client_order_id | Your idempotency key — always send one |
action | buy or sell |
side | yes or no |
type | limit or market |
count | Number of contracts |
yes_price | Limit price in cents (use no_price when working the no side) |
The client_order_id deserves emphasis. If your POST times out, you do not
know whether the order was accepted. Retrying with the same
client_order_id is safe; retrying without one is how bots end up with
double positions. Build this in from day one — more patterns in
how to build a prediction-market bot in Python.
The order lifecycle
Once accepted, an order moves through a small state machine:
- Resting — a limit order sitting in the book, partially or fully unfilled.
- Executed — matched, fully filled.
- Canceled — pulled by you (
DELETE /portfolio/orders/{order_id}) or expired.
Partial fills are normal in thin books: a 100-lot might fill 30 and rest 70.
Track fills via GET /portfolio/fills or, better, the WebSocket fill
channel, and reconcile against GET /portfolio/positions on a timer.
Trusting your local view of state without periodic reconciliation is the
classic slow-motion bug.
Fees: a formula, not a flat rate
Kalshi's standard taker fee is 0.07 × contracts × price × (1 − price)
with price in dollars, rounded up to the next cent — while resting
(maker) orders generally pay no trading fee on most markets. The formula
peaks at 50¢ and shrinks toward the tails, so a fill at 50¢ costs meaningfully
more in fees than the same size at 90¢. Price your quotes with this in mind:
crossing the spread at midprice-ish levels is where the fee hurts most. Full
breakdown in Kalshi fees explained, or plug
your own numbers into the
Kalshi fee calculator.
Common gotchas
- Cents, not dollars.
yes_price: 42is 42 cents. Sending0.42fails validation — but a bot that formats prices for Polymarket (decimals) and reuses the code for Kalshi will produce nonsense. - The no side mirrors yes. Buying no at 58 is the same exposure as selling yes at 42. Pick one internal representation and convert at the edge.
- Clock skew breaks auth. Signature errors that appear randomly are usually a drifting clock, not a bad key.
- Rate limits are real. Kalshi enforces per-tier request limits; naive polling loops hit them fast. Batch reads, prefer WebSockets, and back off on 429s.
- Expiration and settlement lag. A market can close for trading before it settles. Your PnL is not final until settlement, and your bot should distinguish "closed" from "settled".
Where this goes next
A single-venue Kalshi bot is a genuinely tractable project — the API is
clean and well documented. The complexity arrives when you add Polymarket,
because almost everything above is different there:
auth, IDs, price formats, fees, error semantics. If you would rather trade
both venues through one interface, Mithril exposes a single REST API with
unified mkt_ IDs, fee-aware routing across Kalshi and Polymarket, and
server-side risk limits — docs here.
APIs change. Details above reflect public documentation as of July 2026 — always confirm against the venues' own docs before trading real money.
One API + MCP for Kalshi and Polymarket
Fee-aware routing, unified market IDs, and hard server-side risk limits — live today, free during beta.
Keep reading
- The complete guide to prediction market APIs (Kalshi + Polymarket)
- Kalshi API vs Polymarket API: the differences that bite
- How to build a prediction-market trading bot in Python
- Rate limits, order types, and websockets on Kalshi and Polymarket
- Kalshi fees explained (with worked examples)
Terms used