The Kalshi API is the REST, WebSocket, and FIX interface to the only CFTC-regulated prediction market exchange in the US. Production requests go to https://api.elections.kalshi.com/trade-api/v2; the demo sandbox is https://external-api.demo.kalshi.co/trade-api/v2. Market data endpoints are public, but every trading call needs RSA-PSS request signing with a generated key pair, and orders go to POST /portfolio/events/orders (CreateOrder V2) — the legacy /portfolio/orders mutation endpoints were removed in June 2026. All prices and sizes are decimal strings, not integer cents: read yes_bid_dollars ("0.6500"), not yes_bid.
curl -s "https://api.elections.kalshi.com/trade-api/v2/markets?status=open&limit=5&series_ticker=KXBTCD"
Endpoints and field names verified against the live API on 2026-08-30.
This guide covers the API end to end — authentication, market discovery, order placement, WebSocket streaming, and the FIX gateway — taking you from the demo sandbox to a production trading bot. The patterns are familiar (REST over HTTPS, WebSocket for streaming, FIX for institutional connectivity), but the RSA signing flow and Kalshi’s market structure both have quirks worth understanding before you write order-placing code. Start in the demo environment, then switch hosts when you are ready for real money.
Last verified: September 2026. Check the Kalshi Changelog for the latest updates. Major 2026 changes reflected below: the legacy order-mutation endpoints were removed in June 2026 (replaced by CreateOrder V2 at /portfolio/events/orders), and rate limits moved to a token-cost system in April 2026.
Try it live: Test Kalshi and Polymarket endpoints side-by-side in the API Playground — no setup required.
Why Kalshi for Agents
Kalshi stands apart from Polymarket in several ways that matter for autonomous agents:
- Regulated: CFTC Designated Contract Market — legal for U.S. entities and institutions
- Fiat settlement: USD deposits and withdrawals via traditional banking (no crypto wallets needed)
- Demo environment: Full-featured sandbox for testing without real money
- Published fee schedule: Trading fees apply per Kalshi’s published fee schedule — taker fees exist (the API’s post-only option exists specifically to avoid them), so factor fees into edge calculations
- FIX protocol support: Institutional-grade connectivity for low-latency strategies
- Familiar auth patterns: RSA-PSS signed requests, not blockchain wallet signatures
The tradeoff: Kalshi has less total liquidity than Polymarket, fewer concurrent markets, and more restricted market categories (due to CFTC oversight). But for agents operating within U.S. regulatory frameworks, or for strategies that need the demo environment for safe testing, Kalshi is often the better starting point.
Architecture Overview
Kalshi’s API is simpler than Polymarket’s — there’s essentially one API with different access methods.
| Interface | URL | Purpose |
|---|---|---|
| REST API | https://api.elections.kalshi.com/trade-api/v2 | Market data, order management, account operations |
| Demo REST | https://external-api.demo.kalshi.co/trade-api/v2 | Full sandbox environment (same endpoints) |
| WebSocket | wss://external-api-ws.kalshi.com/trade-api/ws/v2 | Real-time market and order updates |
| Demo WebSocket | wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2 | Sandbox WebSocket |
| FIX | Contact Kalshi for connection details | Institutional low-latency trading |
These are the hosts recommended in Kalshi’s current documentation. The docs also list external-api.kalshi.com alongside api.elections.kalshi.com for production REST. The legacy hosts (demo-api.kalshi.co, wss://api.elections.kalshi.com/trade-api/ws/v2) still respond and remain supported, but new integrations should use the documented external-api hosts.
All market data, trading, and account endpoints live under the same base URL. This is a major simplification compared to Polymarket’s multi-service architecture.
How Kalshi Markets Work
Understanding Kalshi’s market structure is essential before you start coding.
Event Contracts
Every Kalshi market is a binary contract that pays out $1.00 if the event occurs, or $0.00 if it doesn’t. You buy contracts at prices between $0.01 and $0.99 (expressed in cents internally — more on this below).
Example: A contract asking “Will Bitcoin be above $100,000 on March 31?” might be trading at $0.65 (65 cents). If Bitcoin is above $100K on that date, you receive $1.00 per contract. If not, you receive $0.00. Your profit on a YES bet is $0.35 per contract; your risk is $0.65.
Ticker Format
Kalshi tickers encode market information directly:
KXNFLGAME-25OCT12CLEPIT
│ │ │ │ │
│ │ │ │ └── Teams (CLE vs PIT)
│ │ │ └──────── Date (October 12)
│ │ └─────────── Year (2025)
│ └─────────────────── Series (NFL Game)
└───────────────────── KX prefix (Kalshi Exchange)
INXCHI-25JAN31-T69.5
│ │ │
│ │ └── Threshold value
│ └─────────── Expiry date
└───────────────── Series ticker
Prices: Fixed-Point Dollar Strings
Critical detail: Kalshi completed a fixed-point migration in March 2026. Prices are now expressed as dollar strings (e.g., "0.6500" for $0.65). The old integer cent fields (yes_price: 65) have been removed from API responses. When placing orders on the CreateOrder V2 endpoint, use price — a fixed-point string with up to 6 decimal places — with side: "bid" or side: "ask", and count as a fixed-point string (e.g., "10.00"). Some markets support subpenny pricing (tick sizes as small as $0.0001) — check the price_level_structure field on market responses. Fractional contract sizes are always supported; the old fractional_trading_enabled flag was removed from the market schema in July 2026.
Getting Started: Demo Environment
The biggest advantage Kalshi has for developers is a fully functional demo environment. Use it.
Demo REST: https://external-api.demo.kalshi.co/trade-api/v2
Demo WebSocket: wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2
(The legacy demo-api.kalshi.co host still works, but the external-api hosts above are what Kalshi’s docs now recommend.)
The demo environment has:
- Fake money with no risk
- All the same endpoints as production
- Real-ish market data to test against
- Separate API keys from production
Always build and test in demo first. Switch to production only after your bot behaves correctly.
Authentication: RSA-PSS Signing
Kalshi uses RSA-PSS (Probabilistic Signature Scheme) for API authentication. This is more complex than a simple API key header, but provides stronger security.
Step 1: Generate API Keys
- Log into your Kalshi account (or demo account)
- Go to Settings → API
- Generate a new API key pair
- Download the private key (PEM file) — store this securely
- Note the API Key ID — this is your public identifier
Step 2: Understand the Signing Process
Every authenticated request requires three custom headers:
KALSHI-ACCESS-KEY: <your-api-key-id>
KALSHI-ACCESS-SIGNATURE: <rsa-pss-signature>
KALSHI-ACCESS-TIMESTAMP: <unix-timestamp-in-milliseconds>
The signature is computed over the concatenation of: timestamp + method + path
Step 3: Implement Request Signing (Python)
import time
import base64
import requests
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
class KalshiClient:
def __init__(self, key_id: str, private_key_path: str, base_url: str = None):
self.key_id = key_id
self.base_url = base_url or "https://api.elections.kalshi.com/trade-api/v2"
with open(private_key_path, "rb") as f:
self.private_key = serialization.load_pem_private_key(
f.read(), password=None
)
def _sign(self, timestamp_ms: str, method: str, path: str) -> str:
"""Sign the request using RSA-PSS"""
message = f"{timestamp_ms}{method}{path}".encode("utf-8")
signature = self.private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH
),
hashes.SHA256()
)
return base64.b64encode(signature).decode("utf-8")
def _headers(self, method: str, path: str) -> dict:
"""Generate authenticated headers for a request"""
timestamp = str(int(time.time() * 1000))
signature = self._sign(timestamp, method, path)
return {
"KALSHI-ACCESS-KEY": self.key_id,
"KALSHI-ACCESS-SIGNATURE": signature,
"KALSHI-ACCESS-TIMESTAMP": timestamp,
"Content-Type": "application/json",
"Accept": "application/json"
}
def get(self, path: str, params: dict = None):
url = f"{self.base_url}{path}"
headers = self._headers("GET", path)
return requests.get(url, headers=headers, params=params)
def post(self, path: str, data: dict = None):
url = f"{self.base_url}{path}"
headers = self._headers("POST", path)
return requests.post(url, headers=headers, json=data)
def delete(self, path: str, data: dict = None):
url = f"{self.base_url}{path}"
headers = self._headers("DELETE", path)
return requests.delete(url, headers=headers, json=data)
Using the Demo Environment
# For demo/testing:
client = KalshiClient(
key_id="your-demo-api-key-id",
private_key_path="path/to/demo-private-key.pem",
base_url="https://external-api.demo.kalshi.co/trade-api/v2"
)
# For production:
client = KalshiClient(
key_id="your-prod-api-key-id",
private_key_path="path/to/prod-private-key.pem",
base_url="https://api.elections.kalshi.com/trade-api/v2"
)
Using the Official Python SDK
Kalshi provides official Python SDKs — synchronous and asynchronous — that handle authentication for you. The old kalshi-python package is deprecated.
# Synchronous (recommended for most bots)
pip install kalshi_python_sync
# Asynchronous (for async/await workflows)
pip install kalshi_python_async
from kalshi_python_sync import Configuration, KalshiClient
config = Configuration(
host="https://external-api.demo.kalshi.co/trade-api/v2" # Demo
)
# Read private key from file
with open("path/to/private_key.pem", "r") as f:
private_key = f.read()
config.api_key_id = "your-api-key-id"
config.private_key_pem = private_key
client = KalshiClient(config)
# Check balance
balance = client.get_balance()
print(f"Balance: ${balance.balance_dollars}") # balance_dollars is a dollar string
# The integer-cent field is also still present: balance.balance / 100
A TypeScript SDK is also available:
npm install kalshi-typescript
import { Configuration, PortfolioApi } from 'kalshi-typescript';
const config = new Configuration({
apiKey: 'your-api-key-id',
privateKeyPath: 'path/to/your/private-key.pem',
basePath: 'https://api.elections.kalshi.com/trade-api/v2'
});
const portfolioApi = new PortfolioApi(config);
const balance = await portfolioApi.getBalance();
See kalshi_python_sync on PyPI and kalshi-typescript on npm for the latest versions.
REST API Reference
Market Data (Public)
GET /markets — List All Markets
The fully-qualified URL, which is the call most agents reach for first:
https://api.elections.kalshi.com/trade-api/v2/markets?status=open&limit=1000
No authentication is required for this endpoint — it is a plain public GET. limit accepts 1–1000; limit=1001 returns 400 Bad Request, as does any unrecognized status value. The response is a two-key object: markets (this page) and cursor (an opaque token you pass back as ?cursor= for the next page).
curl -s "https://api.elections.kalshi.com/trade-api/v2/markets?status=open&limit=1000" \
| jq '{count: (.markets | length), has_cursor: (.cursor != ""), first: .markets[0].ticker}'
{
"count": 1000,
"has_cursor": true,
"first": "KXMVECROSSCATEGORY-SHARD1-S202699B940ED616-163EE09ECDC"
}
Gotcha — the first pages are entirely multivariate combo contracts. Verified against the live API on 2026-08-30: pages 1 and 2 of ?status=open&limit=1000 came back 1000/1000 multivariate event (MVE) contracts on both pages — sharded KXMVE* tickers auto-generated from combinations of underlying markets, with volume_fp of 0.00 and no order book. Zero ordinary markets appeared in the first 2,000 results. If you paginate this endpoint expecting NFL games or Fed decisions, you will burn a lot of requests before you reach one.
The fix is the mve_filter parameter:
curl -s "https://api.elections.kalshi.com/trade-api/v2/markets?status=open&limit=100&mve_filter=exclude" \
| jq '.markets[0] | {ticker, status, yes_bid_dollars, volume_fp}'
{
"ticker": "KXLALIGAGOAL-26AUG30RMAMCF-MCFRENRIQ6-1",
"status": "active",
"yes_bid_dollars": "0.0000",
"volume_fp": "0.00"
}
With mve_filter=exclude, 0 of 100 returned markets were MVE contracts. Without it, 100 of 100 were.
mve_filter fails silently on a typo. Unrecognized values are ignored rather than rejected — mve_filter=exclude_mve, mve_filter=none, and mve_filter=bogus all returned 200 with a full page of MVE contracts, exactly as if the parameter had been omitted. There is no 400 to tell you the filter didn’t apply. Assert on the result instead of trusting the request:
markets = response.json()["markets"]
assert not any(m["ticker"].startswith("KXMVE") for m in markets), "mve_filter did not apply"
Filtering by series_ticker also returns ordinary markets, and is the better choice when you already know what you want:
curl -s "https://api.elections.kalshi.com/trade-api/v2/markets?status=open&limit=5&series_ticker=KXBTCD" \
| jq '.markets[0] | {ticker, status, yes_bid_dollars, yes_ask_dollars, volume_fp}'
{
"ticker": "KXBTCD-26AUG2815-T88799.99",
"status": "active",
"yes_bid_dollars": "0.0000",
"yes_ask_dollars": "0.0100",
"volume_fp": "0.00"
}
Note that the status query filter value (open) is not the same vocabulary as the status field on each market, which reads active. Filtering client-side on market["status"] == "open" silently returns nothing.
response = client.get("/markets", params={
"limit": 20,
"status": "open",
"series_ticker": "KXBTCD",
})
for market in response.json()["markets"]:
print(f"{market['ticker']}: {market['title']}")
print(f" Yes bid: {market.get('yes_bid_dollars', 'N/A')}")
print(f" Volume: {market.get('volume_fp', '0')}")
Key response fields:
ticker— Unique market identifier (e.g.,KXNFLGAME-25OCT12CLEPIT)title— Human-readable descriptionstatus— Market state as returned on the object;activefor open marketsyes_bid_dollars/no_bid_dollars— Best bid prices as dollar strings (e.g.,"0.6500")yes_ask_dollars/no_ask_dollars— Best ask prices as dollar stringslast_price_dollars/previous_price_dollars— Last and prior trade price as dollar stringsyes_bid_size_fp/yes_ask_size_fp— Total contract size at best bid/askvolume_fp/volume_24h_fp— Contracts traded, all-time and trailing 24h, as fixed-point stringsopen_interest_fp— Outstanding contracts, as a fixed-point stringliquidity_dollars— Resting liquidity as a dollar stringexpiration_time— When the market expiresprice_level_structure— Pricing tier (linear_cent,tapered_deci_cent,deci_cent, pluscenter_deci_edge_centi_cent— $0.0001 ticks below $0.01 and above $0.99 — andcenter_centi_edge_centi_centfor combo markets)mve_collection_ticker/mve_selected_legs— Present only on multivariate contracts; the parent collection and the underlying legs
Every price and size field is a string, not a number, and the bare integer-cent fields are gone. Confirmed absent from the live schema on 2026-08-30 (on both ordinary and MVE markets): yes_bid, yes_ask, last_price, volume, volume_24h, open_interest, and liquidity. Code carried over from the cent-based schema — market["yes_bid"] / 100 — now raises KeyError. Read yes_bid_dollars and parse it as a decimal:
from decimal import Decimal
yes_bid = Decimal(market["yes_bid_dollars"]) # "0.6500" -> Decimal('0.6500')
volume = Decimal(market["volume_fp"]) # "13.00" -> Decimal('13.00')
Note: several other fields were removed from the market schema in 2026 — tick_size (May), and fractional_trading_enabled, response_price_units, and resting_orders_count (July). Fractional order sizes are always accepted; there is no per-market flag to check anymore.
GET /markets/{ticker} — Get Single Market
response = client.get("/markets/KXNFLGAME-25OCT12CLEPIT")
market = response.json()["market"]
GET /markets/{ticker}/orderbook — Get Order Book
response = client.get("/markets/KXNFLGAME-25OCT12CLEPIT/orderbook")
book = response.json()["orderbook_fp"]
# Each entry is [price_dollars, count_fp] — both strings
print("YES bids:", book.get("yes_dollars", []))
print("NO bids:", book.get("no_dollars", []))
# Example: [["0.4200", "13.00"], ["0.3500", "11.00"]]
# Sorted ascending — last element is the best (highest) bid
The order book returns bids only — no explicit asks. In binary markets, a YES bid at $X implies a NO ask at $(1.00 - X) and vice versa. Arrays are sorted ascending; the last element is the best bid. Both price and count values are strings to support subpenny pricing and fractional contracts.
To fetch multiple order books in one request, use the batch endpoint GET /markets/orderbooks.
GET /events — List Events
Events group related markets together (e.g., all NFL Week 10 games).
response = client.get("/events", params={"status": "open"})
events = response.json()["events"]
GET /series — List Series
Series are higher-level groupings (e.g., “NFL Games”, “Fed Rate Decisions”).
response = client.get("/series")
Trading (Authenticated)
Breaking change (June 2026): The legacy order-mutation endpoints (
POST /portfolio/orderswithside: yes/no,action: buy/sell,count_fp,yes_price_dollars) were deprecated and removed June 18–25, 2026. The canonical order endpoint is now CreateOrder V2 atPOST /portfolio/events/orders. Before their June 18–25 shutdown, legacy mutation endpoints were billed at 10x the V2 token cost (raised from 5x on June 4, 2026). See the CreateOrder V2 reference and the changelog.
POST /portfolio/events/orders — Place an Order (V2)
import uuid
order = {
"ticker": "KXNFLGAME-25OCT12CLEPIT",
"side": "bid", # "bid" or "ask"
"count": "10.00", # Contract quantity (fixed-point string)
"price": "0.65", # Fixed-point string, up to 6 decimals
"time_in_force": "good_till_canceled", # REQUIRED
"self_trade_prevention_type": "taker_at_cross", # REQUIRED
"client_order_id": str(uuid.uuid4()) # Idempotent tracking ID
}
response = client.post("/portfolio/events/orders", data=order)
print(response.json())
Order parameters:
ticker— The market ticker (required)side—bidorask(required; replaces the old yes/no + buy/sell combination)count— Contract quantity as a fixed-point string (e.g.,"10.00") (required)price— Limit price as a fixed-point string with up to 6 decimal places (e.g.,"0.65")time_in_force— Required:fill_or_kill,good_till_canceled, orimmediate_or_cancelself_trade_prevention_type— Required:taker_at_crossormakerclient_order_id— Optional idempotency key (recommended for deduplication)expiration_time— Optional expiration timestamp in secondssubaccount— Optional: route the order to a subaccountorder_group_id— Optional: attach the order to an order groupexchange_index— Optional: target a specific exchange shardcancel_order_on_pause— Optional: auto-cancel if the market pauses
Note: all orders are limit orders — there is no separate market-order type.
GET /portfolio/orders — List Your Orders
response = client.get("/portfolio/orders", params={
"status": "resting" # resting, canceled, executed
})
Canceling, Amending, and Decreasing Orders (V2)
The legacy cancel (DELETE /portfolio/orders/{order_id}), amend, and decrease endpoints have V2 equivalents under the /portfolio/events/orders family, and batch order creation lives at POST /portfolio/events/orders/batched. See the orders section of the API reference for the exact V2 paths and payloads.
Account & Positions
GET /portfolio/positions — Get Your Positions
response = client.get("/portfolio/positions")
positions = response.json()["positions"]
for pos in positions:
print(f"{pos['ticker']}: {pos['position']} contracts")
print(f" Market exposure: ${pos.get('market_exposure', 0) / 100:.2f}")
GET /portfolio/balance — Check Your Balance
response = client.get("/portfolio/balance")
balance = response.json()
# Preferred: the balance_dollars dollar-string field (added May 2026)
print(f"Available: ${balance['balance_dollars']}")
# The legacy integer-cent field still works:
print(f"Available: ${balance['balance'] / 100:.2f}")
Per the changelog, balance reads are scoped by exchange_index as of August 2026 — if you trade on more than one exchange shard (e.g., the Perps exchange), query the balance for each index.
GET /portfolio/settlements — Settlement History
response = client.get("/portfolio/settlements")
Historical Data
Kalshi partitions data into live and historical tiers. Live endpoints return current and recent data; older data must be queried through dedicated historical endpoints.
GET /historical/cutoff — Get Cutoff Timestamps
response = client.get("/historical/cutoff")
cutoffs = response.json()
# Returns: market_settled_ts, trades_created_ts, orders_updated_ts
Historical Endpoints
| Endpoint | Description |
|---|---|
GET /historical/markets | Settled markets older than the cutoff |
GET /historical/markets/{ticker} | Single historical market by ticker |
GET /historical/markets/{ticker}/candlesticks | Candlestick data for historical markets |
GET /historical/trades | All trades older than the cutoff |
GET /historical/fills | User-scoped fills older than the cutoff |
GET /historical/orders | Canceled/executed orders older than the cutoff |
GET /historical/positions | Historical position records (added July 2026) |
# Example: fetch historical trades
response = client.get("/historical/trades", params={
"ticker": "KXNFLGAME-25OCT12CLEPIT",
"limit": 100
})
The live data window is approximately 3 months. Historical endpoints support the same cursor-based pagination as their live counterparts.
WebSocket API
For real-time data, Kalshi’s WebSocket API streams order book updates, trade executions, and market status changes.
Connection with Authentication
import asyncio
import base64
import json
import time
import websockets
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
KEY_ID = "your-api-key-id"
PRIVATE_KEY_PATH = "path/to/private-key.pem"
WS_URL = "wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2" # Demo
def load_private_key():
with open(PRIVATE_KEY_PATH, "rb") as f:
return serialization.load_pem_private_key(f.read(), password=None)
def sign_ws_message(private_key, timestamp_ms: str) -> str:
message = f"{timestamp_ms}GET/trade-api/ws/v2".encode("utf-8")
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH
),
hashes.SHA256()
)
return base64.b64encode(signature).decode("utf-8")
async def stream_kalshi():
private_key = load_private_key()
timestamp = str(int(time.time() * 1000))
signature = sign_ws_message(private_key, timestamp)
headers = {
"KALSHI-ACCESS-KEY": KEY_ID,
"KALSHI-ACCESS-SIGNATURE": signature,
"KALSHI-ACCESS-TIMESTAMP": timestamp
}
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
print("Connected to Kalshi WebSocket")
# Subscribe to ticker updates
await ws.send(json.dumps({
"id": 1,
"cmd": "subscribe",
"params": {
"channels": ["ticker"]
}
}))
# Subscribe to orderbook for a specific market
await ws.send(json.dumps({
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["orderbook_delta"],
"market_tickers": ["KXNFLGAME-25OCT12CLEPIT"]
}
}))
async for message in ws:
data = json.loads(message)
print(f"Update: {data}")
asyncio.run(stream_kalshi())
Available Channels
Note: The WebSocket connection itself requires authentication — even for public data channels. Auth headers must be included during the handshake.
Private channels:
fill— Your trade executionsuser_orders— Real-time order updates (created, updated, canceled, executed)market_positions— Your position updatesorder_group_updates— Order group status changes
Public channels:
orderbook_delta— Real-time order book changes for specific marketsticker— Market-wide ticker updates for all marketstrade— Public trade feedmarket_lifecycle_v2— Market open/close/settlement events (excludes KXMVE-prefixed tickers)multivariate_market_lifecycle— Lifecycle events for multivariate event (MVE) markets
Subscription Management
You can dynamically add or remove markets from an existing subscription without re-subscribing. A get_snapshot command is also available to request a fresh orderbook snapshot on demand — useful after detecting a sequence gap.
# Add markets to an existing subscription
await ws.send(json.dumps({
"id": 3,
"cmd": "update_subscription",
"params": {
"sids": [1],
"market_tickers": ["NEW-MARKET-1", "NEW-MARKET-2"],
"action": "add_markets"
}
}))
Keepalive
The Python websockets library handles keepalive automatically. Other WebSocket libraries may require manual ping/pong implementation.
FIX Protocol
For institutional-grade connectivity, Kalshi supports the FIX (Financial Information eXchange) protocol — the same protocol used by major stock and futures exchanges. Kalshi’s gateway uses FIXT.1.1 transport with application version FIX50SP2. This is worth stating plainly because a large amount of third-party documentation still describes Kalshi as a “FIX 4.4” venue; it is not, and a FIX 4.4-only engine will not negotiate a session.
Connection parameters (Kalshi FIX connectivity docs, verified August 21, 2026):
| Parameter | Value |
|---|---|
| Session | FIXT.1.1 transport, FIX50SP2 application version |
| SenderCompID | Your FIX API key (UUID format) |
| Session ID | TargetCompID + SenderCompID |
| Order entry host (prod) | mm.fix.elections.kalshi.com |
| Market data host (prod) | marketdata.fix.elections.kalshi.com |
| Order entry host (demo) | fix.demo.kalshi.co |
| Market data host (demo) | marketdata.fix.demo.kalshi.co |
| Transport security | TLS 1.2+ required — plain TCP is rejected |
| Connections | One FIX connection per API key |
Ports and TargetCompIDs:
| Purpose | Port | TargetCompID |
|---|---|---|
| Order entry, no retransmission | 8228 | KalshiNR |
| Order entry, with retransmission | 8230 | KalshiRT |
| Drop copy (historical execution reports) | 8229 | KalshiDC |
| Post trade (settlement, position resolution) | 8231 | KalshiPT |
| RFQ (market maker quotes) | 8232 | KalshiRFQ |
| Market data (book snapshots + increments) | 8233 | KalshiMD |
If your FIX engine cannot do native TLS, front it with a local proxy such as stunnel. To pin the server certificate:
openssl s_client -showcerts -connect fix.demo.kalshi.co:8228 < /dev/null | openssl x509 > kalshi-fix.pem
FIX is relevant if you:
- Have existing FIX trading infrastructure
- Need the lowest possible latency
- Are building for an institutional trading desk
FIX does not raise your throughput ceiling. Application messages use the same token model, token costs, and Read/Write buckets as the equivalent REST operations — order-entry and RFQ messages bill the Write bucket. Logout (35=5), Heartbeat (35=0), and TestRequest (35=1) are excluded; Logon (35=A) is rate-limited. Mass Cancel Request (35=q) is capped at 1 request/second. Order-entry messages carrying ExDestination (tag 100) with a value >= 1 bill a per-shard Write budget.
Two operational details worth building for: sessions may drop during the scheduled maintenance window, and Kalshi does not initiate sequence-number resets — reset on your side when reconnecting. KalshiRT sessions retain message continuity across maintenance and support retransmission of missed messages. Set tag 21006 (CancelOrderOnPause) to Y on New Order Single (35=D) if you want resting orders pulled automatically when a trading pause begins; the default N leaves them on the book.
Private connectivity is available: Premier tier and above can request an AWS PrivateLink endpoint, and Prime tier and above can discuss VPC peering. Contact [email protected] for FIX access and connection details.
Rate Limits
Kalshi replaced its old per-request tiers with a token-cost system in April 2026. Each tier gets separate read and write token budgets per second, and each endpoint consumes a published number of tokens per call (rate limits docs):
| Tier | Read tokens/s | Write tokens/s |
|---|---|---|
| Basic | 200 | 100 |
| Advanced | 300 | 300 |
| Expert | 600 | 600 |
| Premier | 1,000 | 1,000 |
| Paragon | 2,000 | 2,000 |
| Prime | 4,000 | 4,000 |
| Prestige | 10,000 | 8,000 |
How costs work:
- A single order costs 10 write tokens; a batch of N orders costs N × 10
- Before their June 18–25 shutdown, legacy (pre-V2) mutation endpoints were billed at 10x the V2 token cost (raised from 5x on June 4, 2026)
- Advanced is self-serve: upgrade via
POST /account/api_usage_level/upgrade(added June 2026) - Higher tiers are earned automatically from trailing 30-day trading volume (thresholds range from 0.075% to 1.00% of exchange volume; requirements were halved June 25, 2026) and are granted for 30 days at a time
- Write buckets above Basic hold two seconds of budget, so an idle bot can burst up to 2x its per-second budget when the market moves
Key guidelines:
- WebSocket feeds are better suited for latency-sensitive strategies than REST polling
- Cache market metadata (tickers, descriptions) — it rarely changes
- Query your current limits via
GET /account/limits(andGET /account/limits/perpsfor the Perps API) - Query per-endpoint token costs via
GET /account/endpoint_costsrather than hardcoding them
What Else Changed in 2026
A few platform-level additions worth knowing about (changelog):
- Predictions vs. Perps API split: Kalshi now runs a separate Perps API for perpetual futures with margin, alongside the classic prediction market API (docs.kalshi.com/welcome)
- Subaccounts and scoped API keys: API keys can be restricted with granular permissions such as
write::trade(June 2026) andwrite::transfer(June 2026), and orders can be routed to subaccounts - Multi-exchange sharding: an
exchange_indexparameter appears across order placement, balance reads, and other endpoints to target a specific exchange shard - Batch orderbook reads:
GET /markets/orderbooksfetches multiple books in one call - Historical positions:
GET /historical/positions(July 2026) rounds out the historical data tier - WebSocket
get_snapshot: request a fresh orderbook snapshot on an existing subscription instead of tearing it down - Cancel-all-orders endpoints (August 2026): new endpoints cancel every resting order across all subaccounts or one subaccount in a single call — and as of September 2026 a cancel-all costs the same write tokens as cancelling one order. This is the primitive your kill switch should call.
- Exchange auto-routing by default (August 2026): orders that pass
market_tickerwithoutexchange_indexare now auto-routed to the right shard. Note the rate-limit interaction: auto-routed writes bill every shard’s Write bucket. - Volume progress endpoint:
GET /account/api_usage_level/volume_progressreports your trailing 30-day volume against each tier’s earn/keep thresholds — poll it instead of reverse-engineering your tier. exchange_indexon WebSocket order updates (August 2026): theuser_orderschannel now tags each update with its shard.
Putting It Together: A Complete Trading Bot
Here’s a minimal but functional bot that monitors a market and places orders based on a simple threshold strategy:
import time
import uuid
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("kalshi-bot")
class SimpleKalshiBot:
def __init__(self, client: KalshiClient, ticker: str):
self.client = client
self.ticker = ticker
self.position = 0
self.max_position = 50 # Max contracts to hold
def get_market_price(self) -> dict:
"""Fetch current market prices"""
response = self.client.get(f"/markets/{self.ticker}")
market = response.json()["market"]
return {
"yes_bid": market.get("yes_bid_dollars"),
"yes_ask": market.get("yes_ask_dollars"),
"no_bid": market.get("no_bid_dollars"),
"no_ask": market.get("no_ask_dollars"),
"volume": market.get("volume", 0)
}
def check_position(self):
"""Get current position in this market"""
response = self.client.get("/portfolio/positions", params={
"ticker": self.ticker
})
positions = response.json().get("positions", [])
for pos in positions:
if pos["ticker"] == self.ticker:
self.position = pos.get("position", 0)
return
self.position = 0
def place_order(self, side: str, count: int, price: str):
"""Place a limit order via CreateOrder V2. side is 'bid' or 'ask'."""
order = {
"ticker": self.ticker,
"side": side,
"count": f"{count}.00",
"price": price,
"time_in_force": "good_till_canceled",
"self_trade_prevention_type": "taker_at_cross",
"client_order_id": str(uuid.uuid4())
}
response = self.client.post("/portfolio/events/orders", data=order)
if response.status_code in (200, 201):
logger.info(f"Order placed: {side} {count}x @ ${price}")
else:
logger.error(f"Order failed: {response.text}")
return response
def run(self, buy_threshold: str, sell_threshold: str, interval: int = 10):
"""
Simple threshold strategy:
- Buy YES if price drops below buy_threshold
- Sell YES if price rises above sell_threshold
Thresholds are dollar strings (e.g., "0.40", "0.60")
"""
from decimal import Decimal
buy_t = Decimal(buy_threshold)
sell_t = Decimal(sell_threshold)
logger.info(f"Starting bot for {self.ticker}")
logger.info(f"Buy below: ${buy_threshold} | Sell above: ${sell_threshold}")
while True:
try:
prices = self.get_market_price()
self.check_position()
yes_ask = prices["yes_ask"]
yes_bid = prices["yes_bid"]
logger.info(
f"Price: bid=${yes_bid} ask=${yes_ask} | "
f"Position: {self.position} contracts"
)
# Buy signal — bid for YES contracts
if (yes_ask and Decimal(yes_ask) <= buy_t
and self.position < self.max_position):
contracts = min(5, self.max_position - self.position)
self.place_order("bid", contracts, yes_ask)
# Sell signal — offer out our YES contracts
if (yes_bid and Decimal(yes_bid) >= sell_t
and self.position > 0):
contracts = min(5, self.position)
self.place_order("ask", contracts, yes_bid)
except Exception as e:
logger.error(f"Error: {e}")
time.sleep(interval)
# Usage:
client = KalshiClient(
key_id="your-key-id",
private_key_path="path/to/key.pem",
base_url="https://external-api.demo.kalshi.co/trade-api/v2" # START WITH DEMO
)
bot = SimpleKalshiBot(client, ticker="KXNFLGAME-25OCT12CLEPIT")
bot.run(buy_threshold="0.40", sell_threshold="0.60", interval=10)
Common Patterns for Agent Builders
Pattern 1: Multi-Market Scanner
Scan all open markets for opportunities matching your criteria.
from decimal import Decimal
def scan_markets(client, min_volume=1000, max_yes_price="0.30"):
"""Find underpriced YES contracts with decent volume"""
max_price = Decimal(max_yes_price)
response = client.get("/markets", params={
"status": "open",
"limit": 200
})
opportunities = []
for market in response.json()["markets"]:
vol = market.get("volume", 0)
yes_ask = market.get("yes_ask_dollars")
if vol >= min_volume and yes_ask and Decimal(yes_ask) <= max_price:
opportunities.append({
"ticker": market["ticker"],
"title": market["title"],
"yes_ask_dollars": yes_ask,
"volume": vol
})
return sorted(opportunities, key=lambda x: x["volume"], reverse=True)
Pattern 2: Cross-Platform Arbitrage (Kalshi ↔ Polymarket)
Compare prices on the same event across both platforms.
from decimal import Decimal
def find_cross_platform_arbs(kalshi_client, polymarket_markets):
"""
Compare Kalshi and Polymarket prices for matching events.
Requires a mapping of Kalshi tickers to Polymarket token IDs.
"""
kalshi_response = kalshi_client.get("/markets", params={
"status": "open", "limit": 200
})
for kalshi_market in kalshi_response.json()["markets"]:
ticker = kalshi_market["ticker"]
# Check if we have a Polymarket match
if ticker in polymarket_markets:
kalshi_yes = Decimal(kalshi_market.get("yes_ask_dollars", "1.00"))
poly_no = Decimal(str(polymarket_markets[ticker]["no_price"]))
combined = kalshi_yes + poly_no
if combined < Decimal("0.98"): # 2%+ arb
print(f"ARB: {ticker}")
print(f" Kalshi YES: ${kalshi_yes}")
print(f" Poly NO: ${poly_no}")
print(f" Combined: ${combined}")
print(f" Edge: ${Decimal('1.00') - combined}")
Pattern 3: News-Driven Trading
Get started in 1 click: Deploy the Kalshi News Bot — a ready-to-run Claude-powered trading agent for Kalshi’s demo environment.
React to news events by monitoring external sources and executing on Kalshi.
import time
def news_driven_strategy(client, ticker, news_monitor):
"""
When positive news hits, buy YES.
When negative news hits, buy NO.
"""
while True:
signal = news_monitor.get_latest_signal(ticker)
if signal and signal["confidence"] > 0.7:
market = client.get(f"/markets/{ticker}").json()["market"]
# Positive news: bid at the YES ask to buy YES exposure.
# Negative news: hit the YES bid (ask side) to take NO exposure.
if signal["direction"] == "positive":
side, price = "bid", market.get("yes_ask_dollars")
else:
side, price = "ask", market.get("yes_bid_dollars")
if price:
client.post("/portfolio/events/orders", data={
"ticker": ticker,
"side": side,
"count": "5.00",
"price": price,
"time_in_force": "immediate_or_cancel",
"self_trade_prevention_type": "taker_at_cross",
"client_order_id": str(uuid.uuid4())
})
time.sleep(30)
Pattern 4: Portfolio Monitoring Dashboard
Pull all positions and compute aggregate P&L.
def portfolio_summary(client):
"""Get a complete portfolio summary"""
balance = client.get("/portfolio/balance").json()
positions = client.get("/portfolio/positions").json().get("positions", [])
total_exposure = 0
total_unrealized = 0
print(f"Cash Balance: ${balance['balance_dollars']}")
print(f"\nOpen Positions:")
print("-" * 60)
for pos in positions:
exposure = float(pos.get("market_exposure_dollars", "0"))
total_exposure += exposure
print(f" {pos['ticker']}")
print(f" Side: {pos.get('side', 'N/A')}")
print(f" Contracts: {pos.get('position_fp', '0')}")
print(f" Exposure: ${exposure:.2f}")
print("-" * 60)
print(f"Total Exposure: ${total_exposure:.2f}")
print(f"Total Portfolio: ${(balance['balance'] / 100) + total_exposure:.2f}")
Production Deployment Checklist
Before taking your bot from demo to production:
Security:
- Private keys stored in environment variables or a secrets manager (never in code)
- API key permissions scoped to only what’s needed
- Separate keys for demo and production
- No secrets committed to version control
Risk Management:
- Maximum position size limits per market
- Maximum total portfolio exposure limit
- Kill switch that calls the cancel-all-orders endpoint and halts trading
- Daily loss limit with automatic shutdown
- Slippage protection on market orders
Reliability:
- Automatic reconnection for WebSocket disconnections (exponential backoff)
- Graceful error handling for all API calls
- Logging of every order placed and every error encountered
- Health check / heartbeat monitoring
Operational:
- Tested extensively in demo environment
- Start with very small position sizes in production
- Monitor manually for the first 24-48 hours
- Alerts set up for unusual behavior (large losses, connection failures)
- Rate limit awareness built into request logic
Kalshi vs. Polymarket: Choosing Your Platform
| Factor | Kalshi | Polymarket |
|---|---|---|
| Regulation | CFTC-regulated (legal for U.S. institutions) | Unregulated (crypto-native, geo-restricted) |
| Settlement | USD via traditional banking | USDC on Polygon blockchain |
| Authentication | RSA-PSS signed requests | EIP-712 wallet signatures |
| Demo Environment | Yes — full sandbox | No demo available |
| Trading Fees | Per published fee schedule (taker fees apply) | Variable (taker fees by market type, maker rebates) |
| Liquidity | Lower overall | Significantly higher |
| Market Categories | Regulated subset (politics, economics, sports) | Broader (crypto, culture, anything) |
| API Protocols | REST, WebSocket, FIX | REST, WebSocket |
| Best For | Regulated agents, institutional use, safe testing | High-volume trading, crypto markets, maximum liquidity |
For many agent builders, the optimal approach is to start on Kalshi’s demo environment to test your logic safely, then deploy to both platforms to maximize opportunity and diversification.
Beyond prediction markets: Many agents don’t stop at Polymarket and Kalshi. Traditional offshore sportsbooks offer sports odds that overlap with prediction market events — often with different pricing and significantly higher limits. For a full comparison of how these worlds differ in odds quality, API access, and automation potential, see Offshore vs. Regulated Sportsbooks.
See our Polymarket API Guide for the complete Polymarket reference.
Kalshi vs. Other Prediction Market APIs
For builders comparing across the full field, here is how Kalshi’s API stacks up against Polymarket and OG.com at a glance:
| Factor | Kalshi | Polymarket | OG.com |
|---|---|---|---|
| Regulation | CFTC-regulated DCM (US) | Crypto-native, geo-restricted | Offshore, crypto-native |
| Authentication | RSA-PSS signed requests | EIP-712 wallet signatures | API key |
| Settlement | USD via banking | USDC on Polygon | Crypto |
| Demo sandbox | Yes — full production mirror | No | No |
| Protocols | REST v2, WebSocket, FIX | REST, WebSocket | REST |
See the OG.com prediction market guide for the OG.com side of that comparison.
Troubleshooting
For an in-depth breakdown of the 10 most common integration failures — including RSA signing edge cases, order book drift, the fixed-point migration, and fractional contract handling — see the top 10 Kalshi API problems guide.
Authentication errors (401/403):
- Verify your timestamp is in milliseconds (not seconds)
- Check that you’re signing the correct string:
timestamp + method + path - Ensure your private key file is the correct PEM format
- Remember: demo and production use separate keys
“Order rejected” errors:
- If you’re posting to
POST /portfolio/orderswithside: yes/noandaction: buy/sell, your code targets the legacy endpoint disabled in June 2026 — Kalshi now returns “Please switch to the V2 endpoints”. Migrate toPOST /portfolio/events/orders(V2) withside: bid/askand the requiredtime_in_forceandself_trade_prevention_typefields - Check market status — it may be closed or settled
- Verify your price is within the valid range and tick size for the market’s
price_level_structure - Ensure you have sufficient balance
- Check that you haven’t exceeded position limits
Rate limit errors (429):
- Reduce polling frequency
- Switch to WebSocket for real-time data
- Cache market metadata
- Implement exponential backoff on retries
WebSocket disconnections:
- Implement automatic reconnection with exponential backoff
- Re-subscribe to channels after reconnecting
- The
websocketsPython library handles keepalive automatically — other libraries may need manual ping/pong
Demo vs Production discrepancies:
- Some market data in demo may be synthetic or delayed
- Demo order execution may differ slightly from production
- Always re-verify behavior with small production orders before scaling
Official Resources
- Kalshi API Documentation
- Kalshi SDK Overview — Official SDK docs
- Kalshi OpenAPI Spec — Import into Postman, Apidog, etc.
- Kalshi AsyncAPI Spec — WebSocket schema
- kalshi_python_sync (PyPI) — Official synchronous Python SDK
- kalshi_python_async (PyPI) — Official async Python SDK
- kalshi-typescript (npm) — Official TypeScript SDK
- Kalshi Changelog
- Kalshi Academy — Educational resources
- Kalshi Trading Console
Community SDKs and Tools
The official SDKs above receive weekly releases and are the recommended starting point. The old kalshi-python PyPI package is deprecated — do not use it for new projects. Community clients like AndrewNolte/KalshiPythonClient (last pushed 2022) and sswadkar/kalshi-interface are unmaintained and predate the 2026 API changes; the community kalshi-rust crate exists but verify it against the current API before relying on it.
Where This Fits in the Agent Betting Stack
This guide covers Layer 3 (Trading) of the Agent Betting Stack. Kalshi’s API is one execution venue for converting intelligence into positions — and for many U.S.-based agents, it’s the only fully legal one.
To build a complete autonomous agent, you also need:
- Layer 1 — Identity: Register and verify your agent (Moltbook Identity Guide)
- Layer 2 — Wallet: Hold and manage funds (Wallet Comparison Guide)
- Layer 4 — Intelligence: Agent Intelligence Guide — LLM analysis, sentiment, and strategy
- Cross-Market: Offshore Sportsbook API Guide — Extend your agent to traditional sportsbooks
This guide is maintained by AgentBets.ai. Found an error or API change we missed? Let us know on Twitter.
Not financial advice. Built for builders.
