Skip to content

TexasCoding/kalshi-python-sdk

Repository files navigation

kalshi-sdk

Kalshi Python SDK — Trade the Future · Full REST + WebSocket · Sync + Async

A professional, spec-first Python SDK for the Kalshi prediction markets API.

PyPI version Python versions License: MIT Type checked: mypy strict

  • 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 + PerpsWebSocket for the perpetual-futures exchange (34 REST operations, 6 WS channels), plus a KlearClient for 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_v2 plus 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 --strict clean, ships py.typed. Literal types 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/DELETE never retry to avoid duplicate orders or cancels.
  • DataFrame-ready: optional pandas / polars extras 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 PerpsWebSocketsubscribe_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).
  • POST and DELETE are never retried — duplicate order / cancel risk.
  • Exponential backoff with jitter, capped at retry_max_delay.
  • Retry-After is honored but capped at retry_max_delay to 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.

About

Professional Python SDK for the Kalshi prediction markets API

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages