Skip to main content

A professional Python SDK for the Kalshi prediction markets API

Project description

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 (99 operations across 19 resources, OpenAPI v3.20.0) and WebSocket API (11 typed subscribe_* channels + 2 escape-hatch).
  • V2 event-market orders: create_v2 / amend_v2 / decrease_v2 / cancel_v2 plus batched variants on /portfolio/events/orders/*. Legacy /portfolio/orders keeps 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 --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

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 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 (11 typed + 2 escape-hatch). Eleven 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. 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.

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.

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

kalshi_sdk-3.1.0.tar.gz (848.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kalshi_sdk-3.1.0-py3-none-any.whl (175.6 kB view details)

Uploaded Python 3

File details

Details for the file kalshi_sdk-3.1.0.tar.gz.

File metadata

  • Download URL: kalshi_sdk-3.1.0.tar.gz
  • Upload date:
  • Size: 848.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for kalshi_sdk-3.1.0.tar.gz
Algorithm Hash digest
SHA256 6811702ac2c21eddec3eaa4ed0a4ce4c9e52d2d3dc0137d2c8cf4d6469e72426
MD5 4eee466c7d20bfb9866bdcae666712db
BLAKE2b-256 5126c1d4eeaab9d378dbff2b2ea7cf8d7df7bf334cb769cdab55aba08f4022f3

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalshi_sdk-3.1.0.tar.gz:

Publisher: release.yml on TexasCoding/kalshi-python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kalshi_sdk-3.1.0-py3-none-any.whl.

File metadata

  • Download URL: kalshi_sdk-3.1.0-py3-none-any.whl
  • Upload date:
  • Size: 175.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for kalshi_sdk-3.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ec814ebc390c60eabf74171554c294fb89c4f5dfa19905b784355a2a99ed0d70
MD5 8ac5bebc6fc70bff5561370416081f0e
BLAKE2b-256 8527315cb69e8434accaefb1e678d22d647aa65c6703432863c8d32601c9113b

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalshi_sdk-3.1.0-py3-none-any.whl:

Publisher: release.yml on TexasCoding/kalshi-python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page