Introduction

KalshiAPI is a simple market data API for Kalshi markets. You make a request over HTTP, and you get back JSON. That is the whole idea. If you can call a URL, you can use this API.

The base URL for every request is:

https://kalshiapi.com

With it you can list markets, read full details for a single market, pull hourly price candles, pull individual trades, and download bulk historical data for offline analysis. There is also an MCP server so AI and vibe-coding tools can read the data directly.

Tip New here? Grab a free key on the homepage, then copy the first curl example below. It should return data on your first try.

Authentication

Send your API key in the X-API-Key request header. Every endpoint accepts it, and most endpoints require it.

The one exception is /v1/stats, which is public and needs no key. Use it to confirm things are working before you add your key.

Try it instantly (demo key)

Use the public demo key to wire KalshiAPI into your project and get real, live data immediately, no signup. Hit copy on the example below to grab the working key. It's rate-limited per IP and capped to a few rows per call, so it's perfect for building the integration and seeing it work, then get your own free key (1,000 calls/mo) or upgrade for full results.

curl "https://kalshiapi.com/v1/markets?limit=5" -H "X-API-Key: demo_••••••••••••••••••"

Get a key

Grab a free key on the home page, or self-serve from a script or coding agent:

curl -s https://kalshiapi.com/v1/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your-password"}'

The response includes your api_key right away (usable immediately at a reduced rate). We email a verification link; clicking it unlocks the full free tier. password is optional. There's a one-paste setup prompt for coding agents on the home page.

curl https://kalshiapi.com/v1/markets?limit=5 \
  -H "X-API-Key: YOUR_KEY"
import requests

r = requests.get(
    "https://kalshiapi.com/v1/markets",
    params={"limit": 5},
    headers={"X-API-Key": "YOUR_KEY"},
)
print(r.json())
const r = await fetch("https://kalshiapi.com/v1/markets?limit=5", {
  headers: { "X-API-Key": "YOUR_KEY" },
});
const data = await r.json();
console.log(data);

The free key works against sample data. Get one on the homepage. Keep your key private; treat it like a password.

Rate limits & tiers

The free tier is built for trying things out and small projects. Paid tiers raise the limits and unlock full data. See the pricing section for current plans.

TierPer minutePer monthData
Free60 requests1,000 requestsSample data
Starter600 requests100,000 requestsFull data
Pro1,200 requests1,000,000 requestsFull data + live + bulk

If you go over a limit you get a 429 response. Slow down and retry, or move to a higher tier. Check your live usage anytime with GET /v1/me (returns your tier, monthly quota and calls remaining). An unverified email is capped at 15 requests/minute until you click the verification link.

RapidAPI

KalshiAPI is also available on RapidAPI. If you subscribe there, RapidAPI handles your key and billing, just call the endpoints through the RapidAPI host with your RapidAPI key, no separate KalshiAPI key needed.

Markets

List and search markets. This is the endpoint you will use most.

GET /v1/markets

Query parameters

ParamTypeDescription
categorystringFilter by category, for example Crypto, Sports, Financials, Climate and Weather, Commodities, Economics, Elections.
statusstringFilter by market status.
seriesstringFilter by series_ticker.
eventstringFilter by event_ticker.
qstringText search over the market title.
resolvedbooleantrue or false.
limitintegerResults per page. Default 100, max 1000.
offsetintegerNumber of results to skip. Default 0.

Example

curl "https://kalshiapi.com/v1/markets?category=Crypto&limit=100" \
  -H "X-API-Key: YOUR_KEY"
import requests

r = requests.get(
    "https://kalshiapi.com/v1/markets",
    params={"category": "Crypto", "limit": 100},
    headers={"X-API-Key": "YOUR_KEY"},
)
data = r.json()
print(data["total"], "markets total")
for m in data["markets"]:
    print(m["ticker"], m["title"])
const r = await fetch(
  "https://kalshiapi.com/v1/markets?category=Crypto&limit=100",
  { headers: { "X-API-Key": "YOUR_KEY" } }
);
const data = await r.json();
console.log(data.total, "markets total");
data.markets.forEach(m => console.log(m.ticker, m.title));

Response shape

{
  "total": 1342,
  "limit": 100,
  "offset": 0,
  "markets": [
    {
      "ticker": "KXBTCD-25DEC31",
      "event_ticker": "KXBTCD-25DEC31",
      "series_ticker": "KXBTCD",
      "category": "Crypto",
      "title": "Bitcoin price on Dec 31, 2025",
      "yes_sub_title": "Above $100,000",
      "status": "active",
      "result": "",
      "open_time": "2025-01-02T15:00:00Z",
      "close_time": "2025-12-31T22:00:00Z",
      "volume_fp": 184200,
      "open_interest_fp": 52100,
      "liquidity_dollars": 14380.55
    }
  ]
}

Market fields

FieldDescription
tickerUnique market ticker, for example KXBTCD-25DEC31.
event_tickerTicker of the event the market belongs to.
series_tickerTicker of the series the market belongs to.
categoryMarket category.
titleHuman readable market question.
yes_sub_titleShort label describing the YES outcome.
statusCurrent market status.
resultSettlement result, empty until resolved.
open_timeWhen the market opened.
close_timeWhen the market closes.
volume_fpTraded volume.
open_interest_fpOpen interest.
liquidity_dollarsResting liquidity in dollars.
Tip To page through everything, keep limit fixed and add limit to offset on each request until you have seen total markets.

Market detail

Get the full metadata for one market by its ticker. This returns more than the list view, including rules, strikes, settlement details, and outcome.

GET /v1/markets/{ticker}
curl "https://kalshiapi.com/v1/markets/KXBTCD-25DEC31" \
  -H "X-API-Key: YOUR_KEY"
import requests

ticker = "KXBTCD-25DEC31"
r = requests.get(
    f"https://kalshiapi.com/v1/markets/{ticker}",
    headers={"X-API-Key": "YOUR_KEY"},
)
print(r.json())
const ticker = "KXBTCD-25DEC31";
const r = await fetch(`https://kalshiapi.com/v1/markets/${ticker}`, {
  headers: { "X-API-Key": "YOUR_KEY" },
});
console.log(await r.json());

The response includes every field from the list view plus the market's full rules, strike information, settlement details, and outcome.

Candles

Hourly price candles for a single market. Each candle covers one hour and carries open, high, low, close, plus bid and ask detail and volume.

GET /v1/markets/{ticker}/candles

Query parameters

ParamTypeDescription
startintegerStart time in epoch seconds.
endintegerEnd time in epoch seconds.
limitintegerMaximum number of candles to return.
formatstringjson or parquet. Default json.
curl "https://kalshiapi.com/v1/markets/KXBTCD-25DEC31/candles?start=1735689600&end=1735776000" \
  -H "X-API-Key: YOUR_KEY"
import requests

r = requests.get(
    "https://kalshiapi.com/v1/markets/KXBTCD-25DEC31/candles",
    params={"start": 1735689600, "end": 1735776000},
    headers={"X-API-Key": "YOUR_KEY"},
)
data = r.json()
for c in data["data"]:
    print(c["ts"], c["price_close"])
const url =
  "https://kalshiapi.com/v1/markets/KXBTCD-25DEC31/candles?start=1735689600&end=1735776000";
const r = await fetch(url, { headers: { "X-API-Key": "YOUR_KEY" } });
const data = await r.json();
data.data.forEach(c => console.log(c.ts, c.price_close));

Response shape

{
  "ticker": "KXBTCD-25DEC31",
  "kind": "candles",
  "rows": 24,
  "data": [
    {
      "ts": 1735689600,
      "price_open": 61, "price_high": 64, "price_low": 60,
      "price_close": 63, "price_mean": 62, "price_previous": 61,
      "yes_bid_open": 60, "yes_bid_high": 63, "yes_bid_low": 59, "yes_bid_close": 62,
      "yes_ask_open": 62, "yes_ask_high": 65, "yes_ask_low": 61, "yes_ask_close": 64,
      "volume": 1820, "open_interest": 50120
    }
  ]
}

Candle fields

FieldDescription
tsCandle start time in epoch seconds.
price_open, price_high, price_low, price_closeTrade price open, high, low, and close for the hour.
price_meanMean trade price for the hour.
price_previousClose from the prior candle.
yes_bid_open/high/low/closeYES bid price over the hour.
yes_ask_open/high/low/closeYES ask price over the hour.
volumeContracts traded during the hour.
open_interestOpen interest at the candle.

Trades

Individual executed trades for a single market. Use these when you want tick-level detail instead of hourly aggregates.

GET /v1/markets/{ticker}/trades

Query parameters

Same parameters as candles: start and end in epoch seconds, limit, and format (json or parquet, default json).

curl "https://kalshiapi.com/v1/markets/AMAZONFTC-29DEC31/trades?limit=200" \
  -H "X-API-Key: YOUR_KEY"
import requests

r = requests.get(
    "https://kalshiapi.com/v1/markets/AMAZONFTC-29DEC31/trades",
    params={"limit": 200},
    headers={"X-API-Key": "YOUR_KEY"},
)
data = r.json()
for t in data["data"]:
    print(t["created_time"], t["yes_price_dollars"], t["taker_side"])
const url =
  "https://kalshiapi.com/v1/markets/AMAZONFTC-29DEC31/trades?limit=200";
const r = await fetch(url, { headers: { "X-API-Key": "YOUR_KEY" } });
const data = await r.json();
data.data.forEach(t => console.log(t.created_time, t.yes_price_dollars, t.taker_side));

Trade fields

FieldDescription
trade_idUnique trade identifier.
created_timeWhen the trade executed.
yes_price_dollarsYES price in dollars.
no_price_dollarsNO price in dollars.
count_fpNumber of contracts traded.
taker_sideSide the taker was on.
taker_outcome_sideOutcome side the taker took.
is_block_tradeWhether the trade was a block trade.
tickerMarket ticker for the trade.

Teams

Per-team record across every settled game-winner market on Kalshi. Each team gets a real win/loss record. For pro keys, each team also carries the average YES price the market was charging before the game (avg_implied_pct) and gap_pct — the team's real win rate minus that average price. These are descriptive historical figures; we show the numbers, we don't predict future results.

GET /v1/teams

Query parameters

ParamDescription
leagueFilter to one league, e.g. MLB, English Premier League.
min_gamesMinimum settled games (default 1). Use 30+ for stable calibration.
sortgames, win_pct, wins, or gap_pct (pro). Default games.
orderasc or desc (default).
limit / offsetPagination. limit 1–1000 (default 100).

Example

curl "https://kalshiapi.com/v1/teams?league=MLB&min_games=30&sort=gap_pct" \
  -H "X-API-Key: YOUR_KEY"
import requests

r = requests.get(
    "https://kalshiapi.com/v1/teams",
    params={"league": "MLB", "min_games": 30, "sort": "gap_pct"},
    headers={"X-API-Key": "YOUR_KEY"},
)
for t in r.json()["teams"]:
    print(t["team"], t["win_pct"], t.get("gap_pct"))
const r = await fetch(
  "https://kalshiapi.com/v1/teams?league=MLB&min_games=30&sort=gap_pct",
  { headers: { "X-API-Key": "YOUR_KEY" } }
);
const data = await r.json();
data.teams.forEach(t => console.log(t.team, t.win_pct, t.gap_pct));

Response shape

{
  "total": 31,
  "limit": 100,
  "offset": 0,
  "teams": [
    {
      "team": "Chicago WS",
      "league": "MLB",
      "games": 58,
      "wins": 34,
      "losses": 24,
      "win_pct": 58.6,
      "avg_implied_pct": 45.9,
      "gap_pct": 12.7,
      "priced_games": 31
    }
  ]
}

Team fields

FieldDescription
teamTeam name as Kalshi labels it (the YES side of the matchup).
leagueLeague the team plays in.
games / wins / lossesSettled game-winner markets and the record.
win_pctReal win rate, percent.
avg_implied_pct (pro)Average pre-game YES price the market charged, percent.
gap_pct (pro)win_pct minus avg_implied_pct: the historical difference between the team's win rate and the price. Descriptive, not a prediction.
priced_games (pro)Games with candle pricing used for calibration.

Single team: GET /v1/teams/{team} returns one team's row (URL-encode the name, e.g. /v1/teams/Chicago%20WS).

Bulk historical data

Need the whole history at once? Bulk download gives you full datasets as Parquet files, packaged into a single .zip (unzip on any OS, no tools needed). This is the fastest way to load everything into pandas, Polars, or DuckDB for backtesting and research. Bulk is a paid feature, also available as a one-time purchase on the pricing section.

Datasets

DatasetContents
metadataAll market metadata.
candlesAll hourly candles.
tradesAll individual trades.
fullEverything above in one archive.

Snapshots are refreshed monthly.

1. Browse the catalog

See which datasets exist along with their current snapshot size and freshness.

GET /v1/bulk/catalog

2. Place an order

Order a dataset. Large datasets build in the background, so the order starts in a building state and finishes ready.

POST /v1/bulk/order?dataset=full

The response includes the order details:

{
  "id": "ord_8f21",
  "dataset": "full",
  "status": "building"
}

3. Poll until ready

Check the order status. When it is ready the response includes a download_url.

GET /v1/bulk/order/{id}

4. Download

Download the .zip once the order is ready (the link is private to your purchase and expires).

GET /v1/bulk/download/{id}?token=...
# 1. order
curl -X POST "https://kalshiapi.com/v1/bulk/order?dataset=full" \
  -H "X-API-Key: YOUR_KEY"

# 2. poll (repeat until status is ready)
curl "https://kalshiapi.com/v1/bulk/order/ord_8f21" \
  -H "X-API-Key: YOUR_KEY"

