Kalshi API v2: 6 Undocumented Traps That Break Integrations (2026)
Guides

Kalshi API v2: 6 Undocumented Traps That Break Integrations (2026)

· EdgeOutcome Team· kalshi, api, python
Last updated

The Kalshi API v2 documentation at docs.kalshi.com covers authentication, endpoint lists, and parameter schemas. That is the public surface. Beneath it live at least a dozen behaviors, field migrations, and endpoint quirks that either went unmentioned or got buried in changelog entries you would only find if you already knew what to search for.

This guide documents those gaps — the things that will silently break your bot, return data you did not expect, or force a rewrite six months after you ship. It assumes you have already read the official docs and our Kalshi API Python tutorial. This is the deep cut.

The Fixed-Point Migration Is Not Backward-Compatible

The single most disruptive change in the Kalshi API v2 happened in Q1 2026: all price and size fields moved from integers to fixed-point strings. The old integer fields — yes_bid, yes_ask, count — were removed entirely. If your code references any of them, it gets null or a missing key, not a deprecated but still-populated field.

Here is the before-and-after:

Field (old) Type Field (current) Type Example
yes_bid integer (cents) yes_bid_dollars string "0.4200"
yes_ask integer (cents) yes_ask_dollars string "0.4500"
count integer count_fp string "10.00"
volume integer volume_fp string "1542.00"
open_interest integer open_interest_fp string "8234.00"

The new fields always carry four decimal places. A price of 42 cents becomes "0.4200". Convert with float() or Decimal() in your code — do not rely on integer parsing.

Expert insight: The most common failure pattern we see: bots that worked fine for months suddenly returning zero-volume markets. The developer never saw an error because volume became null and if not volume: skip silently filtered every market. Check your field names against the 2026 schema before troubleshooting anything else.

The Events Endpoint: Hidden Events, Nested Markets, and the with_nested_markets Flag

GET /events is the primary market discovery endpoint. The docs tell you it returns events and accepts filters. What they do not emphasize:

1. Hidden events are excluded. Kalshi can hide events from the public API — typically markets undergoing review, sponsor-only events, or test markets. If an incentive program references a hidden event, that program now also disappears from GET /incentive_programs (as of July 23, 2026). Your event count may not match what you see on the web app.

2. with_nested_markets=true is not optional for most use cases. Without it, events return with an empty or absent markets array. You need a second GET /markets call for each event ticker. With it, each event embeds its markets directly — one API call instead of N+1. The trade-off is response size: an event with 50 markets can bloat the payload considerably.

3. Multivariate events live on a separate endpoint. GET /events explicitly excludes multivariate events — use GET /events/multivariate for those. If you are building a market scanner, hit both or you will miss entire event categories.

4. The cursor pagination is opaque. The cursor value is a string, not a page number. You cannot jump to page 5. Store the cursor from each response and pass it back — there is no offset-based alternative.

5. Historical markets are included, but with a catch. The docs say “all events are accessible through this endpoint, even if their associated markets are older than the historical cutoff.” This means events themselves never disappear — but the markets nested inside them might only have partial data if they predate the cutoff. Check close_time and compare it against GET /historical/cutoff before trusting stale market data.

V2 Orders: The Endpoint Moved and the Fields Changed

If you learned Kalshi trading from a 2024 tutorial, you are posting orders to the wrong URL. The v2 order endpoint is:

POST /trade-api/v2/portfolio/events/orders

Not /portfolio/orders. Not /portfolio/events/order. The old endpoints may still resolve, but behavior is undefined and the response schema differs.

The v2 order payload also introduced fields that do not exist in v1:

Field Type What it does
time_in_force string "good_till_canceled" or "immediate_or_cancel". No default — you must specify.
self_trade_prevention_type string "taker_at_cross" (cancel aggressive), "maker" (cancel resting), or "cancel_both". Prevents self-matching.
post_only boolean If true, order only executes as maker. Rejects if it would cross the spread and take liquidity.
cancel_order_on_pause boolean If true, the exchange auto-cancels your order when trading pauses. Useful for news-sensitive positions.
reduce_only boolean If true, the order only reduces your position — it will not open a new one.
subaccount integer Target subaccount number. Defaults to 0 for unrestricted keys. Restricted keys have their subaccount inferred and a mismatch is rejected.
exchange_index integer Which exchange index to route the order to. Default is 0.

Expert insight: post_only is the most underused field. If your strategy relies on capturing the spread (maker rebates), set post_only: true and handle the rejection if the market moves. Without it, your limit order might fill as a taker at a worse effective price, and you will not know until the fill confirmation arrives.

Batch orders are available at POST /portfolio/events/orders/batched — but only for Advanced tier and above in production. The demo environment opens batch endpoints to all tiers.

The order group limit is now 25,000. As of July 22, 2026, Kalshi caps order groups at 25,000 per account. If you use order groups extensively (algos that create new groups per signal), monitor your count. Accounts over the limit at enforcement time had their oldest unused groups cancelled down to 20,000.

The Historical Data Tier: Separate Database, Separate Rules

Since February 2026, Kalshi maintains a separate historical database for settled positions, fills, and market data. The REST surface is:

GET /historical/cutoff
GET /historical/markets
GET /historical/trades
GET /historical/fills
GET /historical/orders
GET /historical/positions     (added July 23, 2026)

Key undocumented behaviors:

1. The cutoff is per-event, not global. When an event settles, the entire event’s data moves to the historical database atomically. You will never find half an event’s positions in /portfolio/positions and half in /historical/positions. Check /historical/cutoff for the market_positions_last_updated_ts — anything older belongs to the historical tier.

2. Historical endpoints have their own rate limits. They share the same token bucket, but historical queries are more expensive per token. Check GET /account/endpoint_costs before building a scanner that pulls years of settlement data.

3. Positions are entire events, never partial. If you are tracking P&L per-market, query /historical/positions with an event_ticker filter after settlement. Do not assume /portfolio/positions still has the data.

Rate Limits: No Headers, No Retry-After

The Kalshi API v2 uses a token-bucket model with five tiers (Basic through Prime). Read and Write are independent buckets. Every endpoint has a token cost — most are 10 tokens per request, but expensive endpoints cost more.

The critical undocumented detail: there are no rate-limit headers. No Retry-After, no X-RateLimit-Remaining, no X-RateLimit-Reset. When you hit the limit, you get a 429 Too Many Requests with an empty or generic body. Your only option is exponential backoff with jitter.

A production-grade backoff wrapper:

import time
import random

def safe_request(func, max_retries=5):
    for attempt in range(max_retries):
        resp = func()
        if resp.status_code == 429:
            wait = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(wait)
            continue
        resp.raise_for_status()
        return resp
    raise Exception("Rate limit exceeded after max retries")

Check your live budget at GET /account/limits — it returns your current tier, token balance, and refill rate. Poll this before running a batch job, not after you hit the wall.

Endpoint costs live at GET /account/endpoint_costs. The historical endpoints and batched endpoints cost more than simple market reads. Factor this into your polling frequency.

The Orderbook Is Bids-Only

Kalshi’s orderbook returns only the bid side. For a YES market priced at $0.60 bid, the implied NO ask is $0.40 (since YES + NO = $1.00). The reasoning: on binary contracts, the full book is redundant — one side implies the other.

In practice, this means:

yes_bid = float(book["yes_bid_dollars"])      # e.g. 0.60
no_ask_implied = 1.0 - yes_bid                 # 0.40

The bulk orderbook endpoint (GET /markets/orderbooks?tickers=...) accepts up to 100 tickers and requires authentication — unlike the single-ticker variant, which is public.

WebSocket: Heartbeats Are Server-Driven

The WebSocket at wss://external-api-ws.kalshi.com/trade-api/ws/v2 changed significantly in 2026:

1. Auth is via headers, not query string. Older WebSocket tutorials appended the signed payload to the URL. Current behavior expects KALSHI-ACCESS-KEY, KALSHI-ACCESS-SIGNATURE, and KALSHI-ACCESS-TIMESTAMP as HTTP headers during the WebSocket handshake.

2. Heartbeats are server-driven. Every 10 seconds, the server sends a Ping with body heartbeat. Your client must respond with Pong. If you implemented client-side pings from old code, remove them — the server will disconnect you for an unexpected Ping.

