
- Auth
- API key id plus an RSA signature over the timestamp, method and path, on every request.
- Read
- One REST API for markets, books, candlesticks; a websocket for fills and book deltas.
- Trade
- Limit orders in cents or dollars per contract; the book is in contracts.
- Taker fee
- 0.07 x contracts x p x (1-p), rounded up to the cent per order. Makers pay nothing on almost all series.
- Settlement
- Exchange-published; crypto markets settle on an average the exchange streams as it accumulates.
- Geography
- US accounts. Reward programs are closed to non-US users.
- Docs
- docs.kalshi.com
Five steps, simplified on purpose. Endpoint paths follow the public reference at the time of writing; check the docs before you rely on them.
Sign every request
Generate an API key in the account settings; you get a key id and a private key file. Each request carries the id, a millisecond timestamp, and an RSA-PSS signature over the timestamp, the HTTP method and the path.
import time, base64, requests from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding BASE = "https://api.elections.kalshi.com/trade-api/v2" key = serialization.load_pem_private_key(open("kalshi.pem", "rb").read(), password=None) def headers(method, path): ts = str(int(time.time() * 1000)) msg = (ts + method + "/trade-api/v2" + path).encode() sig = key.sign(msg, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.DIGEST_LENGTH), hashes.SHA256()) return {"KALSHI-ACCESS-KEY": KEY_ID, "KALSHI-ACCESS-SIGNATURE": base64.b64encode(sig).decode(), "KALSHI-ACCESS-TIMESTAMP": ts}List the markets in a series
r = requests.get(f"{BASE}/markets", params={"series_ticker": "KXBTC15M", "status": "open"}, headers=headers("GET", "/markets")) markets = r.json()["markets"] ticker = markets[0]["ticker"]Read the order book
The book endpoint changed shape: the newer form lists price in dollars and size per level. A parser written for the old cent arrays returns zero depth without an error.
path = f"/markets/{ticker}/orderbook" ob = requests.get(BASE + path, headers=headers("GET", path)).json()["orderbook_fp"] yes_bids = ob["yes_dollars"] # [[price_dollars, size], ...] no_bids = ob["no_dollars"] best_yes_ask = 1 - float(no_bids[-1][0]) # a YES ask is the mirror of the best NO bidThe ladder can be crossed (best YES bid plus best NO bid above 1.00) on a noticeable share of snapshots. Guard against it before you quote.
Place and cancel an order
order = {"ticker": ticker, "action": "buy", "side": "yes", "type": "limit", "count": 20, "yes_price": 85, "client_order_id": "win-1757500800"} r = requests.post(f"{BASE}/portfolio/orders", json=order, headers=headers("POST", "/portfolio/orders")) order_id = r.json()["order"]["order_id"] requests.delete(f"{BASE}/portfolio/orders/{order_id}", headers=headers("DELETE", f"/portfolio/orders/{order_id}"))Read fills and settlements
fills = requests.get(f"{BASE}/portfolio/fills", headers=headers("GET", "/portfolio/fills")).json()["fills"] settled = requests.get(f"{BASE}/portfolio/settlements", headers=headers("GET", "/portfolio/settlements")).json()Use the websocket for fills in a live bot; polling the order endpoint is rate limited.
Free history nobody mentions
The candlesticks endpoint returns the bid, ask and volume history of any market at a chosen interval, back to listing. For research it replaces a logger you have not written yet.
path = f"/series/KXBTC15M/markets/{ticker}/candlesticks"
c = requests.get(BASE + path, params={"start_ts": t0, "end_ts": t1, "period_interval": 1}, headers=headers("GET", path)).json()