Kalshi Trade API v2 — Vendor Facts
Pinned from docs.kalshi.com on 2026-09-04. Do NOT re-fetch from the network (GUARDRAILS.md §1.4).
Base URLs
- Production:
https://external-api.kalshi.com/trade-api/v2 (alt: https://api.elections.kalshi.com/trade-api/v2)
- Demo:
https://external-api.demo.kalshi.co/trade-api/v2
- WebSocket (NOT used in this kit):
wss://external-api-ws.kalshi.com/trade-api/ws/v2
Authentication
Signature-based with RSA private key:
- Headers:
KALSHI-ACCESS-KEY (public key ID), KALSHI-ACCESS-TIMESTAMP (ms), KALSHI-ACCESS-SIGNATURE
- Signature algorithm: RSA-PSS(SHA256, MGF1-SHA256, salt length = digest length)
- Signed string:
f"{timestamp_ms}{METHOD}{path}" where path is URL path WITHOUT query string
- Example: sign
/trade-api/v2/portfolio/orders, not …?status=open
- Private key format: PEM-encoded
Market Payload
Fixed-point dollar strings for prices and sizes (some older payloads use integer cents; parse both):
yes_bid_dollars, yes_ask_dollars, no_bid_dollars, no_ask_dollars — top-of-book bids/asks
last_price_dollars — most recent trade price
*_fp fields — sizes (fixed-point, contract count)
rules_primary, rules_secondary — market rules text
close_time, expected_expiration_time, latest_expiration_time — timing fields
settlement_timer_seconds — dispute-window countdown
settlement_ts — settlement timestamp
result — one of {yes, no, scalar, ""} (empty = unresolved)
fee_waiver_expiration_time — optional fee waiver deadline
price_level_structure — tick sizes by price range
Order Book
GET /markets/{ticker}/orderbook returns:
{
"orderbook_fp": {
"yes_dollars": [[price, qty], ...],
"no_dollars": [[price, qty], ...]
}
}
- Bids only: Both fields are bid levels
- Derive asks: A NO bid at price q is a YES ask at 1−q
- Derivation:
yes_asks = 1 - no_dollars, no_asks = 1 - yes_dollars
Orders (V2)
Used for YES-side orders only (side="bid" to buy, side="ask" to sell —
see the Legacy section below for why NO-side orders and all cancels use a
different endpoint).
POST /portfolio/events/orders with:
ticker — market ticker
side — one of {bid, ask} (not BUY/SELL)
count — size, fixed-point string (contract count)
price — limit price, dollar string (2–4 decimal places)
time_in_force — one of {fill_or_kill, good_till_canceled, immediate_or_cancel}
self_trade_prevention_type — optional
client_order_id — optional, for idempotency
post_only — optional, post-only limit flag
expiration_time — optional
Response includes:
order_id, client_order_id, fill_count, remaining_count
average_fill_price, average_fee_paid
ts_ms — server timestamp (milliseconds)
Orders (Legacy — NO side and all cancels)
app/venues/kalshi/live.py also calls two legacy endpoints, both marked
verified=False in that file's ORDER_ROUTES table because GUARDRAILS.md
§1.4 forbids fetching kalshi.com/kalshi.co to confirm their exact
body/response shape — the field spellings below are a documented,
tested ASSUMPTION, not a vendor-pinned fact:
POST /portfolio/orders — used for NO-side orders instead of V2:
side="no" plus action ("buy" or "sell"). This is deliberate, not
a shortcut — buying/selling NO is a different contract from YES, not a
mirrored ask, and neither venue supports naked shorts, so routing
"buy NO" as a V2 YES ask would either be rejected or (worse) liquidate
an unrelated YES position the account happened to hold.
DELETE /portfolio/orders/{order_id} — used for ALL cancels, YES and
NO alike; there is no V2 cancel path in this adapter.
A wrong field name on either legacy call is expected to surface as a loud
4xx from the venue, not a silent misexecution.
Fees
Formula: fee = rate × C × P × (1 − P) where C = contracts, P = probability in [0, 1]
- Standard rates: Taker 0.07, Maker 0.0 (some series carry maker fees)
- Rounding:
trade_fee = ceil_6dp(model_fee) per fill, then ceil_2dp(net) for non-direct members
- Per-fill ceiling: Applies to each partial fill independently; multi-fill orders sum multiple ceilings
- Fee waiver: Some markets carry
fee_waiver_expiration_time (zero fee until deadline)
- Rates are NOT confirmed at implementation time (PLAN.md D11; the 0.07 default is historical)
Capital
- Account type: USD in CFTC-regulated FCM account
- Deposit/withdrawal: ACH/wire transfers (days, not instant)
- Fungibility: NOT fungible with Polymarket USDC in real time
- Transfer latency: Default
transfer_latency_hours=72
- Per-venue ledgers: Capital does not auto-move between venues; each venue has its own balance and position tracking
Key Implementation Notes
- The field name decides the encoding, never the magnitude: bare
yes/no is the LEGACY integer-cents format (divide by 100 to get a
probability); yes_dollars/no_dollars is the current fixed-point
dollar-string format (already a probability in [0,1], no
conversion). Both can appear regardless of whether the payload's top
level key was orderbook_fp or orderbook — check the field suffix
inside the container, not the container's own key name.
- Derive YES asks from NO bids and vice versa (orderbook is bids-only)
- Prices are probabilities in [0, 1]; no conversion to fixed-point in the adapter
- Sizes are contracts (1 = $1 at resolution); stored as float
- Fee model must round per-fill to 6 decimal places, then optionally to cents
- Signature must be computed at request time with fresh timestamp_ms
- REST polling only; WebSocket not implemented in this kit (raises
NotImplementedError)
1---2name: kalshi-api3description: Kalshi Trade API v2 integration facts, endpoints, and fee model. Reference for venue adapter implementation (app/venues/kalshi/).4---5
6# Kalshi Trade API v2 — Vendor Facts
7
8Pinned from docs.kalshi.com on 2026-09-04. Do NOT re-fetch from the network (GUARDRAILS.md §1.4).
9
10## Base URLs
11
12- **Production**: `https://external-api.kalshi.com/trade-api/v2` (alt: `https://api.elections.kalshi.com/trade-api/v2`)
13- **Demo**: `https://external-api.demo.kalshi.co/trade-api/v2`
14- **WebSocket** (NOT used in this kit): `wss://external-api-ws.kalshi.com/trade-api/ws/v2`
15
16## Authentication
17
18Signature-based with RSA private key:
19
20- Headers: `KALSHI-ACCESS-KEY` (public key ID), `KALSHI-ACCESS-TIMESTAMP` (ms), `KALSHI-ACCESS-SIGNATURE`
21- Signature algorithm: RSA-PSS(SHA256, MGF1-SHA256, salt length = digest length)
22- Signed string: `f"{timestamp_ms}{METHOD}{path}"` where `path` is URL path WITHOUT query string
23- Example: sign `/trade-api/v2/portfolio/orders`, not `…?status=open`
24- Private key format: PEM-encoded
25
26## Market Payload
27
28Fixed-point dollar strings for prices and sizes (some older payloads use integer cents; parse both):
29
30- `yes_bid_dollars`, `yes_ask_dollars`, `no_bid_dollars`, `no_ask_dollars` — top-of-book bids/asks
31- `last_price_dollars` — most recent trade price
32- `*_fp` fields — sizes (fixed-point, contract count)
33- `rules_primary`, `rules_secondary` — market rules text
34- `close_time`, `expected_expiration_time`, `latest_expiration_time` — timing fields
35- `settlement_timer_seconds` — dispute-window countdown
36- `settlement_ts` — settlement timestamp
37- `result` — one of `{yes, no, scalar, ""}` (empty = unresolved)
38- `fee_waiver_expiration_time` — optional fee waiver deadline
39- `price_level_structure` — tick sizes by price range
40
41## Order Book
42
43`GET /markets/{ticker}/orderbook` returns:
44
45```json
46{
47 "orderbook_fp": {
48 "yes_dollars": [[price, qty], ...],
49 "no_dollars": [[price, qty], ...]
50 }
51}
52```
53
54- **Bids only**: Both fields are bid levels
55- **Derive asks**: A NO bid at price q is a YES ask at 1−q
56- **Derivation**: `yes_asks = 1 - no_dollars`, `no_asks = 1 - yes_dollars`
57
58## Orders (V2)
59
60Used for YES-side orders only (`side="bid"` to buy, `side="ask"` to sell —
61see the Legacy section below for why NO-side orders and all cancels use a
62different endpoint).
63
64`POST /portfolio/events/orders` with:
65
66- `ticker` — market ticker
67- `side` — one of `{bid, ask}` (not BUY/SELL)
68- `count` — size, fixed-point string (contract count)
69- `price` — limit price, dollar string (2–4 decimal places)
70- `time_in_force` — one of `{fill_or_kill, good_till_canceled, immediate_or_cancel}`
71- `self_trade_prevention_type` — optional
72- `client_order_id` — optional, for idempotency
73- `post_only` — optional, post-only limit flag
74- `expiration_time` — optional
75
76Response includes:
77
78- `order_id`, `client_order_id`, `fill_count`, `remaining_count`
79- `average_fill_price`, `average_fee_paid`
80- `ts_ms` — server timestamp (milliseconds)
81
82## Orders (Legacy — NO side and all cancels)
83
84`app/venues/kalshi/live.py` also calls two legacy endpoints, both marked
85`verified=False` in that file's `ORDER_ROUTES` table because GUARDRAILS.md
86§1.4 forbids fetching kalshi.com/kalshi.co to confirm their exact
87body/response shape — the field spellings below are a documented,
88tested ASSUMPTION, not a vendor-pinned fact:
89
90- `POST /portfolio/orders` — used for NO-side orders instead of V2:
91 `side="no"` plus `action` (`"buy"` or `"sell"`). This is deliberate, not
92 a shortcut — buying/selling NO is a different contract from YES, not a
93 mirrored `ask`, and neither venue supports naked shorts, so routing
94 "buy NO" as a V2 YES `ask` would either be rejected or (worse) liquidate
95 an unrelated YES position the account happened to hold.
96- `DELETE /portfolio/orders/{order_id}` — used for ALL cancels, YES and
97 NO alike; there is no V2 cancel path in this adapter.
98
99A wrong field name on either legacy call is expected to surface as a loud
1004xx from the venue, not a silent misexecution.
101
102## Fees
103
104**Formula**: `fee = rate × C × P × (1 − P)` where C = contracts, P = probability in [0, 1]
105
106- **Standard rates**: Taker 0.07, Maker 0.0 (some series carry maker fees)
107- **Rounding**: `trade_fee = ceil_6dp(model_fee)` per fill, then `ceil_2dp(net)` for non-direct members
108- **Per-fill ceiling**: Applies to each partial fill independently; multi-fill orders sum multiple ceilings
109- **Fee waiver**: Some markets carry `fee_waiver_expiration_time` (zero fee until deadline)
110- **Rates are NOT confirmed at implementation time** (PLAN.md D11; the 0.07 default is historical)
111
112## Capital
113
114- **Account type**: USD in CFTC-regulated FCM account
115- **Deposit/withdrawal**: ACH/wire transfers (days, not instant)
116- **Fungibility**: NOT fungible with Polymarket USDC in real time
117- **Transfer latency**: Default `transfer_latency_hours=72`
118- **Per-venue ledgers**: Capital does not auto-move between venues; each venue has its own balance and position tracking
119
120## Key Implementation Notes
121
1221. The field name decides the encoding, never the magnitude: bare
123 `yes`/`no` is the LEGACY integer-cents format (divide by 100 to get a
124 probability); `yes_dollars`/`no_dollars` is the current fixed-point
125 dollar-string format (already a probability in `[0,1]`, no
126 conversion). Both can appear regardless of whether the payload's top
127 level key was `orderbook_fp` or `orderbook` — check the field suffix
128 inside the container, not the container's own key name.
1292. Derive YES asks from NO bids and vice versa (orderbook is bids-only)
1303. Prices are probabilities in [0, 1]; no conversion to fixed-point in the adapter
1314. Sizes are contracts (1 = $1 at resolution); stored as float
1325. Fee model must round per-fill to 6 decimal places, then optionally to cents
1336. Signature must be computed at request time with fresh timestamp_ms
1347. REST polling only; WebSocket not implemented in this kit (raises `NotImplementedError`)