Kalshi API integration breaks in predictable ways. After reviewing current documentation, developer forums, and production bot failures, these are the 10 problems that cause the most wasted hours — with working code fixes for each.

1. Bad Request Signatures

The single most common hard failure is signing the wrong string. Kalshi requires timestamp + HTTP_METHOD + path, signed with RSA-PSS and SHA-256. The critical detail: the path must exclude query parameters. This rule applies to both REST endpoints and the WebSocket handshake path (/trade-api/ws/v2).

import base64, time
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

def sign_request(private_key, method: str, path: str):
    ts = str(int(time.time() * 1000))
    path = path.split("?", 1)[0]   # strip query params — critical for Kalshi
    msg = f"{ts}{method.upper()}{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-TIMESTAMP": ts,
        "KALSHI-ACCESS-SIGNATURE": base64.b64encode(sig).decode(),
    }

Debugging tip: log the exact method, path_without_query, and millisecond timestamp you signed. That makes 401 debugging deterministic instead of guesswork. If you are coming from Polymarket’s HMAC-based signing (covered in our Polymarket API guide), note that Kalshi uses RSA-PSS — a completely different cryptographic primitive.

2. Mixing Demo and Production Credentials

Demo and production are separate environments with separate credentials. Kalshi does not share keys between them. The base URLs differ and mixing them produces silent auth failures, not helpful error messages.

EnvironmentREST Base URLWebSocket URL
Demohttps://demo-api.kalshi.co/trade-api/v2wss://demo-api.kalshi.co/trade-api/ws/v2
Productionhttps://api.elections.kalshi.com/trade-api/v2wss://api.elections.kalshi.com/trade-api/ws/v2
from dataclasses import dataclass

@dataclass(frozen=True)
class KalshiEnv:
    rest: str
    ws: str
    key_id: str
    key_path: str

DEMO = KalshiEnv(
    rest="https://demo-api.kalshi.co/trade-api/v2",
    ws="wss://demo-api.kalshi.co/trade-api/ws/v2",
    key_id="demo_key",
    key_path="demo.key",
)

PROD = KalshiEnv(
    rest="https://api.elections.kalshi.com/trade-api/v2",
    ws="wss://api.elections.kalshi.com/trade-api/ws/v2",
    key_id="prod_key",
    key_path="prod.key",
)

Fix: never keep a single API_KEY_ID environment variable. Use KALSHI_DEMO_* and KALSHI_PROD_* prefixes so you cannot accidentally sign production requests with demo keys. This same environment-isolation pattern applies across prediction market APIs — see the prediction market API reference for a cross-platform comparison.

3. Assuming WebSocket Channels Don’t Need Auth

Developers familiar with other exchange APIs often assume public market-data channels like ticker or trade can be consumed without authentication. On Kalshi, this is wrong. The connection itself is authenticated — even when you only subscribe to public channels.

Additional WebSocket traps that catch developers:

  • Kalshi sends ping frames every 10 seconds and expects pong responses. Many DIY WebSocket wrappers do not handle this automatically.
  • The orderbook_delta channel requires market tickers, not market_id or market_ids.
  • Reconnect logic with exponential backoff is not optional — it is a documented requirement.
import asyncio, json, websockets

async def run_ws(ws_url, headers, tickers):
    backoff = 1
    while True:
        try:
            async with websockets.connect(ws_url, additional_headers=headers) as ws:
                await ws.send(json.dumps({
                    "id": 1,
                    "cmd": "subscribe",
                    "params": {
                        "channels": ["orderbook_delta"],
                        "market_tickers": tickers,   # not market_ids
                    }
                }))
                backoff = 1
                async for raw in ws:
                    msg = json.loads(raw)
                    yield msg
        except Exception:
            await asyncio.sleep(backoff)
            backoff = min(backoff * 2, 30)

4. Letting the Local Order Book Drift

Kalshi’s orderbook stream sends an initial orderbook_snapshot, then incremental orderbook_delta messages. Every message includes sid (stream ID) and seq (sequence number). If your client misses a message — network hiccup, slow processing, GC pause — the local book silently diverges from reality.

The fix is simple in concept and routinely skipped in practice: track seq per sid and re-subscribe when you detect a gap.

from decimal import Decimal
from collections import defaultdict

books = {}
last_seq = {}

def apply_snapshot(msg):
    books[msg["market_ticker"]] = {
        "yes": {Decimal(p): Decimal(q) for p, q in msg["yes_dollars_fp"]},
        "no":  {Decimal(p): Decimal(q) for p, q in msg["no_dollars_fp"]},
    }

def apply_delta(msg):
    side = msg["side"]
    px = Decimal(msg["price_dollars"])
    delta = Decimal(msg["delta_fp"])
    book = books[msg["market_ticker"]][side]
    book[px] = book.get(px, Decimal("0")) + delta
    if book[px] <= 0:
        book.pop(px, None)

def process_ws(data):
    sid, seq = data["sid"], data["seq"]
    prev = last_seq.get(sid)
    if prev is not None and seq != prev + 1:
        raise RuntimeError(f"Sequence gap on sid={sid}: {prev} -> {seq}")
    last_seq[sid] = seq
    if data["type"] == "orderbook_snapshot":
        apply_snapshot(data["msg"])
    elif data["type"] == "orderbook_delta":
        apply_delta(data["msg"])

Validation strategy: persist a checksum of your top-of-book state after every N deltas. That makes silent drift visible before it corrupts trading logic. For agents that trade across both Kalshi and Polymarket, maintaining synchronized books is the foundation of cross-market arbitrage.

5. Misreading the Bids-Only Order Book

This one trips up every developer coming from traditional exchange APIs. Kalshi’s REST orderbook endpoint returns bids only: yes_dollars and no_dollars. There are no explicit asks.

Binary markets are reciprocal: a YES bid at price X implies a NO ask at 1.00 - X, and vice versa. The arrays are sorted ascending, so the best bid is the last element — another inversion from the typical descending-bid convention.

from decimal import Decimal

def best_yes_bid_ask(orderbook_fp):
    yes = orderbook_fp.get("yes_dollars", [])
    no  = orderbook_fp.get("no_dollars", [])
    best_yes_bid = Decimal(yes[-1][0]) if yes else None
    best_yes_ask = Decimal("1.00") - Decimal(no[-1][0]) if no else None
    return best_yes_bid, best_yes_ask

Normalize early: convert everything internally to one view (YES bid / YES ask) before sending data to models, dashboards, or the Kelly criterion sizing layer. That eliminates an entire class of side-flip bugs.

6. Missing Data from Pagination and the Live/Historical Split

Two data-access patterns break simultaneously if you aren’t prepared for them.

Cursor-based pagination. List endpoints return a cursor field. If you only process the first page, you silently miss data — positions, fills, historical trades, market listings.

Live vs. historical endpoint split. Kalshi now separates data into live and historical tiers. Older records must be queried through /historical/... endpoints, with cutoff dates available from GET /historical/cutoff. As of the March 2026 migration, historical data has been removed from live endpoints. Build as if the split is mandatory.

import requests

def fetch_all(url, params=None):
    params = dict(params or {})
    out = []
    while True:
        r = requests.get(url, params=params, timeout=20)
        r.raise_for_status()
        data = r.json()
        items = next(v for v in data.values() if isinstance(v, list))
        out.extend(items)
        cursor = data.get("cursor")
        if not cursor:
            return out
        params["cursor"] = cursor

For backtests or account-history rebuilds: query both live and historical endpoints, merge by stable IDs, then dedupe. The prediction market trading layer guide covers how agents should architect data pipelines that account for these kinds of upstream API splits.

7. Using Legacy Integer Price Fields

This was the biggest schema-risk item in the Kalshi ecosystem — and the migration is now complete. As of March 2026, legacy integer fields (prices in cents, counts as integers) have been removed from all REST and WebSocket response payloads. Prices use *_dollars fixed-point strings and counts use *_fp strings exclusively.

All code must use _dollars and _fp as the only interface. If you’re still referencing yes_price, no_price, count, or other integer fields, your integration is broken.

from decimal import Decimal

def parse_price(x: str) -> Decimal:
    return Decimal(x)   # never float()

def parse_count_fp(x: str) -> Decimal:
    return Decimal(x)

# correct usage
yes_px = parse_price(trade["yes_price_dollars"])
qty = parse_count_fp(trade["count_fp"])

In Python, set Decimal everywhere at the API boundary. In TypeScript, use a decimal library like decimal.js or store fixed-point strings until the latest possible moment. Using native float or JavaScript Number for financial calculations is a guaranteed source of reconciliation bugs.

8. Not Handling Fractional Contracts and Fee Rounding

Markets on Kalshi can now be fractional on a per-market basis via the fractional_trading_enabled flag. The _fp values can contain 0–2 decimal places on input. This interacts with fee rounding in ways that break naive P&L tracking.

Sub-penny prices and fractional sizes create sub-cent balance changes. Kalshi resolves these with a rounding fee and a fee accumulator/rebate mechanism. If you reconcile fills by multiplying price × quantity and comparing to your balance delta, the numbers will not match — and the exchange is behaving correctly.

from decimal import Decimal

def fp_to_hundredths(count_fp: str) -> int:
    """Convert '1.55' -> 155 for internal integer arithmetic."""
    return int((Decimal(count_fp) * 100).to_integral_value())

def supports_fractional(market: dict) -> bool:
    return bool(market.get("fractional_trading_enabled"))

Internal representation: store "1.55" as integer 155 (contract hundredths) and "0.1200" as a 4-decimal-place Decimal. That preserves precision without floats and makes reconciliation against Kalshi’s accumulator logic possible.

9. Order Entry Bugs from Side/Action Confusion

Kalshi’s order API accepts side (yes/no) and action (buy/sell), plus multiple price and count representations. The combinatorial surface for bugs is large:

  • buy_max_cost automatically makes the order Fill-or-Kill
  • sell_position_floor is deprecated in favor of reduce_only
  • Self-crossing FoK orders can still produce partial fills depending on self_trade_prevention_type
  • client_order_id is essential for idempotent deduplication and appears in orderbook_delta messages when your own order caused the change
import uuid

def build_order(ticker: str):
    return {
        "ticker": ticker,
        "side": "yes",
        "action": "buy",
        "count_fp": "10.00",
        "yes_price_dollars": "0.5600",
        "client_order_id": str(uuid.uuid4()),
        "reduce_only": False,
        "self_trade_prevention_type": "taker_at_cross",
    }

The reconciliation ledger: maintain a durable local map of client_order_id → intended payload → server order_id → fills/WS events. That one data structure saves enormous debugging time during retries and reconnects. For agents managing positions across multiple markets, the agent betting stack architecture places this reconciliation logic in Layer 3 (Trading).

10. Poor Rate Limit and Throughput Management

Kalshi publishes tiered rate limits that vary by account level:

TierRead/secWrite/secQualification
Basic2010Completing signup
Advanced3030Typeform application
Premier1001003.75% of monthly exchange volume + technical review
Prime4004007.5% of monthly exchange volume + technical review

Additional constraints that developers miss: batch order creates are capped at 20 per request and each item counts against write limits. Batch cancels are special — each cancel counts at 0.2 against the write limit. In demo, batch endpoints are opened to Basic users, but production enforces tier-appropriate limits.

import time
import threading

class TokenBucket:
    def __init__(self, rate_per_sec: int):
        self.rate = rate_per_sec
        self.tokens = rate_per_sec
        self.last = time.time()
        self.lock = threading.Lock()

    def take(self, n=1):
        while True:
            with self.lock:
                now = time.time()
                self.tokens = min(
                    self.rate,
                    self.tokens + (now - self.last) * self.rate,
                )
                self.last = now
                if self.tokens >= n:
                    self.tokens -= n
                    return
            time.sleep(0.01)

Don’t hardcode tier assumptions. Load GET /account/limits at startup and configure separate read/write token buckets dynamically. Kalshi also exposes queue-position endpoints so you can measure why resting orders are not filling instead of guessing. The Kalshi API guide covers the full endpoint reference.

The Kalshi Integration Golden Path

If you’re starting a Kalshi integration from scratch or auditing an existing one, this is the priority checklist:

  1. Build around Decimal + _dollars + _fp only. Legacy integer fields have been removed. Every line of code must use the fixed-point interface.

  2. Treat the live/historical split as already enforced. Query both tiers and merge by stable IDs. Don’t assume live endpoints return historical records.

  3. Make WebSockets authenticated, stateful, and sequence-aware. Auth the connection, track seq per sid, and re-bootstrap the book on any gap.

  4. Use client_order_id everywhere and reconcile across REST + WS. The durable ledger pattern — client_order_id → payload → server_id → fills — is the single most important debugging tool for production bots.

  5. Load rate limits dynamically. Call GET /account/limits at startup. Don’t hardcode tier values that change when your account is upgraded.

For a complete walkthrough of Kalshi’s REST, WebSocket, and FIX APIs, start with the Kalshi API guide. To compare Kalshi’s architecture against Polymarket and other prediction market platforms, see the prediction market API reference. For the Polymarket equivalent of this guide, see the top 10 Polymarket API problems. For agents that trade across multiple platforms, the cross-market arbitrage guide covers the execution layer in detail.