Kalshi API Tutorial: Build and Host a Kalshi Trading Bot

The Kalshi API provides a way to read prediction market data and do trades via code. Market data is public and can be freely fetched without registration, while trading calls for an API key that signs each private request. Below you'll find the endpoints a Kalshi trading bot relies on, Python code for signing requests and sending orders, and the 2026 updates that leave many tutorials out of date.
What changed in 2026
Orders are sent through the V2 endpoints of
/portfolio/events/orders. Prices are quoted from the YES side, withsideeitherbidorask, and the legacy/portfolio/ordersendpoints are on their way out.Dollar prices are string values like
"0.5600", and counts are strings as well, like"10.00". Any code built around integer cents has to be rewritten.Kalshi introduced dedicated API hosts (
external-api.kalshi.com) in May 2026, and the oldapi.elections.kalshi.comhost keeps working.The Ed25519 key type came in September 2026 and is the default when you request a new key. RSA keys are still usable.
Token bucket rate limits with seven tiers were introduced, from Basic up to Prestige.
Facts listed above have been checked using Kalshi API documentation on 7 November 2026. Kalshi often changes its API, so always look at the changelog before going live.
The Kalshi API at a glance
| Interface | Production URL | Use it for |
|---|---|---|
| REST | https://external-api.kalshi.com/trade-api/v2 | Markets, events, order books, your balance, positions and orders |
| WebSocket | wss://external-api-ws.kalshi.com/trade-api/ws/v2 | Live order book changes, trades, tickers and your own fills |
| FIX | mm.fix.elections.kalshi.com (order entry) | FIX 5.0 SP2 sessions for institutional and market-making flow |
| Demo REST | https://external-api.demo.kalshi.co/trade-api/v2 | Testing with fake money |
| Demo WebSocket | wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2 | Testing live feeds |
Demo runs as a system apart from production. Demo keys work only against demo endpoints, so you must create different keys for each one.
The vast majority of bots need only REST and WebSocket. REST handles finding markets and placing orders, while the WebSocket delivers notifications, so there's no polling for each price change.
Reading market data without a key
Market data needs no authentication, and Kalshi arranges it into three classifications:
Series: any repeating question, such as the daily high temperature in New York (
KXHIGHNY).Event: one realization of a series, such as one specific day.
Market: one tradable contract inside the event, which is a single temperature band in this case. You trade using the market's own ticker.
BASE=https://external-api.kalshi.com/trade-api/v2
# Open markets in one series
curl "$BASE/markets?series_ticker=KXHIGHNY&status=open"
# One market, then its order book
curl "$BASE/markets/MARKET_TICKER"
curl "$BASE/markets/MARKET_TICKER/orderbook"
# Hourly candlesticks (period_interval is 1, 60 or 1440 minutes)
curl "$BASE/series/KXHIGHNY/markets/MARKET_TICKER/candlesticks?period_interval=60&start_ts=START&end_ts=END"
The order book consists of bids only: bids to buy YES and bids to buy NO. There is no ask book, because bidding 0.30 NO is equal to selling 0.70 YES. To find the best ask of YES, take the best NO bid and deduct it from 1.
Steps differ per market. Every market quotes its own price_ranges, and each price range has its own start, end, and step, expressed in USD. Round your prices to the step of whichever range they fall in. tick_size does not exist anymore.
Creating a Kalshi API key
Sign in to kalshi.com and go to your account settings.
In the API Keys panel, create a new key: Ed25519 by default, with RSA as an option.
Save the private key file right now because it’s displayed just once and can never be shown by Kalshi.
Remember the key ID, which each signed request needs.
You can also upload your own public key through the API with POST /api_keys, so Kalshi never learns your private key.
Remember that you shouldn’t store your private key in your code and repository. Keep it in a separate file on the server that only the bot's user can read (chmod 600), and point to it with an environment variable.
How request signing works
Every private request carries three headers:
| Header | Value |
|---|---|
KALSHI-ACCESS-KEY | Your key ID |
KALSHI-ACCESS-TIMESTAMP | The current time in milliseconds |
KALSHI-ACCESS-SIGNATURE | A base64 signature of timestamp + method + path |
Text to be signed is composed of the three elements without any separator: 1762500000000GET/trade-api/v2/portfolio/balance.
There are two main reasons for signing mistakes:
Sign the full path from the root of the API. You should sign
/trade-api/v2/portfolio/balance, NOT/portfolio/balance.Remove the query part. Even if your URL contains
?limit=5, you should still sign/trade-api/v2/portfolio/orders.
Ed25519 keys are used to sign this text as it is, while RSA keys use the RSASSA-PSS scheme with SHA-256 (MGF1 using SHA-256, and a salt length equal to the digest size). WebSocket handshake is signed in the same way with GET text and path /trade-api/ws/v2.
Placing an order with Python
This is an example using requests and cryptography. Kalshi releases SDKs (kalshi_python_sync, kalshi_python_async, and kalshi-typescript), but their documentation says that these SDKs sometimes lag behind the API. By signing the requests yourself, you know your implementation meets the current specification.
python3 -m venv ~/kalshi && . ~/kalshi/bin/activate
pip install requests cryptography
It will retrieve your balance, submit a very small post-only bid well below the market, and cancel it again. A post-only order is rejected rather than executed if it would match immediately, which makes it a safer way to test. Try it on demo first.
import base64
import os
import time
import uuid
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
HOST = os.environ.get("KALSHI_HOST", "https://external-api.demo.kalshi.co")
API = "/trade-api/v2"
KEY_ID = os.environ["KALSHI_KEY_ID"]
with open(os.environ["KALSHI_KEY_FILE"], "rb") as f:
KEY = serialization.load_pem_private_key(f.read(), password=None)
def headers(method: str, path: str) -> dict:
ts = str(int(time.time() * 1000))
msg = f"{ts}{method}{path.split('?')[0]}".encode()
if isinstance(KEY, Ed25519PrivateKey):
sig = KEY.sign(msg)
else:
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-TIMESTAMP": ts,
"KALSHI-ACCESS-SIGNATURE": base64.b64encode(sig).decode(),
"Content-Type": "application/json",
}
def call(method: str, path: str, body: dict | None = None) -> requests.Response:
full = API + path
r = requests.request(method, HOST + full, headers=headers(method, full),
json=body, timeout=10)
r.raise_for_status()
return r
print(call("GET", "/portfolio/balance").json())
ticker = "MARKET_TICKER"
order = call("POST", "/portfolio/events/orders", {
"ticker": ticker,
"client_order_id": str(uuid.uuid4()),
"side": "bid", # bid = buy YES, ask = sell YES
"count": "1.00",
"price": "0.0100", # dollars; must fit the market's price step
"time_in_force": "good_till_canceled",
"self_trade_prevention_type": "taker_at_cross",
"post_only": True,
}).json()
print("placed", order["order_id"], order["remaining_count"])
call("DELETE", f"/portfolio/events/orders/{order['order_id']}?market_ticker={ticker}")
print("cancelled")
Place the variables KALSHI_KEY_ID and KALSHI_KEY_FILE in the environment. Replace KALSHI_HOST with https://external-api.kalshi.com only if the demo run is successful.
Fields in an order to know:
| Field | What it does |
|---|---|
side | bid buys YES, ask sells YES. To buy NO at 0.30, send an ask at 0.70 |
time_in_force | good_till_canceled, immediate_or_cancel or fill_or_kill |
post_only | Rejects the order if it would take liquidity |
reduce_only | Only lets the order shrink an existing position |
client_order_id | Your own ID, so a retried request can be matched to the order it created |
expiration_time | Unix time in seconds, used with good_till_canceled |
The same path family processes the rest of an order's lifecycle:
Order batches creation:
POST /portfolio/events/orders/batchedOrder batches cancellation:
DELETE /portfolio/events/orders/batchedOrder amend:
POST /portfolio/events/orders/{order_id}/amendDecreasing order size:
POST /portfolio/events/orders/{order_id}/decreaseCancelling all resting orders:
DELETE /portfolio/events/orders(very helpful if the bot's data feed goes quiet).
Live data over WebSocket
Make the WebSocket handshake with the exact same three headers, but signed for GET and /trade-api/ws/v2. Subscribe:
{"id": 1, "cmd": "subscribe", "params": {"channels": ["orderbook_delta"], "market_tickers": ["MARKET_TICKER"]}}
orderbook_snapshot is your initial message; after it, you receive orderbook_delta messages and apply the updates to your copy of the book. Other helpful channels:
ticker: price and volume aggregatestrade: public tradesfill: your own fillsuser_orders: order updatesmarket_positions: position updatesmarket_lifecycle_v2: markets opening, closing and settling
Every 10 seconds Kalshi sends a ping message. Your client should respond by ponging back. Most WebSocket libraries handle this automatically. Disconnecting is an expected occurrence: reconnect, resubscribe all your channels, take the fresh snapshot and then verify your active orders over REST.
Rate limits
Kalshi's token bucket approach keeps reads and writes in separate budgets, and most operations require 10 tokens. At the Basic level, 200 read tokens/second therefore means about 20 read operations per second, and 100 write tokens means about 10 orders per second.
| Tier | Read tokens/s | Write tokens/s | How you get it |
|---|---|---|---|
| Basic | 200 | 100 | Default |
| Advanced | 300 | 300 | Self-upgrade once at least 1 of your last 100 orders came through the API |
| Expert | 600 | 600 | 30-day trading volume share |
| Premier | 1,200 | 1,200 | 30-day trading volume share |
| Paragon | 2,400 | 2,400 | 30-day trading volume share |
| Prime | 4,800 | 4,800 | 30-day trading volume share |
| Prestige | 12,000 | 9,600 | 30-day trading volume share |
The cost of batches is equal to the cost of sending individual calls: 25 calls within one batch will consume 250 tokens. A cancel costs 2 tokens, making it inexpensive. Balance endpoint gets access to its own dedicated mini-bucket that is one call per second on the Basic and Advanced tiers.
Beyond that limit the API will respond with HTTP 429 status code. Response will not contain any Retry-After header, so use exponential backoff. A bot getting many 429 responses should switch from polling to WebSocket data.
Fees
Kalshi charges takers a fee per trade, rounded up:
fee = 0.07 × contracts × price × (1 − price)
At a price of 0.50 the fee is highest, and it gets lower as the price moves toward 0 or 1. For instance, with 100 contracts at a price of 0.50 the fee is 1.75, but with 100 contracts at 0.10 it comes to 0.63. Some markets also levy a maker fee of 0.0175 × contracts × price × (1 − price), and some series apply their own multiplier, listed in Kalshi's fee schedule. Cancelling a resting order is free.
Who can use the Kalshi API
Kalshi is a CFTC-regulated exchange operated by KalshiEX LLC. The service requires a verified account; during the registration process, you prove who you are and which country you are resident of, and the service's help center asks that traders be at least 18 years old. According to Kalshi, traders from various countries are welcome. Its member agreement specifies the jurisdictions where trading is restricted, for example the UK, Canada, Australia, France and Singapore. Have a look at that list before building something.
Eligibility is tied to your registered account rather than to a certain server. Kalshi's documentation describes no IP-based block of the kind Polymarket uses, so relocating the bot's server does not change what your account may do.
Where to host a Kalshi trading bot
For a bot that places just a few orders per day, any stable server is fine. For market makers and bots responding to the news, though, the round trip to Kalshi's servers decides who gets filled first.
Kalshi doesn't state where its servers are. However, on 7 November 2026, external-api.kalshi.com pointed to an AWS load balancer in the us-east-2 region (Ohio). The older api.elections.kalshi.com host points to Amazon CloudFront edge addresses, which hide the location of the origin. That makes the US East Coast the sensible place to start, and our New York VPS is the nearest OrbitServers site.
The DNS name is no guarantee in itself, so measure from the servers you have in mind. The latency checker supports custom hosts, while the looking glass pings and traceroutes from our own network. Test regularly, because Kalshi can change its endpoints.
Beyond latency, a bot server needs these:
A fixed IP address and a firewall. Permit SSH access only from your own IP. Our guide to securing SSH on Ubuntu covers what you need to know.
Process supervision. Run the bot under systemd so that it starts up again after a crash or a reboot.
Accurate time. Make sure chrony or systemd-timesyncd is always active. Requests are signed with a millisecond timestamp, and order timing matters.
Monitoring. Look out for 429 errors, WebSocket reconnects and refused orders, and get alerted before a broken loop costs money.
The same applies to other prediction markets. Our piece on where to host a Polymarket trading bot covers how the two venues differ, and our Polymarket API guide is the companion to this one. OrbitServers VPS plans start from $39.99/mo, with NVMe drives and full root access.
Frequently asked questions
Is the Kalshi API free?
Yes. No account is required for market data and there will be no charge for it. Fees for API trading are the same as for trading on the website; there is no extra API charge.
Do I need an API key to get Kalshi market data?
No. Series, events, markets, order books and candlesticks are public information. You only need a key to see your balances, positions, fills and orders.
Should I use an Ed25519 or an RSA key?
Ed25519 is the default for new keys, with smaller signatures that are faster to generate. RSA remains valid too. If your app uses Kalshi’s SDKs, Ed25519 support needs version 3.31.0 or later.
Can I use the Kalshi API from outside the United States?
Often, yes. Although it allows traders from various countries, its member agreement restricts trading in listed jurisdictions such as the UK, Canada and Australia. The verified country on your account will matter, not the server location.
How do I build a Kalshi trading bot?
Get started with the demo environment. Read markets over REST and keep the order book up to date over WebSocket. Use the V2 endpoints for orders, with client_order_id set. Add a kill switch that cancels all orders. Run it on a supervised, monitored server close to the Kalshi API.
Get started with Orbit Servers
Low-latency VPS, bare metal, and colocation across the US, EU, and APAC - provisioned instantly and built for performance-critical workloads.
Get startedRelated products
Written by
Julius
Product Manager
Julius is a product-minded builder who cares about making technology simpler and more useful for the people who rely on it. He’s always looking for ways to improve things, one small step at a time, and enjoys turning good ideas into products that work.