
Kalshi API: Keys, Endpoints, WebSocket and Python
The Kalshi API gives REST, WebSocket and FIX access to a CFTC-regulated exchange. Market data needs no key. Trading needs a signed API key.
The Kalshi API is the programmatic interface to Kalshi, a prediction market exchange regulated by the CFTC. It has a REST API, a WebSocket API and a FIX API. The REST market data endpoints need no API key. Orders, portfolio data and WebSocket streams need an API key, and you sign each request with its private key.
This guide covers the endpoints, the demo environment, API keys and request signing, and the market data model. It also covers rate limits, a minimal order on demo and a WebSocket stream. All examples are in Python. We ran the public market data example against production on November 1, 2026, and the output below is from that run.
What you will learn
- Which base URLs to use for production, demo, WebSocket and FIX
- How to create a Kalshi API key and sign a request with Ed25519 or RSA-PSS
- How series, events, markets and tickers fit together
- How to read the order book, which lists bids only
- How the token-bucket rate limits and tiers work
- How to place and cancel an order on demo and stream updates over WebSocket
- How Kalshi compares with Polymarket for bot builders
Prerequisites
- Python 3.10+ (
python3 --version) - For signed requests: a Kalshi account and an API key. Start with a demo account so that you trade with mock funds.
pip install requests cryptography "websockets>=14"Kalshi API endpoints
Kalshi runs separate production and demo environments. Credentials are not shared between them. A demo key works only against demo endpoints, and a production key works only against production endpoints.
| Interface | Production | Demo |
|---|---|---|
| REST | https://external-api.kalshi.com/trade-api/v2 | https://external-api.demo.kalshi.co/trade-api/v2 |
| WebSocket | wss://external-api-ws.kalshi.com/trade-api/ws/v2 | wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2 |
| FIX order entry | mm.fix.elections.kalshi.com | fix.demo.kalshi.co |
| FIX market data | marketdata.fix.elections.kalshi.com | marketdata.fix.demo.kalshi.co |
The external-api hosts are the ones Kalshi recommends for API traders. The older shared hosts, https://api.elections.kalshi.com/trade-api/v2 and https://demo-api.kalshi.co/trade-api/v2, still work. Many older tutorials use them. The elections subdomain serves all Kalshi markets, not only election markets.
The FIX API uses FIXT.1.1 with FIX50SP2 over TLS 1.2 or higher. Order entry uses port 8228 (no retransmission) or 8230 (with retransmission), and market data uses port 8233. Kalshi allows one FIX connection per API key. FIX messages use the same rate-limit buckets as the equivalent REST calls.
Kalshi publishes machine-readable specs: openapi.yaml for REST and asyncapi.yaml for WebSocket. The docs index at docs.kalshi.com/llms.txt lists every page as Markdown, which helps when you point a coding agent at the API.
Kalshi API key setup
Kalshi API keys are asymmetric key pairs. You sign each request with the private key, and Kalshi verifies the signature with the public key it has on file. Kalshi supports two key types:
| Key type | Signature algorithm | Notes |
|---|---|---|
| Ed25519 | Ed25519 over the text to sign | The default in the key dialog. Official SDKs support it from version 3.31.0. |
| RSA (2048-bit) | RSA-PSS with SHA-256, MGF1 with SHA-256, salt length equal to the digest length | Use it if your client supports only RSA-PSS. |
To create a key:
- Sign in to kalshi.com or demo.kalshi.co.
- Open Account & security, then API Keys, and click Create Key.
- Save the Key ID (a UUID such as
a952bcbe-ec3b-4b5b-b8f9-11dae589608c) and the private key PEM file that downloads.
Kalshi does not store the private key. When you close the page, you cannot get it again. To keep the private key on your machine only, generate a key pair yourself. Then paste the public key into the same dialog:
openssl genpkey -algorithm ed25519 -out kalshi.key
openssl pkey -in kalshi.key -pubout -out kalshi.pubYou can also create keys through the API: POST /api_keys registers a public key, and POST /api_keys/generate returns a new key pair.
How Kalshi request signing works
Every authenticated request includes three headers:
| Header | Value |
|---|---|
KALSHI-ACCESS-KEY | Your Key ID |
KALSHI-ACCESS-TIMESTAMP | Current Unix time in milliseconds |
KALSHI-ACCESS-SIGNATURE | Base64 signature of timestamp + METHOD + path |
The path is the full URL path from the root, without the host and without the query string. For https://external-api.kalshi.com/trade-api/v2/portfolio/orders?limit=5 you sign:
1703123456789GET/trade-api/v2/portfolio/ordersThe PEM header does not tell you the key type, because RSA keys in PKCS#8 format also start with -----BEGIN PRIVATE KEY-----. Load the key and check its type, as this helper does:
# kalshi_client.py
import base64
import os
import time
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
BASE_URL = os.environ.get("KALSHI_BASE_URL", "https://external-api.demo.kalshi.co/trade-api/v2")
API_PREFIX = "/trade-api/v2"
KEY_ID = os.environ["KALSHI_KEY_ID"]
with open(os.environ["KALSHI_PRIVATE_KEY_PATH"], "rb") as f:
PRIVATE_KEY = serialization.load_pem_private_key(f.read(), password=None)
def sign(message: str) -> str:
data = message.encode("utf-8")
if isinstance(PRIVATE_KEY, Ed25519PrivateKey):
sig = PRIVATE_KEY.sign(data)
else: # RSA: RSA-PSS with SHA-256, salt length = digest length
sig = PRIVATE_KEY.sign(
data,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.DIGEST_LENGTH),
hashes.SHA256(),
)
return base64.b64encode(sig).decode("utf-8")
def auth_headers(method: str, path: str, prefix: str = API_PREFIX) -> dict:
ts = str(int(time.time() * 1000)) # milliseconds
signed_path = prefix + path.split("?")[0] # full path from the root, no query string
return {
"KALSHI-ACCESS-KEY": KEY_ID,
"KALSHI-ACCESS-TIMESTAMP": ts,
"KALSHI-ACCESS-SIGNATURE": sign(ts + method + signed_path),
}
def request(method: str, path: str, **kwargs) -> requests.Response:
headers = auth_headers(method, path)
return requests.request(method, BASE_URL + path, headers=headers, timeout=10, **kwargs)Test it with your balance:
from kalshi_client import request
r = request("GET", "/portfolio/balance")
print(r.status_code, r.json()) # balance is in centsWe checked both signature types against their public keys. With an unregistered Key ID, the demo API answers 401 with {"error":{"code":"authentication_error",...}}. The most common causes of a 401 are a timestamp in seconds instead of milliseconds, a signed path without the /trade-api/v2 prefix, or a signed query string.
The official Kalshi Python SDK
Kalshi publishes kalshi_python_sync and kalshi_python_async on PyPI and kalshi-typescript on npm. Kalshi deprecated the old kalshi-python package. The Python SDK needs Python 3.13 or later and does the signing for you:
from kalshi_python_sync import Configuration, KalshiClient
config = Configuration(host="https://external-api.demo.kalshi.co/trade-api/v2")
config.api_key_id = "your-key-id"
config.private_key_pem = open("kalshi.key").read()
client = KalshiClient(config)
print(client.get_balance())Kalshi notes that the SDKs can lag the API. It treats the OpenAPI and AsyncAPI specs as the source of truth and recommends generating your own client or integrating directly for production. The rest of this guide uses requests so that each field stays visible.
Kalshi market data without authentication
Series, events, markets, trades and order books are public. You can read them from production with no key:
# market_data.py
from decimal import Decimal
import requests
BASE = "https://external-api.kalshi.com/trade-api/v2"
def get(path: str, **params) -> dict:
r = requests.get(f"{BASE}{path}", params=params, timeout=10)
r.raise_for_status()
return r.json()
# Open markets in one series: the daily high temperature in New York City
markets = get("/markets", series_ticker="KXHIGHNY", status="open", limit=100)["markets"]
market = max(markets, key=lambda m: Decimal(m["volume_fp"]))
print(market["ticker"], "|", market["title"], "|", market["yes_sub_title"])
print("yes bid", market["yes_bid_dollars"], "yes ask", market["yes_ask_dollars"], "volume", market["volume_fp"])
# The order book holds bids only: yes bids and no bids, best bid last
book = get(f"/markets/{market['ticker']}/orderbook", depth=5)["orderbook_fp"]
best_yes_bid = Decimal(book["yes_dollars"][-1][0]) if book["yes_dollars"] else None
best_no_bid = Decimal(book["no_dollars"][-1][0]) if book["no_dollars"] else None
best_yes_ask = Decimal("1") - best_no_bid if best_no_bid is not None else None
print("yes bids:", book["yes_dollars"])
print("no bids: ", book["no_dollars"])
print("best yes bid", best_yes_bid, "| best yes ask (1 - best no bid)", best_yes_ask)Output from our run on November 1, 2026:
KXHIGHNY-26NOV01-T60 | Will the maximum temperature be <60° on Nov 1, 2026? | 59° or below
yes bid 0.5500 yes ask 0.5600 volume 9552.32
yes bids: [['0.5100', '177.00'], ['0.5200', '127.90'], ['0.5300', '14.36'], ['0.5400', '7.00'], ['0.5500', '16.00']]
no bids: [['0.4000', '79.00'], ['0.4100', '110.08'], ['0.4200', '61.00'], ['0.4300', '107.00'], ['0.4400', '37.00']]
best yes bid 0.5500 | best yes ask (1 - best no bid) 0.5600The implied ask of $0.56 matches the yes_ask_dollars field on the market. Other public endpoints include GET /events, GET /series/{series_ticker}, GET /markets/trades, GET /series/{series_ticker}/markets/{ticker}/candlesticks (1-minute, 1-hour and 1-day bars. start_ts, end_ts and period_interval are required) and GET /exchange/status.
List endpoints page with a cursor. The default page size is 100 and /markets accepts up to 1,000. Send the returned cursor back as ?cursor= until it comes back empty. Settled markets and old trades move to the /historical/... endpoints after the cutoff dates that GET /historical/cutoff returns.
Kalshi API concepts: series, events, markets and prices
Kalshi organizes markets in three levels:
| Object | Example ticker | What it is |
|---|---|---|
| Series | KXHIGHNY | A recurring question, such as the daily high temperature in New York City |
| Event | KXHIGHNY-26NOV01 | One instance of the series, for one date. It belongs to one series through series_ticker. |
| Market | KXHIGHNY-26NOV01-T60 | One binary yes/no contract inside the event. It belongs to one event through event_ticker. |
Tickers usually follow the series-event-market pattern, but Kalshi says there are exceptions. Do not parse ticker strings to find relationships. Read series_ticker and event_ticker from the API instead.
Prices. A yes contract pays $1 if the market resolves yes, and a no contract pays $1 if it resolves no. Prices are between $0 and $1, so a price of $0.56 is 56 cents, or an implied probability of 56%. Older Kalshi code used integer-cent fields such as yes_bid. Current responses use fixed-point dollar strings with up to 4 decimal places, with a _dollars suffix ("0.5600"). Contract counts are strings with a _fp suffix and up to 2 decimal places ("16.00"), because some markets allow fractional contracts.
Tick size. Each market lists its valid prices in price_ranges, as bands of {start, end, step}. Some markets trade in steps of $0.01. Others use finer steps near $0 and $1, down to $0.0001. Snap order prices to the band's step, because the exchange rejects prices that are not on the grid. Whole-cent prices are valid in every structure.
The order book lists bids only. GET /markets/{ticker}/orderbook returns yes_dollars and no_dollars. Each is a list of [price, count] pairs, sorted from low to high, so the best bid is the last element. There are no asks, because a yes bid at price X is the same as a no ask at $1 minus X. To get the best yes ask, subtract the best no bid from $1, as the example above does.
Order direction. Orders, fills and trades use outcome_side (yes or no) and book_side (bid or ask). bid always means long yes and ask always means long no. Kalshi deprecated the older action and side fields.
Kalshi API rate limits and tiers
Kalshi meters authenticated requests with token buckets. Most requests cost 10 tokens. Your tier sets how many tokens per second refill. Reads and writes use separate buckets:
| Tier | Read (tokens/s) | Write (tokens/s) | How you get it |
|---|---|---|---|
| Basic | 200 | 100 | Account signup |
| Advanced | 300 | 300 | Call the Upgrade Account API Usage Level endpoint |
| Expert | 600 | 600 | Trading volume, or assigned by Kalshi |
| Premier | 1,200 | 1,200 | Trading volume, or assigned by Kalshi |
| Paragon | 2,400 | 2,400 | Trading volume, or assigned by Kalshi |
| Prime | 4,800 | 4,800 | Trading volume, or assigned by Kalshi |
| Prestige | 12,000 | 9,600 | Trading volume, or assigned by Kalshi |
At 10 tokens per call, Basic gives about 20 reads and 10 orders per second. Write buckets above Basic store up to 3 seconds of budget. An idle Advanced account can therefore send a burst of 90 orders before it falls back to 30 per second. Batch endpoints do not save tokens: 25 orders in one batch cost 250 tokens.
Some details matter for bots:
GET /portfolio/balancehas its own bucket: 1 call per second on Basic and Advanced, 5 per second above. Use thefillWebSocket channel to see balance changes instead of polling.- A throttled request returns
429with{"error": "too many requests"}and noRetry-Afterheader. There is no penalty period. Back off exponentially and retry. GET /account/limitsreturns your current tier and bucket sizes, andGET /account/endpoint_costslists endpoints that cost more or less than 10 tokens.- WebSocket connections have a per-user limit: 200 concurrent connections on Basic, Advanced and Expert, rising to 1,000 on Prime and Prestige.
The Kalshi docs give token costs only for authenticated requests. They do not publish a limit for unauthenticated market data reads. Treat a 429 on public endpoints the same way.
Place and cancel an order on demo
This example places a 1-contract yes bid at $0.01 on an open demo market, then cancels it. It uses the V2 order endpoint, POST /portfolio/events/orders. The legacy create and cancel endpoints under /portfolio/orders are no longer in Kalshi's API spec, so use the V2 paths.
# demo_order.py: run against demo only
import uuid
import requests
from kalshi_client import BASE_URL, request
assert "demo" in BASE_URL, "This example only runs against the demo environment"
# An open demo market, excluding multivariate combos (public read, no signature)
market = requests.get(
f"{BASE_URL}/markets",
params={"status": "open", "limit": 1, "mve_filter": "exclude"},
timeout=10,
).json()["markets"][0]
print("market:", market["ticker"])
order = {
"ticker": market["ticker"],
"side": "bid", # bid = long yes, ask = long no
"count": "1", # contracts, fixed-point string
"price": "0.0100", # $0.01, fixed-point dollars
"time_in_force": "good_till_canceled",
"self_trade_prevention_type": "taker_at_cross",
"client_order_id": str(uuid.uuid4()), # lets you retry without a duplicate order
}
r = request("POST", "/portfolio/events/orders", json=order)
print(r.status_code, r.text)
if r.status_code == 201:
order_id = r.json()["order_id"]
c = request("DELETE", f"/portfolio/events/orders/{order_id}", params={"market_ticker": market["ticker"]})
print("cancel:", c.status_code, c.text)Notes on the order fields:
time_in_forceisgood_till_canceled,immediate_or_cancelorfill_or_kill. For an order that expires at a set time, usegood_till_canceledwithexpiration_time(Unix seconds).- You must send
self_trade_prevention_type. The value istaker_at_crossormaker. client_order_idis optional, but use it. Kalshi says requests are not idempotent, so if a request times out, check your open orders before you resend it.- A successful create returns
201withorder_id,fill_countandremaining_count. Change an order withPOST /portfolio/events/orders/{order_id}/amend.
Kalshi WebSocket API
The WebSocket API streams order book changes, ticker updates, public trades, market lifecycle events and your own fills and positions. The handshake needs the same three signed headers as REST, signed over GET /trade-api/ws/v2. We tested a connection without them, and production returned HTTP 401. After you connect, public channels (ticker, trade, market_lifecycle_v2) need no extra authorization. Kalshi lists orderbook_delta, fill, market_positions, order_group_updates and communications as private channels.
# stream.py
import asyncio
import json
import os
import websockets
from kalshi_client import auth_headers
WS_URL = os.environ.get("KALSHI_WS_URL", "wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2")
async def stream(tickers: list[str]) -> None:
# Sign GET /trade-api/ws/v2: the WebSocket path, not the REST prefix
headers = auth_headers("GET", "/trade-api/ws/v2", prefix="")
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
await ws.send(json.dumps({
"id": 1,
"cmd": "subscribe",
"params": {"channels": ["ticker", "orderbook_delta"], "market_tickers": tickers},
}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "ticker":
m = msg["msg"]
print(m["market_ticker"], "yes bid", m["yes_bid_dollars"], "yes ask", m["yes_ask_dollars"])
elif msg["type"] in ("orderbook_snapshot", "orderbook_delta"):
print(msg["type"], msg["msg"]["market_ticker"])
elif msg["type"] == "error":
print("error", msg["msg"])
asyncio.run(stream([os.environ["KALSHI_TICKER"]]))An orderbook_delta subscription sends an orderbook_snapshot first, then incremental orderbook_delta messages. Apply the deltas to the snapshot to keep a local book. By default, no-side levels on these channels use no-leg prices. Add "use_yes_price": true to the subscribe params to get both sides on the yes price scale. Kalshi plans to make that the default in a future release.
The websockets library answers ping frames for you. Add reconnect logic with exponential backoff. If the server sends error code 25 (subscription buffer overflow), subscribe to fewer markets or read messages faster.
Kalshi vs Polymarket for bot builders
Both are binary prediction markets with a central limit order book, but they differ in access, auth and settlement. For the Polymarket side in detail, read our Polymarket API guide.
| Kalshi | Polymarket (polymarket.com) | |
|---|---|---|
| Regulation | CFTC-regulated Designated Contract Market | International venue. Polymarket US is a separate CFTC-regulated exchange. |
| Account | Identity verification. You must be 18 or older. Kalshi restricts some jurisdictions. | A wallet. Polymarket blocks trading from some regions (check GET https://polymarket.com/api/geoblock). |
| API auth | Key ID plus an Ed25519 or RSA-PSS signature on every request | A wallet signature (L1) creates CLOB credentials (L2), which you send on trading requests |
| Public market data | REST: series, events, markets, order books, trades | Gamma, CLOB reads and Data API |
| Public WebSocket | No. Every connection needs a signed handshake. | Yes. The market channel needs no key. |
| Prices | Dollar strings, 0.0000 to 1.0000 | Decimal prices, 0 to 1 |
| Settlement | Off-chain. A winning contract pays $1 to your Kalshi account. | On-chain on Polygon. Outcome tokens are ERC-1155 and collateral is pUSD. |
The settlement row shapes the infrastructure. A Kalshi bot needs only Kalshi's API, because balances and positions live on the exchange. A Polymarket bot reads balances, approvals and resolutions from Polygon, so it pairs the CLOB with a Polygon RPC endpoint.
In December 2025, Kalshi announced on-chain versions of its markets on Solana, built with Jupiter and DFlow (Decrypt). That product is separate from the Kalshi Trade API that this guide covers.
Going to production
Keep keys on the server. Store the private key in a secret manager, never in client-side code. Use separate keys per process if you run FIX, because Kalshi allows one FIX connection per key.
Trust WebSocket events over polling. Kalshi notes a short delay before exchange events show up in REST responses. Combine the responses to your order calls with the fill and user_orders WebSocket channels for the most accurate state.
Read price_ranges per market. Do not hard-code a $0.01 tick. Markets move between price structures, and the market lifecycle channel announces the change.
Plan for maintenance. Kalshi publishes maintenance windows and trading pauses. Set cancel_order_on_pause on orders that must not rest through a pause.
Next steps
- Read the Polymarket API guide to add a second venue to the same bot.
- Build a full strategy loop with our guide to a Polymarket copy-trading bot.
- For the basics, read what a prediction market is.
The Kalshi API runs off-chain, so it needs no blockchain node. If your bot also trades Polymarket, it needs a Polygon RPC endpoint for balances and settlement. Dwellir serves Polygon among 140+ chains, and you can create a free Dwellir account to get one.
Sources
- Kalshi API documentation
- API Environments and Endpoints
- Test In The Demo Environment
- API Keys
- Quick Start: Authenticated Requests
- Quick Start: Market Data
- Quick Start: Create your first order
- Quick Start: WebSockets
- Kalshi SDKs
- Rate Limits and Tiers
- Orderbook Responses
- Order direction
- Fixed-Point Representation
- Kalshi Glossary
- Understanding Pagination
- Historical Data
- Market Settlement
- Create Order (V2)
- Cancel Order (V2)
- Get Markets
- Get Market Orderbook
- FIX Connectivity
- How is Kalshi regulated?
- Signing Up as an Individual
- Polymarket documentation and Wallets and Authentication
- Decrypt: Kalshi pushes further into crypto with on-chain prediction markets on Solana
Frequently Asked Questions
Does Kalshi have a public API?
Yes. The Kalshi Trade API has REST, WebSocket and FIX interfaces. The REST market data endpoints, such as GET /markets, /events, /series and /markets/{ticker}/orderbook, need no authentication. The production base URL is https://external-api.kalshi.com/trade-api/v2. Orders, portfolio data and all WebSocket connections need a signed API key.
How do I get a Kalshi API key?
Sign in to Kalshi (or the demo site), open Account & security, then API Keys, and click Create Key. Kalshi shows a Key ID and downloads the private key as a PEM file. Kalshi does not store the private key, so save it immediately. You can also register your own Ed25519 or RSA public key. Demo keys and production keys are separate.
How does Kalshi API authentication work?
Each authenticated request includes three headers: KALSHI-ACCESS-KEY (the Key ID), KALSHI-ACCESS-TIMESTAMP (Unix time in milliseconds) and KALSHI-ACCESS-SIGNATURE. The signature is the base64 signature of timestamp + HTTP method + path, where the path starts at /trade-api/v2 and has no query string. Ed25519 keys sign that text directly. RSA keys use RSA-PSS with SHA-256.
Does Kalshi have a demo or sandbox API?
Yes. The demo environment at demo.kalshi.co uses mock funds. Its REST base URL is https://external-api.demo.kalshi.co/trade-api/v2 and its WebSocket URL is wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2. Create separate API keys in the demo account, because production keys do not work there.
What are the Kalshi API rate limits?
Kalshi uses token buckets. Most requests cost 10 tokens, and your tier sets how many tokens per second refill. You get the Basic tier at signup. It refills 200 Read tokens and 100 Write tokens per second, which is about 20 reads and 10 orders per second. Higher tiers come from an API call (Advanced) or from trading volume. A rate-limited request returns HTTP 429.
Is there an official Kalshi Python SDK?
Yes. Kalshi publishes kalshi_python_sync and kalshi_python_async on PyPI, and kalshi-typescript on npm. Kalshi deprecated the old kalshi-python package. Kalshi says the SDKs can lag the API and treats its OpenAPI and AsyncAPI specs as the source of truth. You can also sign requests yourself with requests and cryptography.
Does the Kalshi WebSocket need authentication?
Yes. The WebSocket handshake needs the same three signed headers as REST, signed over GET /trade-api/ws/v2. A connection without them gets HTTP 401. After you connect, public channels such as ticker and trade need no extra authorization. Private channels such as fill and market_positions return your own data.
Polymarket API: Endpoints, API Keys and Trading Guide
The Polymarket API is three HTTP services: Gamma lists markets, CLOB serves order books and takes orders, Data returns positions. Reading needs no API key.
Build a Real-Time Robinhood Chain Whale Tracker in Python
Track whale trades on Robinhood Chain in real time: subscribe to logs over WebSocket, decode large USDG and Stock Token transfers, and alert instantly.