Kalshi Order Book API: Depth in TypeScript and Python

The Short Answer
Kalshi's native order book returns separate YES-bid and NO-bid ladders. It does not return explicit asks because a NO bid at price p implies a YES ask at 1 - p, and the reverse is also true. Predictefy performs that conversion and returns a normalized book with numeric bids and asks. In TypeScript, call client.kalshi.fetchOrderBook({ outcomeId }). In Python, call client.kalshi.fetch_order_book(outcome_id).
Key Takeaways
- Kalshi's native response contains
yes_dollarsandno_dollarsbid arrays, not explicit ask arrays. - Predictefy turns the selected outcome into a conventional
bidsandasksbook with prices from 0 to 1. - Order-book calls use a Predictefy
outcomeId, not a Kalshi market ticker or a parent market id. - The TypeScript and Python SDKs expose the same normalized data model, and TypeScript adds WebSocket helpers for event-driven streaming.
- Sort levels before calculating execution price and inspect freshness and provenance instead of treating every response as equally current.
- The examples below use
@predictefy/sdk@1.0.0-beta.9andpredictefy==1.0.0b7, verified against npm and PyPI on September 20, 2026.
What does the native Kalshi order book return?
Kalshi models a binary contract as two complementary sides. Its current fixed-point order-book response contains orderbook_fp.yes_dollars and orderbook_fp.no_dollars. Every level is a two-item string array containing price in dollars and contract count.
{
"orderbook_fp": {
"yes_dollars": [
["0.4100", "10.00"],
["0.4200", "13.00"]
],
"no_dollars": [
["0.5400", "12.00"],
["0.5600", "17.00"]
]
}
}
The arrays are documented in ascending price order, so the final element is the best bid. In this example, the best YES bid is 0.42. The best NO bid is 0.56, which implies a YES ask of 1 - 0.56 = 0.44. The YES spread is therefore 0.02.
| Selected side | Native bid source | Native source for asks | Ask conversion |
|---|---|---|---|
| YES | yes_dollars | no_dollars | 1 - noBidPrice |
| NO | no_dollars | yes_dollars | 1 - yesBidPrice |
Kalshi's order-book guide explains this reciprocal model and shows an unauthenticated request. Its generated endpoint reference currently renders Kalshi authentication headers as required. A keyless request returned HTTP 200 during our September 18, 2026 verification, but production code should still handle a future authentication change instead of assuming documentation will never move.
What does Predictefy normalize?
Predictefy selects one outcome and presents the book in a conventional exchange-style shape. Both sides contain objects with numeric price and size. A level may also include orderCount. The response can include timestamp, datetime, asOf, lastTradePrice and provenance.
{
"bids": [
{ "price": 0.42, "size": 13 }
],
"asks": [
{ "price": 0.44, "size": 17 }
],
"timestamp": 1789707600000,
"datetime": "2026-09-18T05:00:00.000Z",
"asOf": "2026-09-18T05:00:00.000Z",
"provenance": {
"source": "venue-rest"
}
}
This is the main engineering benefit. Strategy code can consume the same bid-and-ask representation for Kalshi, Polymarket and other served venues instead of carrying Kalshi-only complement logic through every depth calculation. The current Predictefy order-book contract defines prices as probabilities from 0 to 1 and documents both live and stored-snapshot reads.
How do you find the correct outcomeId?
Predictefy order books are outcome-keyed. First discover a Kalshi market, then select the outcome you want from its inline outcomes array. Do not pass the native Kalshi ticker directly to fetchOrderBook. Do not pass the parent marketId either, because a binary market has one outcome book per side.
curl -s \
"https://data.predictefy.com/api/kalshi/fetchMarkets?query=CPI&status=active&limit=1" \
-H "Authorization: Bearer $PREDICTEFY_API_KEY"
A normalized market record includes its marketId, title, outcomes, freshness, provenance and capability information. Choose the outcome by label rather than assuming the first array item is always YES.
TypeScript: fetch normalized Kalshi depth
Install the exact beta version so a future prerelease does not silently change your build:
npm install @predictefy/sdk@1.0.0-beta.9
import Predictefy from '@predictefy/sdk';
const client = new Predictefy({
apiKey: process.env.PREDICTEFY_API_KEY,
});
const markets = await client.kalshi.fetchMarkets({
query: 'CPI',
status: 'active',
limit: 10,
});
const market = markets[0];
if (!market) throw new Error('No matching Kalshi market');
const yes = market.outcomes.find(
(outcome) => outcome.label.toLowerCase() === 'yes',
);
if (!yes) throw new Error('YES outcome not found');
const book = await client.kalshi.fetchOrderBook({
outcomeId: yes.outcomeId,
});
const bestBid = [...book.bids].sort((a, b) => b.price - a.price)[0];
const bestAsk = [...book.asks].sort((a, b) => a.price - b.price)[0];
console.log({
market: market.title,
outcome: yes.label,
bestBid,
bestAsk,
asOf: book.asOf ?? book.datetime,
provenance: book.provenance,
});
The explicit sorts make the calculation independent of response ordering. The code also keeps the market, selected outcome, timestamp and provenance beside the price levels, which is safer than passing an anonymous array deeper into a trading system.
Python: fetch the same normalized book
The synchronous Python client uses snake-case method names and accepts the outcome id directly for a single book:
pip install predictefy==1.0.0b7
import os
from predictefy import Predictefy
with Predictefy(api_key=os.environ["PREDICTEFY_API_KEY"]) as client:
markets = client.kalshi.fetch_markets({
"query": "CPI",
"status": "active",
"limit": 10,
})
if not markets:
raise RuntimeError("No matching Kalshi market")
market = markets[0]
yes = next(
(
outcome
for outcome in market["outcomes"]
if outcome["label"].lower() == "yes"
),
None,
)
if yes is None:
raise RuntimeError("YES outcome not found")
book_response = client.kalshi.fetch_order_book(yes["outcomeId"])
book = book_response["data"]
best_bid = max(book["bids"], key=lambda level: level["price"], default=None)
best_ask = min(book["asks"], key=lambda level: level["price"], default=None)
print({
"market": market["title"],
"outcome": yes["label"],
"best_bid": best_bid,
"best_ask": best_ask,
"as_of": book.get("asOf") or book.get("datetime"),
"provenance": book.get("provenance"),
})
The Python SDK does not currently provide Predictefy WebSocket helpers. Poll fetch_order_book for Python workflows that need refreshed depth, or use the TypeScript streaming client when event-driven updates are required. This split is documented in the current Python SDK documentation.
How do you calculate the price of a larger order?
The best ask covers only the quantity resting at that level. A larger buy must walk asks from cheapest to most expensive. The weighted average is the meaningful execution-price estimate.
type Level = { price: number; size: number };
function quoteBuy(asks: Level[], requestedSize: number) {
let remaining = requestedSize;
let cost = 0;
for (const level of [...asks].sort((a, b) => a.price - b.price)) {
const filledHere = Math.min(remaining, level.size);
cost += filledHere * level.price;
remaining -= filledHere;
if (remaining <= 0) break;
}
const filled = requestedSize - remaining;
return {
requestedSize,
filled,
fillable: remaining <= 0,
averagePrice: filled > 0 ? cost / filled : null,
totalCost: cost,
};
}
from decimal import Decimal
def quote_buy(asks, requested_size):
requested = Decimal(str(requested_size))
remaining = requested
cost = Decimal("0")
levels = sorted(
asks,
key=lambda level: Decimal(str(level["price"])),
)
for level in levels:
price = Decimal(str(level["price"]))
size = Decimal(str(level["size"]))
filled_here = min(remaining, size)
cost += filled_here * price
remaining -= filled_here
if remaining <= 0:
break
filled = requested - remaining
return {
"requested_size": requested,
"filled": filled,
"fillable": remaining <= 0,
"average_price": cost / filled if filled else None,
"total_cost": cost,
}
Returning the partial fill explicitly prevents a thin book from being mistaken for a complete quote. Fees and fill probability still sit outside this calculation. A snapshot shows displayed depth, not a guaranteed execution.
How do you use REST without an SDK?
curl -s \
"https://data.predictefy.com/api/kalshi/fetchOrderBook?outcomeId=OUTCOME_ID" \
-H "Authorization: Bearer $PREDICTEFY_API_KEY"
Every Predictefy route requires a Predictefy bearer key. The native venue's authentication behavior does not carry over to the normalized API. Create a key in the Predictefy developer portal, keep it server-side and send it only in the Authorization header.
What should production code validate?
| Check | Why it matters |
|---|---|
| Capability support | Predictefy is capability-qualified. Read the Kalshi has map instead of assuming every verb works on every venue. |
| Selected outcome | A YES book and a NO book are different views. Log both label and outcomeId. |
| Freshness | Reject a snapshot that is older than the strategy's maximum tolerated age. |
| Provenance | Distinguish a fresh venue read from a live-hub, archive or fixture source. |
| Empty levels | No ask liquidity is not the same as a zero price. Return an unavailable quote. |
| Requested size | Calculate through all required levels and report incomplete depth. |
The single-book endpoint can also serve stored snapshots with at, since and until. Predictefy documents these as captured top-20 snapshots, not a reconstructed full-depth event log. Label historical results accordingly.
When should you use Kalshi directly?
Use Kalshi's native API when the application is Kalshi-only and native field fidelity matters more than a shared model. Use Predictefy when one depth engine must work across venues, when you want explicit asks without maintaining complement logic, or when the same application will combine books with normalized discovery, trades or cross-venue analysis.
Normalization removes parser differences. It does not remove market-specific rules, fees, account requirements or execution risk. Always read the contract terms and venue rules before treating two prices as comparable.
Frequently Asked Questions
Does the Kalshi order book API return asks?
The native binary-market response returns YES bids and NO bids. A NO bid at price p implies a YES ask at 1 - p, while a YES bid implies the complementary NO ask. Predictefy performs that transformation and exposes explicit bid and ask arrays for the selected outcome.
Does a Predictefy Kalshi order-book request use the market ticker?
No. Call fetchMarkets, select the required outcome from the returned market and pass its outcomeId to fetchOrderBook. The native ticker and parent market id identify the market rather than one side of it, and a binary market carries one book per outcome. Resolve the outcome first and the depth you get back is unambiguous.
Are prices returned in cents or probabilities?
Predictefy’s normalized price is a number from 0 to 1. A price of 0.42 represents 42 cents. Do not multiply or divide it again unless your display layer needs percentage or cent formatting. Venues differ in what they return natively, and normalizing to one scale is what lets the same comparison code run against all of them.
Can Python stream normalized Kalshi depth?
Poll fetch_order_book on an interval that matches how fast your markets move. The TypeScript SDK carries the WebSocket helpers for event-driven updates, so a Python service that needs push rather than pull typically pairs a polling loop with the streaming client, or reads the WebSocket surface directly.
Does visible depth guarantee a fill?
No. It is a snapshot of displayed liquidity. Orders can be filled, changed or canceled before yours reaches the venue. Include fees, latency, account eligibility and contract rules in any execution decision. Depth also moves as you consume it, so the price for your last contract is rarely the price for your first.