3. The market_lifecycle_v2 channel now carries price_ranges. As of July 2, 2026, created and price_level_structure_updated events optionally include the price_ranges array — the same {start, end, step} bands from the REST market object. This means you can update your valid-price grid without a follow-up REST call when a market’s tick structure changes.

4. Subaccount-restricted keys can now use WebSocket (July 23, 2026). Previously, a key locked to a single subaccount was rejected at WebSocket session start. Now it connects, and private channels are scoped to that subaccount. Your own orders and fills work; sibling subaccounts remain invisible.

Deprecated and Removed Fields (July 2026)

Kalshi removed these fields from the REST schema in July 2026. If your code references any of them, it breaks silently:

  • Market.response_price_units — removed
  • Market.fractional_trading_enabled — removed
  • MarketPosition.resting_orders_count — removed

Additionally, GET /trade-api/v2/exchange/announcements was removed entirely. Use GET /trade-api/v2/exchange/schedule for exchange status.

Seven New Price Level Structures (Rollout: July–August 2026)

Starting the week of July 27, 2026, Kalshi is piloting seven new price_level_structure values with finer tick sizes. The naming convention is center_{center}_edge_{edge}_cent:

  • center_whole_edge_half_cent — 1¢ center, 0.5¢ edges
  • center_whole_edge_quint_cent — 1¢ center, 0.2¢ edges
  • center_half_edge_half_cent — 0.5¢ uniform
  • center_half_edge_quint_cent — 0.5¢ center, 0.2¢ edges
  • center_half_edge_deci_cent — 0.5¢ center, 0.1¢ edges
  • center_quint_edge_quint_cent — 0.2¢ uniform
  • center_quint_edge_deci_cent — 0.2¢ center, 0.1¢ edges

Edge bands are $0.00–$0.10 and $0.90–$1.00. The center band is everything between.

What this means for your code: Do not hardcode tick sizes. Do not assume 1¢ increments. Always consume price_ranges dynamically from the market object or WebSocket event. Resting orders are preserved across structure changes — they carry over to the new grid automatically.

Practical Recommendations

Use the Parlay MCP server for cross-venue research. If your tool needs to compare Kalshi and Polymarket prices, you do not need to implement two auth schemes and normalize two schemas. Parlay does that layer. For single-venue Kalshi bots, the official kalshi-python-sync SDK is the right answer.

Pin your field names to the 2026 schema. Run a grep for yes_bid, yes_ask, count, volume, resting_orders_count, response_price_units, fractional_trading_enabled in your codebase. Replace every match with the _dollars or _fp equivalent.

Monitor the changelog RSS feed. Subscribe to https://docs.kalshi.com/changelog/rss.xml. The fixed-point migration, deprecated field removals, and new price structures all appeared there before they hit production. A weekly skim is 30 seconds that can save you a weekend debugging session.

Paper trade against the demo environment. The demo REST host is https://external-api.demo.kalshi.co/trade-api/v2 and the demo WebSocket is wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2. These are separate from production — you need a separate API key, and funds are simulated. Test every schema change here before pushing to production.


The Bottom Line

The Kalshi API v2 is a fast, well-designed trading interface — but it moves faster than its documentation. The fixed-point migration, event endpoint behavior, and V2 order fields are the three areas where most developers hit walls that the official docs do not explicitly warn about.

Understand the data model (events contain markets, markets reference series), consume price_ranges dynamically, always check /historical/cutoff before trusting old data, and never assume deprecated integer fields are still populated. Build with those rules and your integration will survive the next schema change without a rewrite.


EdgeOutcome may earn a commission if you sign up through our affiliate links. Ready to build? Create your Kalshi account and generate your API key — zero trading fees on all markets. If you are just getting started with the API, read our Kalshi API Python tutorial first for the full authentication and order-placement walkthrough. Need a strategy to automate? Our five data-driven Kalshi strategies cover everything from weather arbitrage to cross-platform trading.

EdgeOutcome
EdgeOutcome Team

Data-driven prediction market analysis. EdgeOutcome helps traders find real edges on Kalshi and Polymarket — not hype, just numbers. More about us →