A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs
Project description
kalshi-sdk
A professional, spec-first Python SDK for the Kalshi prediction markets API.
- Full coverage of the Kalshi REST API (101 operations across 19 resources, OpenAPI v3.22.0) and WebSocket API (12 typed
subscribe_*channels + 2 escape-hatch). - Perps (margin) API: standalone
PerpsClient/AsyncPerpsClient+PerpsWebSocketfor the perpetual-futures exchange (34 REST operations, 6 WS channels), plus aKlearClientfor the Self-Clearing-Member "Klear" settlement API (9 operations). See Perps (margin) trading. - FIX protocol: an async-first FIX engine (FIXT.1.1 / FIX50SP2) for both products — order-entry, drop-copy, market-data, post-trade (prediction), and RFQ (prediction) sessions (plus order-group management over the order-entry session) with typed message models, sequence recovery, and order-book / settlement reassembly.
from kalshi import FixClient/MarginFixClient. See FIX protocol. - V2 event-market orders:
create_v2/amend_v2/decrease_v2/cancel_v2plus batched variants on/portfolio/events/orders/*— the only order-write surface. - Funding & cost introspection:
portfolio.deposits(),portfolio.withdrawals(),account.endpoint_costs(). - Sync and async clients sharing one transport — no thread-pool wrapping.
- Typed end-to-end: Pydantic v2 models,
mypy --strictclean, shipspy.typed.Literaltypes on fixed-enum kwargs. - Spec-aligned with drift guards: hard-fail contract tests catch query, body, and WebSocket payload drift on every commit.
- Safe defaults: only idempotent verbs (
GET/HEAD/OPTIONS) retry;POST/DELETEnever retry to avoid duplicate orders or cancels. - DataFrame-ready: optional
pandas/polarsextras for analysis workflows. - Offline-testable: record/replay mock transport (
kalshi.testing) for SDK consumers building integration tests.
📖 Full documentation: https://texascoding.github.io/kalshi-python-sdk/
Install
pip install kalshi-sdk
Requires Python 3.12+.
Quickstart — sync
from kalshi import KalshiClient
with KalshiClient(
key_id="your-key-id",
private_key_path="~/.kalshi/private_key.pem",
) as client:
page = client.markets.list(status="open", limit=10)
for market in page:
print(market.ticker, market.yes_bid, market.yes_ask)
Quickstart — async
import asyncio
from kalshi import AsyncKalshiClient
async def main() -> None:
async with AsyncKalshiClient(
key_id="your-key-id",
private_key_path="~/.kalshi/private_key.pem",
) as client:
# list_all() yields across pages — works directly with `async for`.
async for market in client.markets.list_all(status="open"):
print(market.ticker, market.yes_bid)
asyncio.run(main())
Authentication
Kalshi uses RSA-PSS request signing. Generate a key pair in your Kalshi account settings and download the PEM.
From environment variables
export KALSHI_KEY_ID="..."
export KALSHI_PRIVATE_KEY_PATH="~/.kalshi/private_key.pem"
# or, inline:
export KALSHI_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----..."
# Optional:
export KALSHI_DEMO=true # use the demo (sandbox) environment
export KALSHI_API_BASE_URL=... # override base URL
from kalshi import KalshiClient
client = KalshiClient.from_env()
from_env() returns an unauthenticated client if no credentials are set.
Public endpoints still work; private endpoints raise AuthRequiredError.
Demo vs production
KalshiClient(key_id="...", private_key_path="...", demo=True) # sandbox
KalshiClient(key_id="...", private_key_path="...") # production (default)
Public / unauthenticated usage
You don't need credentials to read public market data:
from kalshi import KalshiClient
with KalshiClient(demo=True) as client:
assert client.is_authenticated is False
markets = client.markets.list(status="open", limit=5)
Placing orders
Orders are written through the V2 event-market family on
/portfolio/events/orders/* — event-scoped semantics with single-book
bid/ask sides and fixed-point dollar prices.
import uuid
from decimal import Decimal
from kalshi import KalshiClient, CreateOrderV2Request
with KalshiClient.from_env() as client:
resp = client.orders.create_v2(request=CreateOrderV2Request(
ticker="EVENT-MKT",
client_order_id=str(uuid.uuid4()), # required + server idempotency key
side="bid", # BookSideLiteral: "bid" | "ask"
count=Decimal("10"),
price=Decimal("0.50"), # 50 cents
time_in_force="good_till_canceled",
self_trade_prevention_type="taker_at_cross",
))
print(resp.order_id, resp.remaining_count, resp.fill_count)
Prices and counts are Decimal — never float. Internally the SDK uses the
DollarDecimal type for prices (FixedPointDollars on the wire). side is the
book side ("bid" / "ask"), not "yes" / "no", and client_order_id is
required on CreateOrderV2Request (the server uses it for idempotency).
The V2 surface is model-only — there is no kwarg overload. Every write
takes a fully-constructed request model whose model_config = {"extra": "forbid"} rejects phantom keys at construction time. Cancel takes the order id
directly:
client.orders.cancel_v2(resp.order_id)
See V2 orders docs for amend_v2 / decrease_v2 / batch_create_v2 / batch_cancel_v2.
WebSocket streaming
import asyncio
from kalshi import KalshiAuth, KalshiConfig
from kalshi.ws import KalshiWebSocket
async def main() -> None:
auth = KalshiAuth.from_key_path("your-key-id", "~/.kalshi/private_key.pem")
config = KalshiConfig.demo() # or KalshiConfig.production()
ws = KalshiWebSocket(auth=auth, config=config)
async with ws.connect() as session:
stream = await session.subscribe_orderbook_delta(tickers=["EXAMPLE-25-T"])
async for msg in stream:
print(msg)
asyncio.run(main())
Available channels (12 typed + 2 escape-hatch). Twelve have dedicated
subscribe_* methods — subscribe_ticker, subscribe_trade,
subscribe_orderbook_delta, subscribe_fill, subscribe_market_positions,
subscribe_user_orders, subscribe_order_group,
subscribe_market_lifecycle, subscribe_multivariate,
subscribe_multivariate_lifecycle, subscribe_communications,
subscribe_cfbenchmarks_value. The
AsyncAPI-declared control_frames and root channels are reachable
through the generic subscribe(channel, ...) escape hatch. See
docs/websockets.md for the full
channel table.
Perps (margin) trading
Kalshi's Perps (perpetual futures / margin) API is a separate exchange on its
own host. The SDK exposes it through standalone PerpsClient / AsyncPerpsClient
(and PerpsWebSocket), reusing the same RSA-PSS auth as KalshiClient but with
separate API keys issued for the perps exchange — prod base URL
https://external-api.kalshi.com/trade-api/v2, demo
https://external-api.demo.kalshi.co/trade-api/v2.
from kalshi import PerpsClient
# Reads KALSHI_PERPS_KEY_ID + KALSHI_PERPS_PRIVATE_KEY[_PATH] (separate from KALSHI_*).
with PerpsClient.from_env(demo=True) as perps:
print(perps.exchange.status()) # is the margin exchange trading?
for m in perps.markets.list(status="active"):
print(m.ticker, m.bid, m.ask)
print(perps.margin.balance()) # per-subaccount balance breakdown
import asyncio
from kalshi import AsyncPerpsClient
async def main() -> None:
async with AsyncPerpsClient.from_env(demo=True) as perps:
async for order in perps.orders.list_all():
print(order.order_id, order.price, order.remaining_count)
asyncio.run(main())
Resource families on the perps client: exchange, markets, orders,
order_groups, portfolio, margin (balance/risk/fees), funding, transfers.
Real-time streaming via PerpsWebSocket — subscribe_ticker (carries
funding_rate + next_funding_time_ms), subscribe_orderbook_delta,
subscribe_trade, subscribe_fill, subscribe_user_orders,
subscribe_order_group.
Prices are DollarDecimal (FixedPointDollars, up to 6 decimals); counts are
FixedPointCount (2 decimals, _fp wire suffix). REST timestamps are RFC3339;
WebSocket timestamps are Unix epoch milliseconds (*_ms fields). The
Self-Clearing-Member "Klear" settlement API (margin reports, settlement balances,
obligations, withdrawals) is a third surface exposed via KlearClient, which uses
Bearer token auth (KlearClient(admin_user_id=..., access_token=...))
rather than RSA-PSS. Full guide: docs/perps.md.
FIX protocol (low-latency trading)
For persistent, low-latency sessions the SDK includes a hand-rolled, async-first
FIX engine (FIXT.1.1 / FIX50SP2) for both products: order-entry, drop-copy,
market-data, post-trade (prediction), and RFQ (prediction) sessions — plus
order-group management over the order-entry session — with typed message models,
automatic logon/heartbeat/sequence recovery, and order-book / settlement
reassembly. It reuses the same RSA-PSS key as the REST client (KALSHI_* for
prediction, KALSHI_PERPS_* for margin).
import asyncio
from decimal import Decimal
from kalshi import FixClient, FixEnvironment
from kalshi.fix import NewOrderSingle, ExecutionReport, Side, decode_app_message
async def main() -> None:
async def on_message(raw) -> None:
msg = decode_app_message(raw)
if isinstance(msg, ExecutionReport):
print(msg.cl_ord_id, msg.exec_type, msg.ord_status)
client = FixClient.from_env(environment=FixEnvironment.DEMO)
async with client.order_entry(on_message=on_message) as session:
await session.send(NewOrderSingle(
cl_ord_id="order-1", symbol="KXNBAGAME-26MAY25NYKCLE-NYK",
side=Side.BUY_YES, order_qty=Decimal("10"), price=Decimal("0.55"),
))
await asyncio.sleep(2)
asyncio.run(main())
Prediction uses FixClient; margin uses MarginFixClient. Full guide:
docs/fix.md.
Error handling
All SDK errors inherit from KalshiError:
from kalshi import (
KalshiError,
KalshiAuthError, # 401 / 403
AuthRequiredError, # called private endpoint without credentials
KalshiNotFoundError, # 404
KalshiValidationError, # 400 (has .details: dict[str, str])
KalshiRateLimitError, # 429 (has .retry_after: float | None)
KalshiServerError, # 5xx
# WebSocket-specific:
KalshiWebSocketError,
KalshiConnectionError,
KalshiSequenceGapError,
KalshiBackpressureError,
KalshiSubscriptionError,
)
try:
client.markets.get("DOES-NOT-EXIST")
except KalshiNotFoundError as e:
print(e.status_code, str(e))
Retry policy
- Retries on
429,502,503,504,500(idempotent GET only). POSTandDELETEare never retried — duplicate order / cancel risk.- Exponential backoff with jitter, capped at
retry_max_delay. Retry-Afteris honored but capped atretry_max_delayto prevent a server-controlled stall.
Tune via KalshiConfig:
from kalshi import KalshiClient, KalshiConfig
config = KalshiConfig(
timeout=10.0,
max_retries=5,
retry_base_delay=0.5,
retry_max_delay=15.0,
# Connection pool / HTTP-2 tuning (opt-in; defaults preserve v1 behavior)
http2=False,
limits=None, # httpx.Limits(max_connections=..., keepalive_expiry=...)
extra_headers={"X-My-Tag": "foo"},
)
client = KalshiClient(key_id="...", private_key_path="...", config=config)
Pagination
List endpoints return a Page[T] you can iterate, plus a cursor for manual
control. For "give me everything" use list_all():
# Manual cursor loop:
page = client.markets.list(status="open", limit=200)
while True:
for market in page:
...
if not page.has_next:
break
page = client.markets.list(status="open", limit=200, cursor=page.cursor)
# Or just:
for market in client.markets.list_all(status="open"):
...
# Need a hard cap on pages (e.g. preview / quick sample)?
for market in client.markets.list_all(status="open", max_pages=5):
...
*_all() iterates until the server returns no cursor by default. Pass
max_pages=N for an explicit bound; passing 0 raises ValueError.
Page[T] also converts to a DataFrame when the optional extras are installed:
pip install 'kalshi-sdk[pandas]' # or [polars] or [all]
df = client.markets.list(status="open", limit=100).to_dataframe()
# Decimal and datetime preserved as native types in object columns.
Testing against the SDK (no live API)
For SDK consumers who want offline integration tests, kalshi.testing ships
record-and-replay transports:
from kalshi import KalshiClient
from kalshi.testing import RecordingTransport, ReplayTransport
# Record once against the real demo API:
with KalshiClient.from_env(transport=RecordingTransport("fixtures")) as c:
c.exchange.status()
# Replay in tests — no network:
with KalshiClient(transport=ReplayTransport("fixtures")) as c:
c.exchange.status() # served from fixtures/GET_*.json
Fixtures are JSON; the fingerprint ignores KALSHI-ACCESS-SIGNATURE and
KALSHI-ACCESS-TIMESTAMP so signature drift between record and replay does not
break matching. Always .gitignore the fixture directory when recording
against an authenticated account — fixtures contain the full response body
(balances, positions, PII).
Resources
| Documentation site | https://texascoding.github.io/kalshi-python-sdk/ |
| Kalshi REST OpenAPI spec | https://docs.kalshi.com/openapi.yaml |
| Kalshi WebSocket AsyncAPI spec | https://docs.kalshi.com/asyncapi.yaml |
| Production base URL | https://api.elections.kalshi.com/trade-api/v2 |
| Demo base URL | https://demo-api.kalshi.co/trade-api/v2 |
| Changelog | CHANGELOG.md |
| Issues | https://github.com/TexasCoding/kalshi-python-sdk/issues |
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kalshi_sdk-5.0.1.tar.gz.
File metadata
- Download URL: kalshi_sdk-5.0.1.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
012e28875c064041fa8dc77026ff445eb6344681142b8d6c891598682363b75e
|
|
| MD5 |
eb8ab2ace6d5e1ddc551c2d137fa936e
|
|
| BLAKE2b-256 |
695fda566281a10f82716d2ae08c94a0db42b32d4756b8414d6c11e866b02521
|
Provenance
The following attestation bundles were made for kalshi_sdk-5.0.1.tar.gz:
Publisher:
release.yml on TexasCoding/kalshi-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kalshi_sdk-5.0.1.tar.gz -
Subject digest:
012e28875c064041fa8dc77026ff445eb6344681142b8d6c891598682363b75e - Sigstore transparency entry: 1982687483
- Sigstore integration time:
-
Permalink:
TexasCoding/kalshi-python-sdk@16442c2cb4f8307f6f08cb271a3d29463f6439e1 -
Branch / Tag:
refs/tags/v5.0.1 - Owner: https://github.com/TexasCoding
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@16442c2cb4f8307f6f08cb271a3d29463f6439e1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file kalshi_sdk-5.0.1-py3-none-any.whl.
File metadata
- Download URL: kalshi_sdk-5.0.1-py3-none-any.whl
- Upload date:
- Size: 344.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b977a3e3f4401fc536f82a7711b252c6f2a158536152db511ead57e769bed082
|
|
| MD5 |
152e378ba87e5ac865e4bb1089937af3
|
|
| BLAKE2b-256 |
e03ec5e49fbaa6c866ae79188154ae3a08fcfe5555d531f680b1a20a8b9e3022
|
Provenance
The following attestation bundles were made for kalshi_sdk-5.0.1-py3-none-any.whl:
Publisher:
release.yml on TexasCoding/kalshi-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kalshi_sdk-5.0.1-py3-none-any.whl -
Subject digest:
b977a3e3f4401fc536f82a7711b252c6f2a158536152db511ead57e769bed082 - Sigstore transparency entry: 1982687832
- Sigstore integration time:
-
Permalink:
TexasCoding/kalshi-python-sdk@16442c2cb4f8307f6f08cb271a3d29463f6439e1 -
Branch / Tag:
refs/tags/v5.0.1 - Owner: https://github.com/TexasCoding
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@16442c2cb4f8307f6f08cb271a3d29463f6439e1 -
Trigger Event:
push
-
Statement type: