A professional Python SDK for the Kalshi prediction markets API
Project description
kalshi-sdk
A professional, spec-first Python SDK for the Kalshi prediction markets API.
- Full coverage of the Kalshi REST API (85 endpoints across 19 resources, OpenAPI v3.18.0) and WebSocket API (13 channels).
- V2 event-market orders:
create_v2/amend_v2/decrease_v2/cancel_v2plus batched variants on/portfolio/events/orders/*. Legacy/portfolio/orderskeeps working — deprecated no earlier than May 6, 2026. - 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
from kalshi import KalshiClient
with KalshiClient.from_env() as client:
order = client.orders.create(
ticker="EXAMPLE-25-T",
side="yes",
action="buy",
count=10,
yes_price="0.65", # 65 cents
time_in_force="good_till_canceled",
client_order_id="my-uuid", # idempotency key
)
print(order.order_id, order.status)
Prices are decimal dollars (e.g. "0.65") per the Kalshi spec. Internally
the SDK uses Decimal via the DollarDecimal type — never float.
Every POST/PUT/DELETE-with-body method also accepts a pre-built request model as an alternative to individual kwargs (useful for programmatic order construction):
from kalshi import CreateOrderRequest
client.orders.create(request=CreateOrderRequest(
ticker="EXAMPLE-25-T", side="yes", action="buy",
count=10, yes_price="0.65",
))
V2 event-market orders
Spec v3.18.0 introduced the V2 family on /portfolio/events/orders/* —
event-scoped semantics with single-book bid/ask sides and fixed-point
dollar prices. Legacy /portfolio/orders keeps working and will be
deprecated no earlier than May 6, 2026.
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"),
time_in_force="good_till_canceled",
self_trade_prevention_type="taker_at_cross",
))
print(resp.order_id, resp.remaining_count, resp.fill_count)
The V2 surface is model-only (no kwarg overload); pass a fully-constructed request model. See V2 orders docs for amend/decrease/batch variants.
WebSocket streaming
import asyncio
from kalshi import KalshiAuth, KalshiConfig
from kalshi.ws.client 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 (13): ticker, trade, orderbook_delta, fill,
market_positions, user_orders, order_group_updates,
market_lifecycle_v2, multivariate, multivariate_market_lifecycle,
communications, control_frames, root.
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_more:
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
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-2.2.0.tar.gz.
File metadata
- Download URL: kalshi_sdk-2.2.0.tar.gz
- Upload date:
- Size: 670.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a66e0ab3346755d5e739d0ffad3fadde19d0001ca41d9b08dc40ae99ed1c2ca6
|
|
| MD5 |
13b24580effc832c5053ee9b919d6933
|
|
| BLAKE2b-256 |
383d27914208463e81077839c08be308083b921635fa5960d74a0c8a8407f042
|
Provenance
The following attestation bundles were made for kalshi_sdk-2.2.0.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-2.2.0.tar.gz -
Subject digest:
a66e0ab3346755d5e739d0ffad3fadde19d0001ca41d9b08dc40ae99ed1c2ca6 - Sigstore transparency entry: 1575818188
- Sigstore integration time:
-
Permalink:
TexasCoding/kalshi-python-sdk@2319fee6fdd78d1746a88ce24182c6d95f11497c -
Branch / Tag:
refs/tags/v2.2.0 - Owner: https://github.com/TexasCoding
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2319fee6fdd78d1746a88ce24182c6d95f11497c -
Trigger Event:
push
-
Statement type:
File details
Details for the file kalshi_sdk-2.2.0-py3-none-any.whl.
File metadata
- Download URL: kalshi_sdk-2.2.0-py3-none-any.whl
- Upload date:
- Size: 125.2 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 |
79e43c662c4b91e04d7fd5e9ab2004595fa56e5631f01e28dd4ff2569d7d98ab
|
|
| MD5 |
ce1123d57614cf04cf236c4c7fa8f3d3
|
|
| BLAKE2b-256 |
4f72eff7058b998ed42035eda36c79c47b28e1b265956eccb4323be42628b875
|
Provenance
The following attestation bundles were made for kalshi_sdk-2.2.0-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-2.2.0-py3-none-any.whl -
Subject digest:
79e43c662c4b91e04d7fd5e9ab2004595fa56e5631f01e28dd4ff2569d7d98ab - Sigstore transparency entry: 1575818308
- Sigstore integration time:
-
Permalink:
TexasCoding/kalshi-python-sdk@2319fee6fdd78d1746a88ce24182c6d95f11497c -
Branch / Tag:
refs/tags/v2.2.0 - Owner: https://github.com/TexasCoding
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2319fee6fdd78d1746a88ce24182c6d95f11497c -
Trigger Event:
push
-
Statement type: