▌tickfoundry
capture livesign inget the data →
docs / kalshi

Kalshi — schema & delivery

We capture Kalshi's market-data websocket on two independent legs, merge and deduplicate them hourly, and seal one UTC day at a time into four tables per series (the 25-level book, top of book, trades and Kalshi's ticker) from 2026-09-27 (11:00 UTC that day), plus the CF Benchmarks index feed as one whole-exchange file a day from 2026-09-28. Before that Kalshi has trades only, sold as kalshi_trades_history from 2021-06-30. It is sold per day with a series filter on the Starter, L10 and L25 plans bought for Kalshi (L10 serves the book at 10 levels; the index feed rides on L25), its own subscription that adds up with plans for other venues, and any account can pull the sample day (2026-10-07, up to 1.0 GB). Pull from /catalog/kalshi or the API below.

File layout

One parquet per table per series per day, in a table-first tree, so a single read_parquet on a table directory reads every series and day you pulled with date injected from the path. The series directory is plain (not series=): every file carries market_ticker, and the series is its prefix before the first -. The CF Benchmarks index is one file a day (the collector files it under _cfbenchmarks).

bundle layouttext
kalshi-kxfeddecision-2026-10-01-to-2026-10-07-l25/
  README.md
  kalshi_l1/date=2026-10-01/KXFEDDECISION/l1.parquet
  kalshi_l2/date=2026-10-01/KXFEDDECISION/l2.parquet
  kalshi_trades/date=2026-10-01/KXFEDDECISION/trades.parquet
  kalshi_ticker/date=2026-10-01/KXFEDDECISION/ticker.parquet
  kalshi_cfbenchmarks/date=2026-10-01/cfbenchmarks.parquet
  …

kalshi-exchange-2021-07-01-to-2021-07-31-trades/
  README.md
  kalshi_trades_history/date=2021-07-01/trades.parquet
  …
read the book of one market and walk itpython
import pandas as pd

l2 = pd.read_parquet("kalshi_l2")                    # every series and day pulled
mkt = l2[l2.market_ticker == "KXFEDDECISION-26OCT-H0"].sort_values("ts_recv_ns")

# YES terms: asks are 1 - NO bids, at the NO bids' sizes
def cost_to_buy(row, contracts, levels=25):
    left, cost = contracts, 0.0
    for i in range(1, levels + 1):
        px, sz = row[f"ask_px_{i}"], row[f"ask_sz_{i}"]
        if pd.isna(px) or left <= 0:
            break
        take = min(left, sz)
        cost, left = cost + take * px, left - take
    return cost if left <= 0 else None              # None: not enough depth

trades = pd.read_parquet("kalshi_trades")
history = pd.read_parquet("kalshi_trades_history")  # whole exchange, one file a day
fed = history[history.market_ticker.str.startswith("KXFEDDECISION-")]

Tables

Every table carries ts_src_ms (the exchange's clock), ts_recv_ns (ours, the earlier of the two legs) and market_ticker. Prices are dollars exact to 1e-4, sizes contracts exact to 1e-2.

kalshi_l1 — Top of book (every change)

Best YES bid and ask with sizes and the spread, one row whenever either changed, derived tick by tick from the rebuilt book: not Kalshi's conflated ticker. ~2.4 GB · all series.

columntypedescription
ts_src_msint64Exchange timestamp, ms since the Unix epoch, UTC.
ts_recv_nsint64When our collector received the message, ns since the Unix epoch, UTC (the earlier of the two legs).
market_tickerstringKalshi market ticker, e.g. KXFEDDECISION-26OCT-H0. The series ticker is the part before the first -.
best_biddoubleBest YES bid, dollars.
best_bid_sizedoubleContracts at the best bid.
best_askdoubleBest YES ask, dollars.
best_ask_sizedoubleContracts at the best ask.
spreaddoublebest_ask - best_bid.
source_event_typestringThe l2 message the change came from (orderbook_delta / orderbook_snapshot).
msg_seqint64Kalshi's sequence number of that message.

kalshi_l2 — Order book, 25 levels, every change

The whole ladder after every book event, in YES terms (the NO bids become the asks, at their sizes), 25 levels a side: wide columns bid_px_1 … ask_sz_25, plus the exchange's own hourly snapshots. Each row is self-contained, so you can start reading mid-day. ~23 GB · all series.

columntypedescription
ts_src_msint64Exchange timestamp, ms since the Unix epoch, UTC.
ts_recv_nsint64When our collector received the message, ns since the Unix epoch, UTC (the earlier of the two legs).
market_tickerstringKalshi market ticker, e.g. KXFEDDECISION-26OCT-H0. The series ticker is the part before the first -.
event_kindstringsnapshot (an exchange book snapshot, hourly per market and on subscribe) or update (one level changed).
source_event_typestringorderbook_snapshot / orderbook_delta, the websocket message the row came from.
msg_seqint64Kalshi's per-subscription sequence number.
bid_levelsint16Price levels on that side of the exchange's book after this event; can exceed 25 (the columns carry the best 25; capped at 10 on L10).
ask_levelsint16Same for the ask side.
best_biddouble= bid_px_1.
best_askdouble= ask_px_1.
bid_px_1 … bid_px_25doubleYES bid price at level 1 (best) … 25, dollars (exact to 1e-4). Null beyond the levels present.
bid_sz_1 … bid_sz_25doubleResting contracts at that level (exact to 1e-2).
ask_px_1 … ask_px_25doubleYES ask price: one minus the NO bid at that level, at the NO bid's size.
ask_sz_1 … ask_sz_25doubleContracts at that level.
legsstringWhich capture legs received the message: ab both, a / b one (snapshots are per leg).

kalshi_trades — Trades

Every trade: YES and NO price, size in contracts, the taker's side, block-trade flag, trade id. ~440 MB · all series.

columntypedescription
ts_src_msint64Exchange timestamp, ms since the Unix epoch, UTC.
ts_recv_nsint64When our collector received the message, ns since the Unix epoch, UTC (the earlier of the two legs).
market_tickerstringKalshi market ticker, e.g. KXFEDDECISION-26OCT-H0. The series ticker is the part before the first -.
pricedoubleTrade price of YES, dollars.
sizedoubleContracts.
sidestringThe TAKER's outcome side: yes / no.
trade_idstringKalshi trade id.
no_pricedoubleTrade price of NO (1 - price).
taker_book_sidestringKalshi's own taker_book_side field as published: bid / ask (in every file we checked, bid for YES takers and ask for NO takers).
is_block_tradeboolBlock trade flag from the exchange.
msg_seqint64Websocket sequence number of the trade message.
legsstringWhich capture legs saw the trade (a, b, ab).

kalshi_ticker — Kalshi ticker (~1 s)

Kalshi's own ticker feed, about one update a second per market: last price, YES bid/ask with sizes, cumulative volume and open interest in contracts and dollars. ~1.1 GB · all series.

columntypedescription
ts_src_msint64Exchange timestamp, ms since the Unix epoch, UTC.
ts_recv_nsint64When our collector received the message, ns since the Unix epoch, UTC (the earlier of the two legs).
market_tickerstringKalshi market ticker, e.g. KXFEDDECISION-26OCT-H0. The series ticker is the part before the first -.
pricedoubleLast traded price, dollars.
yes_biddoubleBest YES bid per the ticker.
yes_askdoubleBest YES ask per the ticker.
yes_bid_sizedoubleContracts at yes_bid.
yes_ask_sizedoubleContracts at yes_ask.
last_trade_sizedoubleSize of the last trade.
volumedoubleContracts traded, cumulative.
open_interestdoubleOpen interest, contracts.
dollar_volumedoubleDollar volume, cumulative.
dollar_open_interestdoubleDollar open interest.
msg_seqint64Websocket sequence number.
legsstringWhich capture legs received the frame.

kalshi_cfbenchmarks — CF Benchmarks index ticks

The CF Benchmarks reference indexes Kalshi's crypto markets settle on (BRTI and the ETH, SOL, XRP, DOGE … real-time indexes), 1-second and 5 Hz, with Kalshi's 60-second averages. One file a day; on L25. ~72 MB · one file.

columntypedescription
ts_src_msint64Index timestamp, ms UTC.
ts_recv_nsint64Receive time, ns UTC.
index_idstringCF Benchmarks index, e.g. BRTI, ETHUSD_RTI.
channelstring1s or 5hz.
valuedoubleIndex value.
kalshi_received_msint64When Kalshi stamped it.
avg_60s_valuedoubleTrailing 60-second average, where Kalshi publishes it.
avg_60s_window_sizeint64Points in that average.
avg_60s_window_start_msint64Window start, ms UTC.
avg_60s_window_end_msint64Window end, ms UTC.
extra_jsonstringAny other fields Kalshi sent, verbatim JSON.
msg_seqint64Websocket sequence.
legsstringWhich capture legs received the frame.

kalshi_trades_history — Trade history before our capture (trades only)

Every trade Kalshi reports from 30 June 2021 to 26 September 2026, for the whole exchange, one file a day. Trades only: Kalshi's historical API has no tick-level order book, so these days carry no l1, l2 or ticker. ~4 kB a day in 2021, ~300 MB a day by September 2026.

columntypedescription
ts_src_msint64The trade's created_time from Kalshi's REST history, ms UTC.
ts_recv_nsint64Always null: these rows were not received live.
market_tickerstringKalshi market ticker, e.g. KXFEDDECISION-26OCT-H0. The series ticker is the part before the first -.
pricedoubleTrade price of YES, dollars.
sizedoubleContracts.
sidestringThe TAKER's outcome side: yes / no.
trade_idstringKalshi trade id.
no_pricedoubleTrade price of NO (1 - price).
taker_book_sidestringKalshi's own taker_book_side field as published: bid / ask (in every file we checked, bid for YES takers and ask for NO takers).
is_block_tradeboolBlock trade flag from the exchange.
msg_seqint64The trade's position in that day's file (0, 1, 2 …): an ordinal, not a websocket sequence.
legsstringAlways rest.

Read before you pull

One book per market, in YES terms. Kalshi quotes YES and NO bids. The NO bids become the asks (price 1 − NO bid, at its size), so the book is one ladder per market and nothing on the NO side is lost. bid_levels / ask_levels count the levels on the exchange's book, which can exceed 25; the columns carry the best 25.

L10 is a cut, not a different capture. On L10 the l2 file keeps levels 1-10, caps the level counts at 10, and drops a row in which only levels beyond 10 moved, so every row is a real change to the levels you have.

The trades history is trades only. Before 2026-09-27 Kalshi publishes trades but no tick-level book; those days are one whole-exchange file each, sorted by market_ticker, with ts_recv_ns null and legs = rest. They are bought as backfill on any Kalshi plan ($7.50 a month), never per series and never at book depth.

The first day starts at 11:00 UTC. Our capture began at 11:00 UTC on 27 September 2026: 13 hours that day, not 24, and no CF Benchmarks index file (that capture starts on the 28th).

A book-quality dip on 2026-09-29. During a pipeline cutover 10,255 exchange snapshots disagreed with the rebuilt book (41 to 1,378 on every other day) and 16,678 negative levels were clamped to zero. The book resynchronises at every exchange snapshot; trades and ticker are unaffected.

REST API

GET /api/v1/kalshi lists coverage, the trades history's range, your access and one page of series (?q=, ?category=, ?sort=, ?page=). POST /api/v1/kalshi/downloads requests a range: up to 31 days and 10 GB of captured tables per request, or kalshi_trades_history alone, up to 31 days and 12 GB. Poll GET /api/v1/downloads/{id} like every other download.

request a week of one seriespython
import requests, time
API = "https://tickfoundry.com/api/v1"
H = {"Authorization": "Bearer tf_live_…"}
job = requests.post(f"{API}/kalshi/downloads", headers=H, json={
    "start": "2026-10-01", "end": "2026-10-07",
    "datasets": ["kalshi_l1", "kalshi_l2", "kalshi_trades"],
    "series": ["KXFEDDECISION"],
}).json()
while (s := requests.get(f"{API}/downloads/{job['id']}", headers=H).json())["status"] == "staging":
    time.sleep(5)
open("kalshi.zip", "wb").write(requests.get(s["url"], headers=H).content)