# 3. download once ready
curl -L "https://kalshiapi.com/v1/bulk/download/ord_8f21?token=TOKEN" \
  -H "X-API-Key: YOUR_KEY" -o kalshi-full.tar
import time, requests

h = {"X-API-Key": "YOUR_KEY"}

order = requests.post(
    "https://kalshiapi.com/v1/bulk/order",
    params={"dataset": "full"}, headers=h,
).json()
oid = order["id"]

while True:
    o = requests.get(f"https://kalshiapi.com/v1/bulk/order/{oid}", headers=h).json()
    if o["status"] == "ready":
        break
    time.sleep(10)

data = requests.get(o["download_url"], headers=h)
open("kalshi-full.tar", "wb").write(data.content)
const h = { "X-API-Key": "YOUR_KEY" };

let order = await (await fetch(
  "https://kalshiapi.com/v1/bulk/order?dataset=full",
  { method: "POST", headers: h }
)).json();

let o;
do {
  await new Promise(r => setTimeout(r, 10000));
  o = await (await fetch(
    `https://kalshiapi.com/v1/bulk/order/${order.id}`, { headers: h }
  )).json();
} while (o.status !== "ready");

const res = await fetch(o.download_url, { headers: h });
// save res.body to disk in your runtime
Tip Unpack the tar and you get Parquet files. Point pandas, Polars, or DuckDB straight at them, no database setup needed.

MCP server

The MCP server is the easy way to use this data with AI and vibe-coding tools. It lets assistants like Claude Desktop and Cursor read the data directly while you build, no glue code required. Want a walkthrough? Read the blog post.

The remote endpoint uses streamable HTTP:

https://kalshiapi.com/mcp

It works with no key against sample data, so you can paste the URL into your client and try it instantly. To raise your limits and reach full data, pass your key in the X-API-Key header in your client config.

Config example

Add this to your Claude Desktop or Cursor MCP configuration:

{
  "mcpServers": {
    "kalshiapi": {
      "url": "https://kalshiapi.com/mcp",
      "headers": { "X-API-Key": "YOUR_KEY" }
    }
  }
}

Drop the headers block to run keyless on sample data.

Available tools

ToolWhat it does
dataset_statsDataset coverage and freshness.
category_yes_ratesYES resolution rates by category.
list_marketsList and search markets.
get_marketFull detail for one market.
get_candlesHourly candles for a market.
get_tradesIndividual trades for a market.

Live stream (WebSocket)

For real-time data, open a WebSocket and subscribe to the markets you care about. You'll get a message every time one of your subscribed markets updates, the same firehose that feeds the dataset. Built for live trading bots and dashboards.

Connect to the endpoint and pass your key as a query parameter (browsers can't set headers on a WebSocket):

wss://kalshiapi.com/v1/stream?api_key=YOUR_KEY

On connect you get a welcome message with your tier and ticker limit. Then send a subscribe action with up to 1,000 tickers. The server pushes an update message (with the full market row) whenever a subscribed market changes.

Protocol

You sendServer replies / pushes
{"action":"subscribe","tickers":["KXBTC...","KXNBA..."]}{"type":"subscribed","count":2,"rejected":0}
{"action":"unsubscribe","tickers":["KXBTC..."]}{"type":"unsubscribed","count":1}
{"action":"ping"}{"type":"pong"}
{"type":"update","market":{ …live market row… }}

Example

const ws = new WebSocket("wss://kalshiapi.com/v1/stream?api_key=YOUR_KEY");
ws.onopen = () => ws.send(JSON.stringify({ action: "subscribe", tickers: ["KXBTCD-25DEC31"] }));
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "update") console.log(msg.market);
};
Note The live stream needs a valid key; higher tiers raise your concurrent ticker and connection limits. See pricing.

Data formats

The API speaks JSON by default. On the candles and trades endpoints you can pass format=parquet to get Apache Parquet instead. Bulk downloads are always Parquet, packaged in a .zip.

Parquet is a columnar format that loads fast and keeps types intact. Read it straight into pandas:

import pandas as pd
df = pd.read_parquet("candles.parquet")

Polars and DuckDB read the same files with pl.read_parquet(...) and SELECT * FROM 'candles.parquet'.

Errors

Errors use standard HTTP status codes. Check the code first, then read the JSON body for detail.

StatusMeaning
400Bad request. A parameter is missing or malformed.
401Invalid or missing API key. Check your X-API-Key header.
404Not found. The ticker or resource does not exist.
429Rate limit exceeded. Slow down or move to a higher tier.
Tip Getting a 401 on every call? Confirm the header name is exactly X-API-Key and that your key has no extra spaces. Still stuck? Contact support.