Skip to content

API Reference

Auto-generated from docstrings on the public surface of the kalshi package.

For narrative coverage of any of these, follow the section links — every class here also has a dedicated user-facing page elsewhere in the docs.

Clients

kalshi.client.KalshiClient

KalshiClient(
    *,
    key_id: str | None = None,
    private_key_path: str | Path | None = None,
    private_key: str | bytes | None = None,
    auth: KalshiAuth | None = None,
    config: KalshiConfig | None = None,
    demo: bool = False,
    base_url: str | None = None,
    timeout: float | None = None,
    max_retries: int | None = None,
    transport: BaseTransport | None = None
)

Synchronous client for the Kalshi prediction markets API.

Usage

with KalshiClient(key_id="...", private_key_path="~/.kalshi/key.pem") as client: markets = client.markets.list(status="open") for market in markets: print(market.ticker, market.yes_ask)

Or without context manager

client = KalshiClient(key_id="...", private_key_path="~/.kalshi/key.pem") try: markets = client.markets.list() finally: client.close()

is_authenticated property

is_authenticated: bool

Whether this client has auth credentials configured.

from_env classmethod

from_env(
    **kwargs: Unpack[ClientInitKwargs],
) -> KalshiClient

Create client from environment variables.

Reads

KALSHI_KEY_ID (optional — omit for unauthenticated access) KALSHI_PRIVATE_KEY (PEM string) or KALSHI_PRIVATE_KEY_PATH (file path) KALSHI_API_BASE_URL (optional, overrides base_url) KALSHI_DEMO (optional, "true" for demo environment)

Returns an unauthenticated client if no credentials are configured.

close

close() -> None

Close the underlying HTTP connection pool.

kalshi.async_client.AsyncKalshiClient

AsyncKalshiClient(
    *,
    key_id: str | None = None,
    private_key_path: str | Path | None = None,
    private_key: str | bytes | None = None,
    auth: KalshiAuth | None = None,
    config: KalshiConfig | None = None,
    demo: bool = False,
    base_url: str | None = None,
    timeout: float | None = None,
    max_retries: int | None = None,
    transport: AsyncBaseTransport | None = None
)

Asynchronous client for the Kalshi prediction markets API.

Usage

async with AsyncKalshiClient(key_id="...", private_key_path="~/.kalshi/key.pem") as client: markets = await client.markets.list(status="open") for market in markets: print(market.ticker, market.yes_ask)

is_authenticated property

is_authenticated: bool

Whether this client has auth credentials configured.

ws property

ws: KalshiWebSocket

WebSocket client for real-time streaming.

.. note:: Each access returns a new KalshiWebSocket instance. Per-instance state (callbacks registered via .on(), pending subscriptions) does not carry across accesses — assign the property to a local variable if you need to share state across multiple operations::

    ws = client.ws  # capture once
    @ws.on("ticker_v2")
    async def handle(msg): ...
    async with ws.connect() as session: ...

The sync ``KalshiClient`` does not expose ``.ws``; WebSocket
access is async-only.

Usage::

async with client.ws.connect() as session:
    async for msg in session.subscribe_ticker(tickers=["ECON-GDP-25Q1"]):
        print(msg.msg.yes_bid)

from_env classmethod

from_env(
    **kwargs: Unpack[ClientInitKwargs],
) -> AsyncKalshiClient

Create async client from environment variables.

Reads

KALSHI_KEY_ID (optional — omit for unauthenticated access) KALSHI_PRIVATE_KEY (PEM string) or KALSHI_PRIVATE_KEY_PATH (file path) KALSHI_API_BASE_URL (optional, overrides base_url) KALSHI_DEMO (optional, "true" for demo environment)

Returns an unauthenticated client if no credentials are configured.

close async

close() -> None

Close the underlying async HTTP connection pool.

Configuration

kalshi.config.KalshiConfig dataclass

KalshiConfig(
    base_url: str = PRODUCTION_BASE_URL,
    timeout: float = DEFAULT_TIMEOUT,
    max_retries: int = DEFAULT_MAX_RETRIES,
    retry_base_delay: float = 0.5,
    retry_max_delay: float = 30.0,
    total_timeout: float | None = None,
    extra_headers: Mapping[str, str] = dict(),
    ws_base_url: str = PRODUCTION_WS_URL,
    ws_max_retries: int = DEFAULT_WS_MAX_RETRIES,
    http2: bool = False,
    limits: Limits | None = None,
    ws_ping_interval: float = 20.0,
    ws_close_timeout: float = 5.0,
    rest_json_loads: Callable[[bytes], Any] | None = None,
    ws_json_loads: (
        Callable[[bytes | str], Any] | None
    ) = None,
    ws_json_dumps: (
        Callable[[Any], bytes | str] | None
    ) = None,
    allow_unknown_host: bool = False,
)

Client configuration.

Attributes:

Name Type Description
base_url str

API base URL. Defaults to production.

timeout float

Request timeout in seconds. Defaults to 30.

max_retries int

Max retry attempts for transient errors. Defaults to 3.

retry_base_delay float

Base delay in seconds for exponential backoff. Defaults to 0.5.

retry_max_delay float

Maximum delay in seconds for backoff. Defaults to 30.

total_timeout float | None

Hard cap on cumulative time spent inside a single request including retries, in seconds. None disables (legacy behaviour: retry until max_retries exhausted regardless of wall-clock).

http2 bool

Enable HTTP/2 for REST requests. Off by default for compat. Requires the h2 package (install httpx[http2] or h2).

limits Limits | None

Custom httpx.Limits for connection pool tuning. None uses httpx defaults.

ws_ping_interval float

Interval (s) between WS keepalive pings. Default 20.

ws_close_timeout float

Time (s) to wait for graceful WS close handshake. Default 5.

rest_json_loads Callable[[bytes], Any] | None

Optional callable used to parse REST response bodies. None falls back to :func:httpx.Response.json (stdlib :func:json.loads). Set to e.g. orjson.loads for ~2-3x faster parsing on list endpoints (markets.list_all, trades.list_all, fcm.orders).

ws_json_loads Callable[[bytes | str], Any] | None

Optional callable used to parse WS frames. None falls back to :func:json.loads. Set to e.g. orjson.loads for ~2-3x faster recv-loop parsing on high-volume streams.

ws_json_dumps Callable[[Any], bytes | str] | None

Optional callable used to serialize outbound WS commands. None falls back to :func:json.dumps.

production classmethod

production(**kwargs: object) -> KalshiConfig

Create config for Kalshi production environment.

demo classmethod

demo(**kwargs: object) -> KalshiConfig

Create config for Kalshi demo/sandbox environment.

Authentication

kalshi.auth.KalshiAuth

KalshiAuth(key_id: str, private_key: RSAPrivateKey)

RSA-PSS request signer for the Kalshi API.

The private key is loaded once at construction time and cached in memory. Thread-safe: RSA signing is stateless.

private_key property

private_key: RSAPrivateKey

The loaded RSA private key.

Exposed (read-only) so other signing surfaces in the SDK can reuse the same key without re-loading the PEM — notably the FIX logon signer (:class:kalshi.fix.auth.FixSigner), which signs a different payload (the FIX Logon pre-hash string) with the identical RSA-PSS scheme.

from_key_path classmethod

from_key_path(
    key_id: str,
    key_path: str | Path,
    *,
    password: (
        bytes | str | Callable[[], bytes | str] | None
    ) = None
) -> KalshiAuth

Load auth from a PEM private key file.

Supports ~ expansion (e.g., ~/kalshi.pem).

password accepts str / bytes / a zero-arg callable that returns either (the callable form lets you defer fetching the secret from a vault until load-time).

from_pem classmethod

from_pem(
    key_id: str,
    pem_data: str | bytes,
    *,
    password: (
        bytes | str | Callable[[], bytes | str] | None
    ) = None
) -> KalshiAuth

Load auth from PEM-encoded private key content.

password accepts str / bytes / a zero-arg callable that returns either; pass None (default) for an unencrypted PEM.

from_env classmethod

from_env(
    *,
    password: (
        bytes | str | Callable[[], bytes | str] | None
    ) = None
) -> KalshiAuth

Load auth from environment variables.

Reads

KALSHI_KEY_ID (required) KALSHI_PRIVATE_KEY (PEM string) or KALSHI_PRIVATE_KEY_PATH (file path) KALSHI_PRIVATE_KEY_PASSPHRASE (optional; used when the PEM is encrypted)

The explicit password= keyword overrides KALSHI_PRIVATE_KEY_PASSPHRASE when both are supplied.

try_from_env classmethod

try_from_env(
    *,
    password: (
        bytes | str | Callable[[], bytes | str] | None
    ) = None
) -> KalshiAuth | None

Load auth from environment variables, returning None if not configured.

Returns None if KALSHI_KEY_ID is not set, or if neither KALSHI_PRIVATE_KEY nor KALSHI_PRIVATE_KEY_PATH is set.

Note: Does not raise on missing variables, but may still raise KalshiAuthError if variables are set with invalid data (e.g., malformed PEM content or nonexistent key file path).

See :meth:from_env for the password / KALSHI_PRIVATE_KEY_PASSPHRASE semantics.

sign_request

sign_request(
    method: str, path: str, timestamp_ms: int | None = None
) -> dict[str, str]

Sign a request and return the auth headers.

Parameters:

Name Type Description Default
method str

HTTP method (GET, POST, DELETE, etc.)

required
path str

Full API path (e.g., /trade-api/v2/markets). Query strings and URL fragments are stripped.

required
timestamp_ms int | None

Unix timestamp in milliseconds. Auto-generated if None.

None

Returns:

Type Description
dict[str, str]

Dict with KALSHI-ACCESS-KEY, KALSHI-ACCESS-SIGNATURE, KALSHI-ACCESS-TIMESTAMP.

sign_request_async async

sign_request_async(
    method: str, path: str, timestamp_ms: int | None = None
) -> dict[str, str]

Async variant of :meth:sign_request that offloads to a dedicated executor.

Uses loop.run_in_executor(self._sign_executor, ...) so the ~1-10 ms of RSA-PSS CPU does not block the asyncio event loop. The executor is dedicated (not asyncio's default pool) so signs don't queue behind getaddrinfo / file I/O / other to_thread() work — relevant during WS reconnect storms where DNS dominates.

The sync :meth:sign_request API is unchanged; callers running on the sync transport should keep using it.

close

close() -> None

Shut down the sign-offload executor and mark this auth closed.

Idempotent. Safe to call without an executor having been initialised. Long-lived clients ordinarily rely on interpreter-shutdown cleanup (atexit-joined executor threads); this hook is for tests and other callers that want deterministic teardown.

Terminal: a subsequent :meth:sign_request_async raises RuntimeError rather than silently respawning a fresh executor. The synchronous :meth:sign_request is unaffected — it never used the executor — but callers should treat the instance as retired.

Errors

See Errors — that page is the canonical autodoc reference for every exception class.

Pagination

kalshi.models.common.Page

Bases: BaseModel, Generic[T]

A page of results from a list endpoint.

Iterable over items. Also exposes cursor metadata for manual pagination.

Usage

page = client.markets.list(limit=50) for item in page: # iterate items process(item) print(page.cursor) # access cursor print(page.has_next) # check if more pages exist

to_dataframe

to_dataframe() -> pandas.DataFrame

Return page items as a pandas DataFrame (requires kalshi-sdk[pandas]).

to_polars

to_polars() -> polars.DataFrame

Return page items as a polars DataFrame (requires kalshi-sdk[polars]).

Custom types

kalshi.types

Custom types and Pydantic field helpers for the Kalshi SDK.

DollarDecimal module-attribute

DollarDecimal = Annotated[
    Decimal,
    BeforeValidator(_coerce_decimal),
    PlainSerializer(
        _decimal_to_str, return_type=str, when_used="json"
    ),
]

A Decimal field that handles bidirectional conversion for Kalshi dollar values.

  • Parse: Accepts str/int/float/Decimal, converts via Decimal(str(value))
  • Serialize: Outputs string representation for API requests

FixedPointCount module-attribute

FixedPointCount = Annotated[
    Decimal,
    BeforeValidator(_coerce_decimal),
    PlainSerializer(
        _decimal_to_str, return_type=str, when_used="json"
    ),
]

A Decimal field that handles bidirectional conversion for Kalshi count/volume values.

  • Parse: Accepts str/int/float/Decimal, converts via Decimal(str(value))
  • Serialize: Outputs string representation for API requests

MultiplierDecimal module-attribute

MultiplierDecimal = Annotated[
    Decimal,
    BeforeValidator(_coerce_decimal),
    PlainSerializer(
        _decimal_to_str, return_type=str, when_used="json"
    ),
]

A Decimal field for fee/rate multipliers (e.g. Series.fee_multiplier).

Wire shape is type: number, format: double per spec. Using the bare float annotation commits the value to binary float at parse time (0.65 -> 0.65000000000000002...); using bare Decimal skips _coerce_decimal entirely and routes JSON numbers through float. This alias applies the same coercion as :data:DollarDecimal / :data:FixedPointCount so multipliers honor the #225 invariant.

NullableList module-attribute

NullableList = Annotated[
    list[T], BeforeValidator(_none_to_empty_list)
]

A list[T] that tolerates server-returned null by coercing to [].

Use on response-model fields (extra='allow') whose spec says 'array' but where the live API has been observed to return null. Do NOT use on request bodies — those should enforce strict validation.

UnixSecondsTimestamp module-attribute

UnixSecondsTimestamp = Annotated[
    int, "Unix epoch seconds (int64) per OpenAPI spec"
]

Server-side format: int64 Unix-seconds timestamp.

Same wire shape as plain int; the alias exists to make it obvious at the field site that the value is seconds (not milliseconds, not a datetime). Callers wanting a :class:datetime.datetime can use datetime.fromtimestamp(value, tz=timezone.utc).

StrictInt module-attribute

StrictInt = Annotated[
    int, BeforeValidator(_reject_bool_int)
]

int that rejects bool — use on every Request-model integer field. See #295.

OrderPrice module-attribute

OrderPrice = Annotated[
    Decimal,
    BeforeValidator(_coerce_decimal),
    AfterValidator(_ensure_request_price),
    PlainSerializer(
        _decimal_to_str, return_type=str, when_used="json"
    ),
]

Request-side dollar price with sign and $0.0001 tick guards (#343).

Same wire shape as :data:DollarDecimal (BeforeValidator coerces to Decimal without float drift, PlainSerializer emits a fixed-point string on mode="json"), but adds an :class:AfterValidator that rejects negatives and sub-tick precision. Use on CreateOrderRequest / AmendOrderRequest / V2 equivalents so a caller passing Decimal('-0.65') or Decimal('0.12345') fails at construction instead of round-tripping to a server 400.

to_decimal

to_decimal(value: int | float | str | Decimal) -> Decimal

Convert a user-supplied price/count value to Decimal safely.

Always goes through str() to avoid float representation errors. e.g., to_decimal(0.65) returns Decimal("0.65"), not Decimal(0.65).

Rejects bool inputs (#225 pattern) and non-finite values (NaN/Infinity); delegates to :func:_coerce_decimal so the public helper and the :data:DollarDecimal / :data:FixedPointCount validators share a single guard (#325).

Request models

kalshi.models.orders.CreateOrderV2Request

Bases: BaseModel

Body for POST /portfolio/events/orders — the event-market order API.

Notable shape:

  • side is BookSideLiteral (bid/ask). V2 narrows the type on the model itself since there is no kwarg overload at the resource-method boundary (the V2 surface is model-only).
  • client_order_id is required by this model because the server uses it for idempotency on V2 orders. (OpenAPI v3.20.0 relaxed it to optional server-side, but the SDK keeps it required by design so every V2 order is idempotent.)
  • Price is a single price field (dollars).

kalshi.models.orders.AmendOrderV2Request

Bases: BaseModel

Body for POST /portfolio/events/orders/{order_id}/amend.

kalshi.models.orders.DecreaseOrderV2Request

Bases: BaseModel

Body for POST /portfolio/events/orders/{order_id}/decrease.

Spec marks all fields optional but server requires exactly one of reduce_by or reduce_to. Enforced at construction.

kalshi.models.orders.BatchCreateOrdersV2Request

Bases: BaseModel

Body for POST /portfolio/events/orders/batched.

kalshi.models.orders.BatchCancelOrdersV2Request

Bases: BaseModel

Body for DELETE /portfolio/events/orders/batched.

kalshi.models.orders.BatchCancelOrdersV2RequestOrder

Bases: BaseModel

Single entry in BatchCancelOrdersV2Request.orders.

kalshi.models.api_keys.CreateApiKeyRequest

Bases: BaseModel

Body for POST /api_keys — register a caller-supplied public key.

public_key must be a PEM-encoded RSA public key (i.e. starts with -----BEGIN PUBLIC KEY-----). scopes defaults to full access (["read", "write"]) server-side when omitted. If "write" is included, "read" must be too.

kalshi.models.api_keys.GenerateApiKeyRequest

Bases: BaseModel

Body for POST /api_keys/generate — let Kalshi mint a key pair.

kalshi.models.communications.CreateRFQRequest

Bases: BaseModel

Body for POST /communications/rfqs.

Spec allows contracts (integer) or contracts_fp (fixed-point string); SDK commits to the integer form. Target cost uses _dollars wire suffix. target_cost_centi_cents is deprecated upstream — omitted here.

Spec only requires market_ticker and rest_remainder; contracts and target_cost are both optional and may both be omitted (a "shopping around" RFQ with no committed size or cost). The SDK does not enforce a contracts-or-cost invariant because the spec doesn't.

kalshi.models.communications.CreateQuoteRequest

Bases: BaseModel

Body for POST /communications/quotes.

Unlike order/amend requests, the spec names the wire fields yes_bid / no_bid (no _dollars suffix) for this request. The Quote response fields, however, use the _dollars suffix — handled on the response model.

kalshi.models.communications.AcceptQuoteRequest

Bases: BaseModel

Body for PUT /communications/quotes/{quote_id}/accept.

kalshi.models.communications.ProposeBlockTradeRequest

Bases: BaseModel

Body for POST /communications/block-trade-proposals.

price_centi_cents (centi-cents) and centicount (centicounts) are plain integers per spec, both minimum: 1. buyer_subtrader_id / buyer_subaccount are mutually exclusive (same for the seller pair), but the spec does not encode the exclusivity, so the SDK does not enforce it.

kalshi.models.communications.AcceptBlockTradeProposalRequest

Bases: BaseModel

Body for POST /communications/block-trade-proposals/{id}/accept.

Both fields are optional (spec requestBody required: false); accept as primary by sending an empty body. subtrader_id / subaccount are mutually exclusive but the spec does not encode it, so it is not enforced.

kalshi.models.multivariate.CreateMarketInMultivariateEventCollectionRequest

Bases: BaseModel

Parameters for POST /multivariate_event_collections/{collection_ticker}.

Matches spec components.schemas.CreateMarketInMultivariateEventCollectionRequest. Required: selected_markets. Optional: with_market_payload.

Carve-out: extra="forbid" on this model rejects unknown top-level keys but NOT unknown keys inside each TickerPair in selected_marketsTickerPair itself is extra="allow" (see its docstring for why). Phantom keys nested inside a TickerPair currently pass through to the wire. Tracked as a v0.9 follow-up.

See kalshi.resources.multivariate.MultivariateCollectionsResource.create_market — v0.8.0 builds this model internally; method signature unchanged.

kalshi.models.multivariate.LookupTickersForMarketInMultivariateEventCollectionRequest

Bases: BaseModel

Parameters for PUT /multivariate_event_collections/{collection_ticker}/lookup.

Matches spec components.schemas.LookupTickersForMarketInMultivariateEventCollectionRequest. Only selected_markets, required.

Carve-out: extra="forbid" on this model rejects unknown top-level keys but NOT unknown keys inside each TickerPair in selected_marketsTickerPair itself is extra="allow" (see its docstring for why). Phantom keys nested inside a TickerPair currently pass through to the wire. Tracked as a v0.9 follow-up.

See kalshi.resources.multivariate.MultivariateCollectionsResource.lookup_tickers — v0.8.0 builds this model internally; method signature unchanged.

kalshi.models.order_groups.CreateOrderGroupRequest

Bases: BaseModel

Create body. SDK sends integer form; spec's contracts_limit_fp string variant unused.

kalshi.models.order_groups.UpdateOrderGroupLimitRequest

Bases: BaseModel

Update-limit body. No subaccount — spec omits SubaccountQuery on /limit.

kalshi.models.subaccounts.ApplySubaccountTransferRequest

Bases: BaseModel

Body for POST /portfolio/subaccounts/transfer.

amount_cents is integer cents per spec (e.g. 500 for $5.00, never a Decimal). from_subaccount / to_subaccount use 0 for the primary account and a positive integer for numbered subaccounts. The server is the source of truth for the upper bound: spec describes 1-63 in prose but defines no JSON-schema maximum, and demo has been observed allocating values above 32. The SDK validates only the lower bound (ge=0) so server-assigned numbers always round-trip.

kalshi.models.subaccounts.UpdateSubaccountNettingRequest

Bases: BaseModel

Body for PUT /portfolio/subaccounts/netting.

Response models

Markets, events, series

kalshi.models.markets

Market-related models.

MarketStatusLiteral module-attribute

MarketStatusLiteral = Literal[
    "unopened", "open", "paused", "closed", "settled"
]

Market status filter for GET /markets. Spec: MarketStatusQuery enum.

MveFilterLiteral module-attribute

MveFilterLiteral = Literal['only', 'exclude']

Multivariate-event filter for GET /markets. Spec: MveFilterQuery enum.

Market

Bases: BaseModel

A Kalshi prediction market.

Price fields accept both _dollars-suffixed names from the API (e.g. yes_bid_dollars) and short names (e.g. yes_bid). Volume/count fields accept both _fp-suffixed names and short names.

OrderbookLevel

Bases: BaseModel

A single price level in the orderbook.

Quantity is FixedPointCount — fractional contract counts are sent as FixedPointCount strings like "100.50" from the API.

Orderbook

Bases: BaseModel

Orderbook for a market.

BidAskDistribution

Bases: BaseModel

OHLC data for bid/ask prices within a candlestick period.

PriceDistribution

Bases: BaseModel

OHLC data for trade prices within a candlestick period.

Fields are nullable because there may be no trades in a period.

Candlestick

Bases: BaseModel

A candlestick data point for a market.

The API returns nested OHLC objects for yes_bid, yes_ask, and price, plus volume and open interest as FixedPointCount strings.

MarketCandlesticks

Bases: BaseModel

Per-market candlestick bundle in a bulk response.

Spec schema MarketCandlesticksResponse — wraps a ticker and its candlesticks. The outer bulk response is {markets: [...]}.

GetMarketCandlesticksResponse

Bases: BaseModel

Spec GetMarketCandlesticksResponse — single-market candlesticks envelope.

ticker and candlesticks are spec-required. candlesticks uses :data:~kalshi.types.NullableList so a missing key hard-fails (surfacing spec drift instead of silently returning []) while a null value — Kalshi's observed "empty-as-null" convention — coerces to [] (the prior data.get(...) extraction would TypeError on a null array).

BatchGetMarketCandlesticksResponse

Bases: BaseModel

Spec BatchGetMarketCandlesticksResponse — bulk-candlesticks envelope.

markets is spec-required (NullableList: missing key -> error, null -> []).

GetMarketOrderbooksResponse

Bases: BaseModel

Spec GetMarketOrderbooksResponse — bulk-orderbooks envelope.

orderbooks is spec-required (NullableList: missing key -> error, null -> []). Items stay raw dicts — the resource transforms each via _orderbook_from_item (the yes/no FixedPoint parsing it already owns).

kalshi.models.events

Event-related models.

EventStatusLiteral module-attribute

EventStatusLiteral = Literal[
    "unopened", "open", "closed", "settled"
]

Event status filter for GET /events. Spec: GetEvents.status query enum.

SettlementSource

Bases: BaseModel

A settlement source for an event.

Event

Bases: BaseModel

A Kalshi event (container for one or more markets).

EventFeeChange

Bases: BaseModel

A scheduled event-level fee override (GET /events/fee_changes).

Event fees layer on top of the parent series' fee structure. fee_type_override and fee_multiplier_override are both None when the override has been cleared (the event falls back to the series fee). Both are spec-required keys (EventFeeChange.required) but nullable — the key must be present; None is a meaningful "override cleared" signal, so neither carries a default.

MarketMetadata

Bases: BaseModel

Metadata for a market within an event.

EventMetadata

Bases: BaseModel

Metadata for an event including images and settlement sources.

kalshi.models.series

Series-related models.

Series

Bases: BaseModel

A Kalshi series (template for recurring events).

SeriesFeeChange

Bases: BaseModel

A scheduled fee change for a series.

EventCandlesticks

Bases: BaseModel

Event-level candlestick data spanning multiple markets.

Unlike market candlesticks (flat list for one market), event candlesticks return a nested structure: one candlestick list per market in the event.

PercentilePoint

Bases: BaseModel

A single forecast value at a given percentile.

ForecastPercentilesPoint

Bases: BaseModel

A forecast data point with percentile breakdowns.

Trading

kalshi.models.orders.Order

Bases: BaseModel

A Kalshi order.

Price/cost fields accept both _dollars-suffixed names from the API (e.g. yes_price_dollars) and short names (e.g. yes_price).

kalshi.models.orders.Fill

Bases: BaseModel

A filled trade.

Price fields accept both _dollars-suffixed names from the API and short names. Count accepts _fp-suffixed name.

kalshi.models.orders.AmendOrderV2Response

Bases: BaseModel

Response from POST /portfolio/events/orders/{order_id}/amend.

kalshi.models.orders.OrderQueuePosition

Bases: BaseModel

Queue position for a single resting order.

kalshi.models.portfolio

Portfolio-related models.

SettlementStatusLiteral module-attribute

SettlementStatusLiteral = Literal[
    "all", "unsettled", "settled"
]

Position settlement status filter for GET /fcm/positions. Spec: settlement_status query enum.

PaymentStatusLiteral module-attribute

PaymentStatusLiteral = Literal[
    "pending", "applied", "failed", "returned"
]

Status of a Deposit/Withdrawal. Spec defines two structurally-identical inline enums (Deposit.status, Withdrawal.status); the SDK shares one alias since the values are identical.

PaymentTypeLiteral module-attribute

PaymentTypeLiteral = Literal[
    "ach", "wire", "crypto", "debit", "apm"
]

Payment method used for a deposit/withdrawal.

IndexedBalance

Bases: BaseModel

Balance for a single exchange shard. Added by spec v3.18.0 alongside the balance_breakdown field on :class:Balance.

Currently only exchange_index=0 is supported per spec.

Type note: balance here is DollarDecimal (fixed-point dollar string per spec), unlike :attr:Balance.balance which is integer cents. Same field name, different semantics — be deliberate when reading from balance.balance_breakdown[i].balance versus balance.balance. The :attr:Balance.balance_dollars field rendered in dollars matches the breakdown's units.

Balance

Bases: BaseModel

Account balance.

balance is integer cents (legacy field). balance_dollars is the same value as a fixed-point dollar string, added by spec v3.18.0 and now required on every response. balance_breakdown (optional) splits the total across exchange shards when present.

TotalRestingOrderValue

Bases: BaseModel

Total value of resting orders in cents.

Spec: "intended for use by FCM members (rare)". Non-FCM accounts see 403 on this endpoint (demo audit 2026-04-18).

MarketPosition

Bases: BaseModel

A position in a single market.

total_traded and position are typed DollarDecimal | None / FixedPointCount | None without default=None because the OpenAPI spec (MarketPosition.required) marks total_traded_dollars and position_fp as required response keys. The | None admits the server's observed null on flat positions while a missing key still raises ValidationError — silent omission is treated as a schema regression, not a default.

EventPosition

Bases: BaseModel

A position aggregated at the event level.

PositionsResponse

Bases: BaseModel

Response from the positions endpoint containing both market and event positions.

Deposit

Bases: BaseModel

A single deposit history entry. Amounts are integer cents.

Withdrawal

Bases: BaseModel

A single withdrawal history entry. Amounts are integer cents.

Settlement

Bases: BaseModel

A settled market position.

Account, subaccounts, API keys, FCM

kalshi.models.account

Account-scoped models — API tier limits, etc.

RateLimit

Bases: BaseModel

Per-direction (read/write) token-bucket rate limit.

The server enforces a token bucket per direction: bucket_capacity tokens are allowed in a burst; refill_rate tokens are added per second up to the cap. Requests above the cap return 429.

EndpointTokenCost

Bases: BaseModel

Configured token cost for a single API endpoint.

AccountEndpointCosts

Bases: BaseModel

Response from GET /account/endpoint_costs.

Lists API v2 endpoints whose configured token cost differs from default_cost. Endpoints using the default are omitted.

ApiUsageLevelGrant

Bases: BaseModel

One API usage-level grant for a single exchange lane.

Spec ApiUsageLevelGrant. Each grant applies to its exchange_instance (event_contract or margined); level is the usage level it confers (e.g. premier/paragon/prime). source records how it was created (volume for trading-volume earned, manual for Kalshi-assigned). expires_ts is a Unix-seconds expiry, absent (None) for permanent grants.

AccountApiLimits

Bases: BaseModel

Rate limits associated with the authenticated user's API tier.

NOTE: The published OpenAPI spec (v3.13.0) declares read_limit and write_limit as ints, but the live server returns nested token-bucket objects under read and write. The SDK matches the server. If the spec is corrected upstream, the contract-drift test will flag it.

grants lists the caller's active usage-level grants across exchange lanes; usage_tier is the effective tier reported by this endpoint.

AccountApiUsageLevelVolumeGoal

Bases: BaseModel

A single volume goal for one API usage level.

Spec AccountApiUsageLevelVolumeGoal. earn_volume_goal is the trailing-30-day contract volume needed to earn this level; keep_volume_goal is the (typically lower) volume needed to keep a level already held. Both are fixed-point contract counts.

AccountApiUsageLevelVolumeProgress

Bases: BaseModel

One cron-computed volume-progress snapshot for the predictions lane.

Spec AccountApiUsageLevelVolumeProgress. computed_ts is the Unix timestamp (seconds) at which this snapshot was computed; trailing_30d_volume is the trailing-30-day fixed-point contract volume ending at that time. goals lists the per-level earn/keep volume thresholds.

AccountVolumeProgress

Bases: BaseModel

Response from GET /account/api_usage_level/volume_progress.

Spec GetAccountApiUsageLevelVolumeProgressResponse. Wraps the list of latest cron-computed volume-progress snapshots toward volume-based API usage tiers for the predictions (event_contract) lane.

kalshi.models.subaccounts

Subaccount models — multi-account workflows under one authenticated user.

CreateSubaccountResponse

Bases: BaseModel

Response from POST /portfolio/subaccounts — the new subaccount number.

ApplySubaccountTransferRequest

Bases: BaseModel

Body for POST /portfolio/subaccounts/transfer.

amount_cents is integer cents per spec (e.g. 500 for $5.00, never a Decimal). from_subaccount / to_subaccount use 0 for the primary account and a positive integer for numbered subaccounts. The server is the source of truth for the upper bound: spec describes 1-63 in prose but defines no JSON-schema maximum, and demo has been observed allocating values above 32. The SDK validates only the lower bound (ge=0) so server-assigned numbers always round-trip.

SubaccountBalance

Bases: BaseModel

Balance for a single subaccount.

Note: updated_ts is a Unix seconds integer per spec (format: int64), not an ISO datetime. RFQ/Quote timestamps are format: date-time and surface as datetime; subaccount timestamps follow the spec's int wire format. Callers wanting a datetime can datetime.fromtimestamp(obj.updated_ts, tz=timezone.utc).

GetSubaccountBalancesResponse

Bases: BaseModel

Response from GET /portfolio/subaccounts/balances.

SubaccountTransfer

Bases: BaseModel

A past transfer between subaccounts.

created_ts is a Unix seconds integer per spec (format: int64), matching SubaccountBalance.updated_ts. This is intentionally different from RFQ/Quote timestamps, which are ISO datetime strings.

UpdateSubaccountNettingRequest

Bases: BaseModel

Body for PUT /portfolio/subaccounts/netting.

SubaccountNettingConfig

Bases: BaseModel

Netting state for a single subaccount.

GetSubaccountNettingResponse

Bases: BaseModel

Response from GET /portfolio/subaccounts/netting.

kalshi.models.api_keys

API Key models — programmatic credentials management.

API keys allow programmatic access without username/password. Each key has a unique api_key_id and a user-provided name for bookkeeping. create accepts a caller-supplied RSA public key (PEM); generate has Kalshi mint a fresh key pair and returns the private key once (store it — it cannot be retrieved again).

ApiKey

Bases: BaseModel

An API key registered on the authenticated user's account.

scopes uses NullableList for the same reason envelope-level list fields do — Kalshi has historically returned JSON null for required list fields, and silently coercing None -> [] is safer than raising a Pydantic ValidationError on a field most callers don't use.

GetApiKeysResponse

Bases: BaseModel

Response from GET /api_keys.

api_keys uses NullableList since Kalshi has returned JSON null for required list fields in other envelopes (see v0.9.0 Series fix). Coercing None -> [] matches the envelope-list pattern established across the rest of the SDK.

CreateApiKeyRequest

Bases: BaseModel

Body for POST /api_keys — register a caller-supplied public key.

public_key must be a PEM-encoded RSA public key (i.e. starts with -----BEGIN PUBLIC KEY-----). scopes defaults to full access (["read", "write"]) server-side when omitted. If "write" is included, "read" must be too.

CreateApiKeyResponse

Bases: BaseModel

Response from POST /api_keys — the new key's ID.

GenerateApiKeyRequest

Bases: BaseModel

Body for POST /api_keys/generate — let Kalshi mint a key pair.

GenerateApiKeyResponse

Bases: BaseModel

Response from POST /api_keys/generate.

private_key is a PEM-encoded RSA private key returned ONLY in this response. Store it securely — Kalshi does not retain a copy and it cannot be retrieved later. The field is typed as :class:pydantic.SecretStr so it prints as '**********' in repr()/logs; call response.private_key.get_secret_value() to retrieve the PEM string when you need to persist it.

kalshi.models.exchange

Exchange-related models.

ExchangeIndexStatus

Bases: BaseModel

Operational status of a single exchange index (shard).

ExchangeStatus

Bases: BaseModel

Current exchange operational status.

DailySchedule

Bases: BaseModel

A single trading session within a day.

WeeklySchedule

Bases: BaseModel

Weekly trading hours with per-day sessions.

MaintenanceWindow

Bases: BaseModel

A scheduled maintenance window.

Schedule

Bases: BaseModel

Exchange operating schedule.

Announcement

Bases: BaseModel

An exchange-wide announcement.

UserDataTimestamp

Bases: BaseModel

Timestamp when user-scoped exchange data was last validated.

Combine with WebSocket feeds for a live view — REST endpoints like GetBalance/GetOrders/GetFills/GetPositions may lag the exchange by a short delay; as_of_time is the upper bound on that lag.

kalshi.models.historical

Historical data models.

MveHistoricalFilterLiteral module-attribute

MveHistoricalFilterLiteral = Literal['exclude']

Multivariate-event filter for GET /historical/markets. Spec: MveHistoricalFilterQuery.

HistoricalCutoff

Bases: BaseModel

Timestamps defining the boundary between live and historical data.

Trade

Bases: BaseModel

A public trade on the exchange.

count is typed FixedPointCount | None without default=None because the OpenAPI spec (Trade.required) marks count_fp as a required response key. The | None admits server-side null on rare degenerate trades while a missing key still raises ValidationError — schema regression, not a default.

RFQ / Quote, multivariate, live data, milestones, structured targets, incentive programs, order groups

kalshi.models.communications

Communications / RFQ models — request-for-quote and quote subsystem.

UserFilterLiteral module-attribute

UserFilterLiteral = Literal['self']

Filter for items created by the authenticated user. Spec: UserFilter enum.

RfqStatusLiteral module-attribute

RfqStatusLiteral = Literal['open', 'closed']

RFQ status filter for GET /communications/rfqs. Spec: RFQ.status enum.

QuoteStatusLiteral module-attribute

QuoteStatusLiteral = Literal[
    "open", "accepted", "confirmed", "executed", "cancelled"
]

Quote status filter for GET /communications/quotes. Spec: Quote.status enum.

MveSelectedLeg

Bases: BaseModel

A selected leg within a multivariate event collection RFQ.

RFQ

Bases: BaseModel

An RFQ — request for quote on a market.

Quote

Bases: BaseModel

A quote responding to an RFQ.

GetCommunicationsIDResponse

Bases: BaseModel

Wraps the caller's public communications ID.

GetRFQsResponse

Bases: BaseModel

Paginated list of RFQs.

GetRFQResponse

Bases: BaseModel

Single-RFQ envelope.

CreateRFQRequest

Bases: BaseModel

Body for POST /communications/rfqs.

Spec allows contracts (integer) or contracts_fp (fixed-point string); SDK commits to the integer form. Target cost uses _dollars wire suffix. target_cost_centi_cents is deprecated upstream — omitted here.

Spec only requires market_ticker and rest_remainder; contracts and target_cost are both optional and may both be omitted (a "shopping around" RFQ with no committed size or cost). The SDK does not enforce a contracts-or-cost invariant because the spec doesn't.

CreateRFQResponse

Bases: BaseModel

Wraps the newly-created RFQ's id.

GetQuotesResponse

Bases: BaseModel

Paginated list of quotes.

GetQuoteResponse

Bases: BaseModel

Single-quote envelope.

CreateQuoteRequest

Bases: BaseModel

Body for POST /communications/quotes.

Unlike order/amend requests, the spec names the wire fields yes_bid / no_bid (no _dollars suffix) for this request. The Quote response fields, however, use the _dollars suffix — handled on the response model.

CreateQuoteResponse

Bases: BaseModel

Wraps the newly-created quote's id.

AcceptQuoteRequest

Bases: BaseModel

Body for PUT /communications/quotes/{quote_id}/accept.

BlockTradeProposal

Bases: BaseModel

A block trade proposal — bilateral negotiated trade awaiting both sides.

price_centi_cents and centicount are plain integers in the spec (centi-cents and centicounts respectively), NOT FixedPointDollars/_fp fixed-point wire fields, so they are not DollarDecimal/FixedPointCount.

GetBlockTradeProposalsResponse

Bases: BaseModel

Paginated list of block trade proposals.

ProposeBlockTradeRequest

Bases: BaseModel

Body for POST /communications/block-trade-proposals.

price_centi_cents (centi-cents) and centicount (centicounts) are plain integers per spec, both minimum: 1. buyer_subtrader_id / buyer_subaccount are mutually exclusive (same for the seller pair), but the spec does not encode the exclusivity, so the SDK does not enforce it.

ProposeBlockTradeResponse

Bases: BaseModel

Wraps the newly-created block trade proposal's id.

AcceptBlockTradeProposalRequest

Bases: BaseModel

Body for POST /communications/block-trade-proposals/{id}/accept.

Both fields are optional (spec requestBody required: false); accept as primary by sending an empty body. subtrader_id / subaccount are mutually exclusive but the spec does not encode it, so it is not enforced.

kalshi.models.multivariate

Multivariate event collection models.

MultivariateCollectionStatusLiteral module-attribute

MultivariateCollectionStatusLiteral = Literal[
    "unopened", "open", "closed"
]

Multivariate collection status filter.

Spec: GetMultivariateEventCollections.status query enum.

AssociatedEvent

Bases: BaseModel

An event associated with a multivariate collection.

MultivariateEventCollection

Bases: BaseModel

A multivariate event collection (combo contract template).

TickerPair

Bases: BaseModel

A market+event ticker pair with side, used in create/lookup request bodies.

Note: extra="allow" is intentional — the spec's TickerPair schema has no additionalProperties: false, and some multivariate responses echo back extra provider-specific keys. Because extra does not inherit, request models that embed list[TickerPair] cannot rely on their own extra="forbid" to reject phantom keys inside items. See v0.9 TODO on nested-model drift coverage.

CreateMarketInMultivariateEventCollectionRequest

Bases: BaseModel

Parameters for POST /multivariate_event_collections/{collection_ticker}.

Matches spec components.schemas.CreateMarketInMultivariateEventCollectionRequest. Required: selected_markets. Optional: with_market_payload.

Carve-out: extra="forbid" on this model rejects unknown top-level keys but NOT unknown keys inside each TickerPair in selected_marketsTickerPair itself is extra="allow" (see its docstring for why). Phantom keys nested inside a TickerPair currently pass through to the wire. Tracked as a v0.9 follow-up.

See kalshi.resources.multivariate.MultivariateCollectionsResource.create_market — v0.8.0 builds this model internally; method signature unchanged.

LookupTickersForMarketInMultivariateEventCollectionRequest

Bases: BaseModel

Parameters for PUT /multivariate_event_collections/{collection_ticker}/lookup.

Matches spec components.schemas.LookupTickersForMarketInMultivariateEventCollectionRequest. Only selected_markets, required.

Carve-out: extra="forbid" on this model rejects unknown top-level keys but NOT unknown keys inside each TickerPair in selected_marketsTickerPair itself is extra="allow" (see its docstring for why). Phantom keys nested inside a TickerPair currently pass through to the wire. Tracked as a v0.9 follow-up.

See kalshi.resources.multivariate.MultivariateCollectionsResource.lookup_tickers — v0.8.0 builds this model internally; method signature unchanged.

CreateMarketResponse

Bases: BaseModel

Response from creating a market in a multivariate collection.

LookupTickersResponse

Bases: BaseModel

Response from looking up tickers in a multivariate collection.

LookupPoint

Bases: BaseModel

A point in the lookup history of a multivariate collection.

kalshi.models.live_data

Live data models — real-time event state keyed by milestone.

LiveData.details is deliberately a loose dict[str, Any]: the spec marks it additionalProperties: true with no fixed schema because the shape varies per milestone type (e.g., football vs. political race). GetGameStatsResponse.pbp (play-by-play) is similarly loose — each period/event is a free-form object.

LiveData

Bases: BaseModel

Live-data payload for a specific milestone.

GetLiveDataResponse

Bases: BaseModel

Response from GET /live_data/milestone/{milestone_id}.

GetLiveDatasResponse

Bases: BaseModel

Response from GET /live_data/batch — multiple milestones at once.

PlayByPlayPeriod

Bases: BaseModel

A single period within a game's play-by-play.

events is a loose list of free-form objects (spec has no fixed event schema) because each sport emits different event shapes. NullableList coerces server-returned null to [] (Kalshi has historically sent null for required list fields).

PlayByPlay

Bases: BaseModel

Play-by-play data organized by period.

GetGameStatsResponse

Bases: BaseModel

Response from GET /live_data/milestone/{milestone_id}/game_stats.

pbp is None for unsupported milestone types or milestones without a Sportradar ID (spec: "Returns null for unsupported milestone types or milestones without a Sportradar ID").

kalshi.models.milestones

Milestone models — structured markers for sports/elections/crypto events.

Milestones anchor live-data feeds (see :mod:kalshi.models.live_data). Each milestone has a type (e.g., football_game, political_race) that determines what the details object contains. last_updated_ts and start_date are ISO date-time strings per spec (unlike subaccount timestamps which are Unix ints).

Milestone

Bases: BaseModel

A structured event milestone (game, race, tournament, etc.).

GetMilestoneResponse

Bases: BaseModel

Response from GET /milestones/{milestone_id}.

GetMilestonesResponse

Bases: BaseModel

Response from GET /milestones — paginated list.

milestones uses NullableList since Kalshi has returned JSON null for required list fields in other envelopes (see the v0.9.0 Series fix). Coercing None -> [] here matches the pattern established on Milestone.related_event_tickers and primary_event_tickers.

kalshi.models.structured_targets

Structured targets — external entities (players, teams, etc.) markets anchor to.

Unlike most spec schemas, every StructuredTarget field is optional per spec — Kalshi returns partial records depending on the target type. The SDK mirrors that: all fields default to None / {}.

details is flexible JSON (additionalProperties: true) whose keys depend on type (e.g., a basketball_player carries different keys than a horse_race_entry). Consumers are expected to branch on type.

StructuredTarget

Bases: BaseModel

An external entity a market can be structured against.

target_type is exposed under the SDK-local name (spec wire key is type) to avoid shadowing the Python built-in — same convention as the SDK's query-param renames on this and related resources.

GetStructuredTargetsResponse

Bases: BaseModel

Paginated response for GET /structured_targets.

GetStructuredTargetResponse

Bases: BaseModel

Response for GET /structured_targets/{id}.

kalshi.models.incentive_programs

Incentive programs — market-level liquidity/volume reward programs.

Note on period_reward: spec says "Total reward for the period in centi-cents". The SDK exposes it as a plain int (caller converts to dollars by dividing by 10 000). Fractional values come through target_size_fp (FixedPointCount string).

IncentiveProgramStatusLiteral module-attribute

IncentiveProgramStatusLiteral = Literal[
    "all", "active", "upcoming", "closed", "paid_out"
]

Incentive program status filter. Spec: GetIncentivePrograms.status query enum.

IncentiveProgramTypeLiteral module-attribute

IncentiveProgramTypeLiteral = Literal[
    "all", "liquidity", "volume"
]

Incentive program type filter. Spec: GetIncentivePrograms.type query enum.

IncentiveProgram

Bases: BaseModel

A single incentive program rewarding liquidity or volume on a market.

GetIncentiveProgramsResponse

Bases: BaseModel

Paginated response for GET /incentive_programs.

Uses next_cursor (not cursor) as the pagination key — unique to this endpoint.

kalshi.models.order_groups

Order Groups models — rolling 15-second contracts-limit groups for linked orders.

OrderGroup

Bases: BaseModel

A single order group (list response entry).

GetOrderGroupResponse

Bases: BaseModel

Single-group response — omits id (path param), adds member order IDs.

CreateOrderGroupResponse

Bases: BaseModel

Create response — wraps the new group's id.

CreateOrderGroupRequest

Bases: BaseModel

Create body. SDK sends integer form; spec's contracts_limit_fp string variant unused.

UpdateOrderGroupLimitRequest

Bases: BaseModel

Update-limit body. No subaccount — spec omits SubaccountQuery on /limit.

WebSocket

See WebSocket for the narrative version.

kalshi.ws.client.KalshiWebSocket

KalshiWebSocket(
    auth: KalshiAuth,
    config: KalshiConfig,
    heartbeat_timeout: float = 30.0,
    on_state_change: _StateChangeCb | None = None,
    on_error: (
        Callable[[ErrorMessage], Awaitable[None]] | None
    ) = None,
)

WebSocket client for real-time Kalshi market data.

Usage::

ws = KalshiWebSocket(auth=auth, config=config)
async with ws.connect() as session:
    async for msg in session.subscribe_ticker(tickers=["ECON-GDP-25Q1"]):
        print(msg.msg.yes_bid)

connect

connect() -> _WebSocketSession

Return an async context manager for the WebSocket session.

subscribe_orderbook_delta async

subscribe_orderbook_delta(
    *, tickers: list[str] | None = None, maxsize: int = 1000
) -> AsyncIterator[
    OrderbookSnapshotMessage | OrderbookDeltaMessage
]

Subscribe to orderbook_delta for the given tickers.

Note: OrderbookSnapshotMessage.msg.yes and .no are live dicts owned by the :class:OrderbookManager after this dispatch — they mutate on every delta. Use :meth:orderbook if you need an immutable snapshot view.

subscribe_market_lifecycle async

subscribe_market_lifecycle(
    *, tickers: list[str] | None = None, maxsize: int = 1000
) -> AsyncIterator[
    MarketLifecycleMessage | EventFeeUpdateMessage
]

Subscribe to the market_lifecycle_v2 channel.

Yields :class:MarketLifecycleMessage for lifecycle events and :class:EventFeeUpdateMessage for event_fee_update frames — both ride this channel. Discriminate on the .type field.

subscribe_cfbenchmarks_value async

subscribe_cfbenchmarks_value(
    *,
    index_ids: list[str] | None = None,
    maxsize: int = 1000
) -> AsyncIterator[
    CFBenchmarksValueMessage | CFBenchmarksIndexListMessage
]

Subscribe to the auth-required cfbenchmarks_value index feed.

Seed index_ids (e.g. ["BRTI", "ETHUSD_RTI"] or ["all"]) to receive values immediately; subscribing with no ids yields nothing until indices are added. The stream yields both cfbenchmarks_value data messages and cfbenchmarks_value_indexlist control responses, so discriminate with isinstance(msg, CFBenchmarksValueMessage) (or check msg.type) before reading msg.msg.

unsubscribe async

unsubscribe(client_id: int) -> None

Tear down a subscription and its associated local state (#206).

Removes any local :class:OrderbookManager books seeded by the sub's current server_sid (works for both explicit-tickers and all-markets orderbook subscriptions via the manager's per-sid index), then delegates to :meth:SubscriptionManager.unsubscribe to send the wire command, push the consumer-iterator sentinel, and drop the bookkeeping. Resets the seq watermark too so any sid reuse after server-side renumber doesn't replay against a stale floor.

on

on(channel: str) -> _CallbackDecorator

Decorator to register a callback for a channel.

Works both before and after connect(). Callbacks registered before connect are buffered and applied when the session starts.

run_forever async

run_forever(stop_event: Event | None = None) -> None

Block until the recv loop terminates. Use with the callback API.

Requires at least one prior subscribe_* (or generic :meth:subscribe) call in the same session — the recv loop is started lazily by the subscribe machinery, and without it there is nothing to drain. Registering an @ws.on(channel) callback does NOT subscribe; the server only sends frames for channels you have explicitly subscribed to, so a callback without a matching subscribe sees nothing.

:param stop_event: optional asyncio.Event used to terminate run_forever() cooperatively (#177). When set — typically from a signal handler such as add_signal_handler(SIGINT, stop.set) — this method clears _running, closes the connection, and drains the recv loop. The recv loop sees ConnectionClosed on its next read and exits via the normal not self._running branch, NOT via cancellation, so no CancelledError leaks out. When None (the default) the method blocks on _recv_task directly and external cancellation still propagates as before.

External cancellation of ``run_forever()`` itself (e.g.,
``task.cancel()`` on the awaiting task) while ``stop_event``
is provided still propagates — the cancellation cleans up the
internal ``stop_waiter`` task but does NOT trigger the
cooperative shutdown branch. ``_recv_task`` keeps running
until the session's ``__aexit__`` calls ``_stop()`` for the
full teardown. Use the event for graceful exit; rely on
``__aexit__`` for hard cancellation.

:raises KalshiSubscriptionError: run_forever() was called before any subscribe_* request landed (formerly a silent no-op return — fixed in #175).

orderbook async

orderbook(
    ticker: str, *, maxsize: int = 100
) -> AsyncIterator[Orderbook]

Subscribe to orderbook_delta and yield full Orderbook on each update.

kalshi.ws.connection.ConnectionState

Bases: Enum

WebSocket connection lifecycle states.

kalshi.ws.backpressure.OverflowStrategy

Bases: Enum

What to do when the message queue is full.

DROP_OLDEST class-attribute instance-attribute

DROP_OLDEST = 'drop_oldest'

Ring buffer: evict oldest message, keep newest. Safe for latest-wins channels (ticker).

ERROR class-attribute instance-attribute

ERROR = 'error'

Raise KalshiBackpressureError. Use for stateful channels (orderbook_delta).

kalshi.ws.backpressure.MessageQueue

MessageQueue(
    maxsize: int = 1000,
    overflow: OverflowStrategy = OverflowStrategy.DROP_OLDEST,
    *,
    channel: str | None = None,
    client_id: int | None = None
)

Bases: Generic[T]

Bounded async queue with configurable overflow behavior.

Implements AsyncIterator so consumers can async for msg in queue. Iteration stops when a sentinel is pushed (graceful shutdown), and raises the wrapped exception when an error sentinel from :meth:put_error is reached (#207).

put async

put(item: T) -> None

Add an item to the queue, applying overflow strategy if full.

put_sentinel async

put_sentinel() -> None

Push shutdown sentinel. Causes async iteration to stop.

Idempotent: after the queue is closed (sentinel or error sentinel already pushed), subsequent calls are no-ops. Without this the deque's maxlen=maxsize+1 would evict a real buffered item to make room for a redundant sentinel — losing the last in-flight message between e.g. an ERROR-overflow put_error and the recv-loop's broadcast put_sentinel fan-out.

put_error async

put_error(exc: BaseException) -> None

Push a terminal error sentinel.

The iterator yields any items already in the buffer first, then raises exc when it reaches the sentinel. Subsequent put / put_error / put_sentinel calls are silently dropped (the queue is now closed). Used by the recv loop on KalshiBackpressureError so consumers see the failure rather than a silent StopAsyncIteration (#207).

get async

get() -> T

Get next item, waiting if empty.

qsize

qsize() -> int

Number of items currently in the queue (excludes sentinel). O(1).

Derived from len(self._buffer) minus one when a terminal sentinel has been appended (put_sentinel / put_error both flip _closed and push exactly one sentinel-shaped item).

kalshi.ws.models

WebSocket message models for all 11 Kalshi channels.

BaseMessage

Bases: BaseModel

Base for all WebSocket messages.

ErrorMessage

Bases: BaseModel

Error response from the server.

ErrorPayload

Bases: BaseModel

Error message payload.

OkMessage

Bases: BaseModel

Generic success response (list_subscriptions, update_subscription).

SubscribedMessage

Bases: BaseModel

Response to a subscribe command.

SubscriptionInfo

Bases: BaseModel

Subscription confirmation payload.

UnsubscribedMessage

Bases: BaseModel

Response to an unsubscribe command.

CFBenchmarksAvgData

Bases: BaseModel

Windowed-average metadata for a CF Benchmarks index value.

value is an exact 8-dp decimal string. The tracked indices (e.g. BRTI, ETHUSD_RTI) are USD-denominated reference rates, so it is typed :data:~kalshi.types.DollarDecimal — exact Decimal from the string, no binary-float drift. window_size counts the ticks in the window; window_start_ts_ms/window_end_ts_exclusive bound it in Unix milliseconds (end-exclusive).

CFBenchmarksIndexListMessage

Bases: BaseModel

cfbenchmarks_value_indexlist message — response to the indexlist action.

id echoes the command id that requested the list; it is absent on unsolicited frames, so it is optional.

CFBenchmarksIndexListPayload

Bases: BaseModel

cfbenchmarks_value_indexlist.msg — the available index IDs.

CFBenchmarksValueMessage

Bases: BaseModel

cfbenchmarks_value data message envelope.

CFBenchmarksValuePayload

Bases: BaseModel

cfbenchmarks_value.msg — one index value with trailing averages.

data is the raw CF Benchmarks JSON frame as a string (upstream-defined and index-specific, so it is left unparsed — call json.loads(msg.data) to read it). avg_60s_data is always present (trailing 60-second average). last_60s_windowed_average_15min is present only in the final minute before a quarter-hour close (:00/:15/:30/:45), so it is optional.

CommunicationsMessage

Bases: BaseModel

RFQ and quote notifications. Payload varies by sub-type, so msg is a dict.

Users who want typed parsing can use the individual payload models (RfqCreatedPayload, QuoteCreatedPayload, etc.) to validate msg contents.

QuoteAcceptedPayload

Bases: BaseModel

Quote accepted notification payload.

QuoteCreatedPayload

Bases: BaseModel

Quote created notification payload.

QuoteExecutedPayload

Bases: BaseModel

Quote executed notification payload.

RfqCreatedPayload

Bases: BaseModel

RFQ created notification payload.

Wire format per AsyncAPI spec: created_ts is an RFC3339 date-time string; target_cost_dollars is a dollar string; contracts_fp is a 2-decimal fixed-point count string.

RfqDeletedPayload

Bases: BaseModel

RFQ deleted notification payload.

deleted_ts is an RFC3339 date-time string per AsyncAPI spec.

EventFeeUpdateMessage

Bases: BaseModel

event_fee_update message delivered on the market_lifecycle_v2 channel.

Rides the same channel as :class:MarketLifecycleMessage; subscribers to subscribe_market_lifecycle receive both message types. NO required seq.

EventFeeUpdatePayload

Bases: BaseModel

Payload for event_fee_update messages (market_lifecycle_v2 channel).

Emitted when an event-level fee override is set or cleared. fee_type_override and fee_multiplier_override are both None when the override has been cleared (the event falls back to the parent series' fee structure). Both are spec-required keys but nullable — the key is present; None is the meaningful "override cleared" signal, so neither carries a default.

FillMessage

Bases: BaseModel

Fill update message. NO required seq.

FillPayload

Bases: BaseModel

Payload for fill messages (private channel).

Wire format per AsyncAPI spec: yes_price_dollars is a dollar-decimal string; count_fp / post_position_fp are fixed-point count strings; fee_cost is a dollar-decimal string; ts is an integer Unix timestamp (seconds).

MarketLifecycleMessage

Bases: BaseModel

Market lifecycle v2 update message. NO required seq.

MarketLifecyclePayload

Bases: BaseModel

Payload for market_lifecycle_v2 messages (public channel).

Discriminated by event_type field. Conditional fields depend on event_type: - created/activated: open_ts, close_ts, title, subtitle, series_ticker - determined: result, determination_ts - settled: settlement_value, settled_ts - deactivated: is_deactivated

MarketPositionsMessage

Bases: BaseModel

Market positions update message. NO required seq.

MarketPositionsPayload

Bases: BaseModel

Payload for market_positions messages (private channel).

Dollar-denominated fields use :data:DollarDecimal per the CLAUDE.md convention. position is a fixed-point contract count (string).

MultivariateLifecycleMessage

Bases: BaseModel

Multivariate market lifecycle message. Same payload as MarketLifecycleMessage.

MultivariateMessage

Bases: BaseModel

Multivariate update message. NO required seq.

MultivariatePayload

Bases: BaseModel

Payload for multivariate messages (public channel).

SelectedMarket

Bases: BaseModel

A selected market within a multivariate collection.

OrderGroupMessage

Bases: BaseModel

Order group update message. HAS required seq (one of only 2 channels).

OrderGroupPayload

Bases: BaseModel

Payload for order_group_updates messages (private channel).

OrderbookDeltaMessage

Bases: BaseModel

Incremental orderbook update.

OrderbookDeltaPayload

Bases: BaseModel

Payload for orderbook_delta messages.

Wire format per AsyncAPI spec and confirmed on demo: price_dollars is a dollar-decimal string (e.g. "0.0200"), delta_fp is a fixed-point count string (e.g. "10.00", may be negative), ts is an RFC3339 timestamp string (e.g. "2026-04-19T18:43:37.662364Z").

OrderbookSnapshotMessage

Bases: BaseModel

Full orderbook snapshot, sent on initial subscribe.

OrderbookSnapshotPayload

Bases: BaseModel

Payload for orderbook_snapshot messages.

Wire format per AsyncAPI spec: yes_dollars_fp and no_dollars_fp are arrays of [price_in_dollars, contract_count_fp] string pairs. Each row is two JSON strings, e.g. ["0.5500", "100.00"].

These are validated directly into dict[Decimal, Decimal] (price -> count) in a single walk so OrderbookManager._apply_snapshot_inplace can adopt the map with no second iteration (#263).

Both sides are required (#268): a snapshot envelope missing yes_dollars_fp or no_dollars_fp is schema drift or a partial server response, not "an empty book on that side". Surfacing a ValidationError lets the recv loop's malformed-frame handler log it and roll back the seq watermark for #241 rather than silently resetting the local book to empty.

Aliasing note: when delivered through :meth:KalshiWSClient.subscribe_orderbook_delta, yes / no are live dicts owned by the :class:OrderbookManager after dispatch — they mutate on every subsequent delta.

TickerMessage

Bases: BaseModel

Ticker update message. NO required seq.

TickerPayload

Bases: BaseModel

Payload for ticker messages (public channel).

Wire format per AsyncAPI spec: yes_bid_dollars / yes_ask_dollars are dollar-decimal strings; ts is an integer Unix timestamp (seconds); _fp fields are fixed-point count strings.

TradeMessage

Bases: BaseModel

Trade update message. NO required seq.

TradePayload

Bases: BaseModel

Payload for trade messages (public channel).

Wire format per AsyncAPI spec: yes_price_dollars / no_price_dollars are dollar-decimal strings; count_fp is a fixed-point count string; ts is an integer Unix timestamp (seconds).

UserOrdersMessage

Bases: BaseModel

User orders update message. NO required seq.

UserOrdersPayload

Bases: BaseModel

Payload for user_orders messages (private channel).

Wire format captured on demo 2026-04-19: yes_price_dollars is a dollar string with up to 4 decimals ("0.0100"); *_fp counts are 2-decimal fixed-point strings; taker_fill_cost_dollars / maker_fill_cost_dollars / taker_fees_dollars / maker_fees_dollars are dollar strings with up to 6 decimals. All stored as :class:decimal.Decimal via :data:DollarDecimal.

FIX protocol

See FIX protocol for the narrative version.

kalshi.fix.client.FixClient

FixClient(
    signer: FixSigner,
    *,
    environment: FixEnvironment = FixEnvironment.PRODUCTION,
    config: FixConfig | None = None,
    ssl_context: SSLContext | None = None
)

Bases: _BaseFixClient

FIX client for the prediction (event-contract) gateway.

Supports all six session types: order entry (NR/RT), drop copy, market data, post trade, and RFQ. Construct from a :class:FixSigner, or from an existing :class:KalshiAuth via :meth:from_auth / the KALSHI_* env via :meth:from_env.

from_env classmethod

from_env(
    *,
    environment: FixEnvironment = FixEnvironment.PRODUCTION,
    config: FixConfig | None = None,
    ssl_context: SSLContext | None = None,
    password: (
        bytes | str | Callable[[], bytes | str] | None
    ) = None
) -> Self

Build a prediction FIX client from the KALSHI_* environment vars.

password (or KALSHI_PRIVATE_KEY_PASSPHRASE) decrypts a passphrase-protected private key — parity with the REST client.

post_trade

post_trade(
    *,
    on_message: MessageHandler | None = None,
    on_state_change: StateChangeHandler | None = None,
    on_decode_error: DecodeErrorHandler | None = None
) -> FixSession

A post-trade (KalshiPT) session for market-settlement reports.

rfq

rfq(
    *,
    on_message: MessageHandler | None = None,
    on_state_change: StateChangeHandler | None = None,
    on_decode_error: DecodeErrorHandler | None = None
) -> FixSession

An RFQ (KalshiRFQ) market-maker session.

kalshi.perps.fix.MarginFixClient

MarginFixClient(
    signer: FixSigner,
    *,
    environment: FixEnvironment = FixEnvironment.PRODUCTION,
    config: FixConfig | None = None,
    ssl_context: SSLContext | None = None
)

Bases: _BaseFixClient

FIX client for the margin (perps) gateway.

Supports order entry (NR/RT), drop copy, and market data. Construct from a :class:~kalshi.fix.auth.FixSigner, from an existing :class:~kalshi.auth.KalshiAuth via :meth:from_auth, or from the KALSHI_PERPS_* environment via :meth:from_env.

from_env classmethod

from_env(
    *,
    environment: FixEnvironment = FixEnvironment.PRODUCTION,
    config: FixConfig | None = None,
    ssl_context: SSLContext | None = None,
    password: (
        bytes | str | Callable[[], bytes | str] | None
    ) = None
) -> Self

Build a margin FIX client from the KALSHI_PERPS_* environment vars.

kalshi.fix.config.FixConfig dataclass

FixConfig(
    product: FixProduct = FixProduct.PREDICTION,
    environment: FixEnvironment = FixEnvironment.PRODUCTION,
    host: str | None = None,
    port: int | None = None,
    use_tls: bool = True,
    heartbeat_interval: int = DEFAULT_HEARTBEAT_INTERVAL,
    connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
    max_retries: int = DEFAULT_MAX_RETRIES,
    retry_base_delay: float = 0.5,
    retry_max_delay: float = 30.0,
    cancel_orders_on_disconnect: bool = False,
    listener_session: bool = False,
    skip_pending_exec_reports: bool = False,
    receive_settlement_reports: bool | None = None,
    use_dollars: bool | None = None,
    allow_unknown_host: bool = False,
)

FIX connectivity + session configuration.

A single config describes a product/environment plus tuning; the concrete host/port are resolved per :class:FixSessionType (one TCP connection per session, one connection per API key). host / port overrides target a mock server or a local TLS proxy (stunnel) and, when set, apply to every session.

A non-loopback host override outside the known Kalshi FIX endpoints is rejected unless allow_unknown_host=True or the process-wide KALSHI_FIX_ALLOW_UNKNOWN_HOST=1 environment variable is set (an escape hatch for CI / staging proxies).

Use :meth:prediction / :meth:margin for the canonical environments.

allowed_sessions property

allowed_sessions: frozenset[FixSessionType]

Session types valid for this product.

effective_use_dollars property

effective_use_dollars: bool

Resolved UseDollars flag: margin is always on; prediction opt-in.

prediction classmethod

prediction(
    environment: FixEnvironment = FixEnvironment.PRODUCTION,
    *,
    host: str | None = None,
    port: int | None = None,
    use_tls: bool = True,
    heartbeat_interval: int = DEFAULT_HEARTBEAT_INTERVAL,
    connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
    max_retries: int = DEFAULT_MAX_RETRIES,
    retry_base_delay: float = 0.5,
    retry_max_delay: float = 30.0,
    cancel_orders_on_disconnect: bool = False,
    listener_session: bool = False,
    skip_pending_exec_reports: bool = False,
    receive_settlement_reports: bool | None = None,
    use_dollars: bool | None = None,
    allow_unknown_host: bool = False
) -> FixConfig

Config for the prediction (event-contract) FIX gateway.

margin classmethod

margin(
    environment: FixEnvironment = FixEnvironment.PRODUCTION,
    *,
    host: str | None = None,
    port: int | None = None,
    use_tls: bool = True,
    heartbeat_interval: int = DEFAULT_HEARTBEAT_INTERVAL,
    connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
    max_retries: int = DEFAULT_MAX_RETRIES,
    retry_base_delay: float = 0.5,
    retry_max_delay: float = 30.0,
    cancel_orders_on_disconnect: bool = False,
    listener_session: bool = False,
    skip_pending_exec_reports: bool = False,
    receive_settlement_reports: bool | None = None,
    use_dollars: bool = True,
    allow_unknown_host: bool = False
) -> FixConfig

Config for the margin (perps) FIX gateway (fixed-point dollars enforced).

host_for

host_for(session: FixSessionType) -> str

Resolve the TCP host for session (honoring a host override).

port_for

port_for(session: FixSessionType) -> int

Resolve the TCP port for session (honoring a port override).

target_comp_id

target_comp_id(session: FixSessionType) -> str

The wire TargetCompID (tag 56) for session.

supports_retransmission

supports_retransmission(session: FixSessionType) -> bool

Whether session supports ResendRequest/SequenceReset recovery.

When False the session must logon with ResetSeqNumFlag=Y and a forward sequence gap is unrecoverable.

kalshi.fix.session.FixSession

FixSession(
    signer: FixSigner,
    config: FixConfig,
    session_type: FixSessionType,
    *,
    on_message: MessageHandler | None = None,
    on_state_change: StateChangeHandler | None = None,
    on_decode_error: DecodeErrorHandler | None = None,
    ssl_context: SSLContext | None = None
)

A single async FIX session.

Usage::

signer = FixSigner.from_env()
config = FixConfig.prediction(environment=FixEnvironment.DEMO)
session = FixSession(signer, config, FixSessionType.ORDER_ENTRY_NR,
                     on_message=handle)
async with session:
    ...  # send order-entry messages, receive execution reports

on_message receives every inbound application message as a raw :class:~kalshi.fix.codec.RawMessage (decode it with :func:~kalshi.fix.messages.decode_app_message). Setting on_decode_error additionally routes a registered-but-malformed message (a real message lost to one off-spec field) — but it makes the session run a full Pydantic validation on every inbound application message to detect failures, so on a high-rate session that is real per-message overhead (and the consumer's own decode in on_message is then a second pass). Leave it unset unless you need malformed messages surfaced; there is no cost when it is None.

state property

state: FixSessionState

Current session state.

session_type property

session_type: FixSessionType

The session type (TargetCompID) this session connects as.

outbound_seq_num property

outbound_seq_num: int

The MsgSeqNum that will be used for the next outbound message.

inbound_seq_num property

inbound_seq_num: int

The MsgSeqNum expected on the next inbound message.

start async

start() -> None

Connect, log on, and start the background recv + heartbeat tasks.

close async

close() -> None

Stop background tasks, send a best-effort (bounded) Logout, close the socket.

The heartbeat loop is cancelled first so it cannot race the Logout send, and the Logout is wrapped in a timeout so a dead/half-open connection (where writer.drain() never returns) cannot hang close().

send async

send(message: FixMessage) -> int

Send an application message; returns the assigned MsgSeqNum.

Admin messages are managed by the session itself — use the typed order-entry / market-data helpers (later phases) rather than sending session-layer messages directly.

kalshi.fix.orderbook.FixOrderBook

FixOrderBook()

Reconstructs aggregated market books from FIX W snapshots + X incrementals.

Usage::

book = FixOrderBook()
book.apply(decode_app_message(raw))   # W or X (others ignored)
view = book.get("KXNBAGAME-...")       # MarketDataBook | None

apply_snapshot

apply_snapshot(msg: MarketDataSnapshotFullRefresh) -> None

Replace a market's book from a full snapshot (35=W).

A snapshot without a symbol is ignored (nothing to key on). Unknown MDEntryType entries are skipped, as are non-positive-size levels (a 0 size means "no level" — parity with an incremental Delete). An empty snapshot is a valid empty book.

apply_incremental

apply_incremental(msg: MarketDataIncrementalRefresh) -> int

Apply incremental level changes (35=X). Returns the number applied.

Each entry routes by its own symbol. Change sets the level's new absolute size (a non-positive size removes the level); Delete removes it. An entry for a market with no snapshot yet, or with an unknown MDUpdateAction/MDEntryType, is skipped without mutating the book.

apply

apply(msg: object) -> None

Apply a decoded W or X message; anything else is ignored.

Convenience for feeding :func:~kalshi.fix.messages.decode_app_message output straight in without a type switch at the call site.

get

get(symbol: str) -> MarketDataBook | None

The current book view for symbol, or None if not seeded.

symbols

symbols() -> set[str]

The set of markets with a seeded book.

remove

remove(symbol: str) -> None

Drop a market's book (e.g. on unsubscribe or settlement).

clear

clear() -> None

Drop all books (e.g. before re-subscribing after a reconnect).

kalshi.fix.settlement.SettlementReassembler

SettlementReassembler()

Reassemble paginated UMS fragments into one report per settlement batch.

Usage::

reasm = SettlementReassembler()
complete = reasm.add(decode_app_message(raw))  # MarketSettlementReport | None
if complete is not None:
    ...  # all parties for the batch are in complete.parties

Assumptions / limitations. Symbol is the only correlation field (the report id differs per page), so this keys solely on it and assumes a batch's fragments arrive as one contiguous run terminated by LastFragment=Y (or a standalone report). It does not reconcile against TotNumMarketSettlementReports (20106). Consequences:

  • Two batches for the same symbol must not interleave — a second batch's terminal arriving while the first is still buffered would coalesce both into one report. A well-formed gateway sends a symbol's fragments contiguously (each batch ends with LastFragment=Y before the next begins), so this does not occur in practice; sequential same-symbol batches reassemble independently because the buffer clears on each terminal.
  • If a terminal fragment never arrives (mid-batch disconnect), the partial parties stay buffered — see :meth:pending_symbols — until :meth:clear (call it on reconnect).
  • A re-delivered terminal fragment re-emits its report, so consumers should handle emitted reports idempotently.

add

add(
    report: MarketSettlementReport,
) -> MarketSettlementReport | None

Feed one fragment; return the assembled report on the terminal fragment.

Returns None while more fragments are expected (LastFragment=N). A terminal fragment (LastFragment True or absent) returns the report with every accumulated party; a standalone report is returned unchanged. The assembled report carries the terminal fragment's MarketSettlementReportID (each page has its own — there is no canonical batch id), so key any dedup off Symbol + clearing date, not that id.

pending_symbols

pending_symbols() -> set[str]

Symbols with buffered fragments still awaiting their terminal page.

clear

clear() -> None

Drop all buffered fragments (e.g. on reconnect).

Warns when discarding non-empty buffers so silently-dropped partial settlement data is observable.

kalshi.fix.messages

Typed FIX message models (FIX Dictionary v1.03).

Exposes the base framework (scalar + repeating-group fields), the session-layer (admin) messages, the shared repeating-group entry components, and the order-entry message flow. Market-data, drop-copy, and RFQ/settlement flows are added in later phases — see GH #402.

FixGroup

Bases: _FixFieldModel

Base for one entry of a repeating group (no MsgType of its own).

FixGroupMeta dataclass

FixGroupMeta(
    num_in_group_tag: int, entry_model: type[FixGroup]
)

Annotated metadata marking a repeating-group field.

Declare a group as Annotated[list[EntryModel], FixGroupMeta(count_tag, EntryModel)] where count_tag is the NumInGroup tag and EntryModel is the :class:FixGroup subclass describing one entry.

FixMessage

Bases: _FixFieldModel

Base for all typed FIX messages. Subclasses set the MSG_TYPE class var.

FixType

Bases: StrEnum

Wire type of a FIX field, governing string<->Python conversion.

CollateralAmountChange

Bases: FixGroup

One NoCollateralAmountChanges (1703) entry — delimiter (1704).

MarketSettlementParty

Bases: FixGroup

One NoMarketSettlementPartyIDs (20108) entry — delimiter (20109).

Nests NoCollateralAmountChanges (1703) and NoMiscFees (136).

MiscFee

Bases: FixGroup

One NoMiscFees (136) entry — delimiter MiscFeeAmt (137).

MultivariateSelectedLeg

Bases: FixGroup

One NoMultivariateSelectedLegs (20181) entry — delimiter (20182).

Party

Bases: FixGroup

One NoPartyIDs (453) entry — delimiter PartyID (448).

EventResendComplete

Bases: FixMessage

EventResendComplete (35=U2) — all requested events have been resent.

EventResendReject

Bases: FixMessage

EventResendReject (35=U3) — the resend request could not be fulfilled.

event_resend_reject_reason is a raw int (compare against :class:~kalshi.fix.enums.EventResendRejectReason).

EventResendRequest

Bases: FixMessage

EventResendRequest (35=U1) — request execution reports in an ExecID range.

MarketDataIncrementalRefresh

Bases: FixMessage

MarketDataIncrementalRefresh (35=X) — changed book levels.

Each entry carries its own symbol (the group spans markets).

MarketDataRequest

Bases: FixMessage

MarketDataRequest (35=V) — request order-book snapshots / updates.

Prefer the :meth:snapshot / :meth:subscribe / :meth:unsubscribe / :meth:unsubscribe_all constructors: NoRelatedSym/Symbol are required for snapshot and subscribe, and a cancel-all request omits them entirely.

MDReqID (262) is intentionally omitted: the Kalshi MD docs do not define it on 35=V/W/X/Y (subscriptions and rejects correlate by Symbol, not a request id), unlike generic FIX 5.0SP2.

snapshot classmethod

snapshot(symbols: Iterable[str]) -> MarketDataRequest

A one-shot snapshot request (263=0) for the given market tickers.

subscribe classmethod

subscribe(symbols: Iterable[str]) -> MarketDataRequest

A snapshot-plus-updates subscription (263=1) for the given tickers.

unsubscribe classmethod

unsubscribe(symbols: Iterable[str]) -> MarketDataRequest

Cancel the subscriptions (263=2) for the given tickers.

unsubscribe_all classmethod

unsubscribe_all() -> MarketDataRequest

Cancel every subscription on the session (263=2 with no symbols).

MarketDataRequestReject

Bases: FixMessage

MarketDataRequestReject (35=Y) — a MarketDataRequest could not be accepted.

md_req_rej_reason is a raw char (compare against :class:~kalshi.fix.enums.MDReqRejReason).

MarketDataSnapshotFullRefresh

Bases: FixMessage

MarketDataSnapshotFullRefresh (35=W) — the full aggregated book for a market.

Correlate by symbol. An empty entries list is a valid empty book (the server returns one for a symbol it has no order book for).

MDIncrementalEntry

Bases: FixGroup

One NoMDEntries (268) entry in an incremental — delimiter MDUpdateAction (279).

md_update_action / md_entry_type are raw chars (compare against :class:~kalshi.fix.enums.MDUpdateAction / MDEntryType). md_entry_size is the new absolute size at the level (0 when the level is deleted). Price/size are optional for inbound robustness (a Delete that omits them still parses), matching the order-entry inbound convention.

MDSnapshotEntry

Bases: FixGroup

One NoMDEntries (268) entry in a snapshot — delimiter MDEntryType (269).

md_entry_type is a raw char (compare against :class:~kalshi.fix.enums.MDEntryType: 0=bid, 1=offer). Price/size are optional for inbound robustness (parse an off-spec entry rather than rejecting the whole message), matching the order-entry inbound convention.

RelatedSymbol

Bases: FixGroup

One NoRelatedSym (146) entry — delimiter Symbol (55).

SecurityStatus

Bases: FixMessage

SecurityStatus (35=f) — a market's trading-status change.

security_trading_status is a raw int (compare against :class:~kalshi.fix.enums.SecurityTradingStatus).

SecurityStatusRequest

Bases: FixMessage

SecurityStatusRequest (35=e) — subscribe/unsubscribe a market's status stream.

subscribe classmethod

subscribe(symbol: str) -> SecurityStatusRequest

Subscribe (263=1) to symbol's trading-status changes.

unsubscribe classmethod

unsubscribe(symbol: str) -> SecurityStatusRequest

Unsubscribe (263=2) from symbol's trading-status changes.

BusinessMessageReject

Bases: FixMessage

BusinessMessageReject (35=j) — application-level rejection of a message.

ExecutionReport

Bases: FixMessage

ExecutionReport (35=8) — order state change from the exchange.

exec_type / ord_status / side are raw chars (compare against :class:~kalshi.fix.enums.ExecType / OrdStatus / Side); rejection and STP codes are raw ints. Position/fee/collateral detail is present on ExecType=Trade via the misc-fees and collateral groups.

NewOrderSingle

Bases: FixMessage

NewOrderSingle (35=D) — submit a new order.

TransactTime (60) is intentionally not sent: the Kalshi dictionary v1.03 does not include it on 35=D (the gateway stamps its own time), unlike the generic FIX 5.0SP2 message.

OrderCancelReject

Bases: FixMessage

OrderCancelReject (35=9) — an amend/cancel was rejected by the exchange.

OrderCancelReplaceRequest

Bases: FixMessage

OrderCancelReplaceRequest (35=G) — amend an order's price/quantity.

OrderCancelRequest

Bases: FixMessage

OrderCancelRequest (35=F) — cancel an order's remaining quantity.

OrderMassCancelReport

Bases: FixMessage

OrderMassCancelReport (35=r) — response to a mass-cancel request.

OrderMassCancelRequest

Bases: FixMessage

OrderMassCancelRequest (35=q) — cancel all orders for the session (NR only).

OrderGroupRequest

Bases: FixMessage

OrderGroupRequest (35=UOG) — create/reset/delete/trigger/update a group.

Field order follows the docs examples (action, id, contracts-limit, account); FIX scalar field order is not significant, so the helpers below are the intended construction path.

create classmethod

create(
    contracts_limit: int,
    *,
    alloc_account: int | None = None
) -> OrderGroupRequest

Create a group with a contracts limit (the server assigns the id).

reset classmethod

reset(
    order_group_id: str, *, alloc_account: int | None = None
) -> OrderGroupRequest

Reset a group's rolling contract count.

delete classmethod

delete(
    order_group_id: str, *, alloc_account: int | None = None
) -> OrderGroupRequest

Delete a group (cancels all resting orders in it).

trigger classmethod

trigger(
    order_group_id: str, *, alloc_account: int | None = None
) -> OrderGroupRequest

Trigger a group — immediately cancel all its orders (TriggerCancel=4).

update classmethod

update(
    order_group_id: str,
    contracts_limit: int,
    *,
    alloc_account: int | None = None
) -> OrderGroupRequest

Update a group's contracts limit.

OrderGroupResponse

Bases: FixMessage

OrderGroupResponse (35=UOH) — the gateway's reply to an OrderGroupRequest.

order_group_contracts_limit is echoed only on Create and Update responses. Fields are optional for inbound robustness; business-logic errors arrive as a BusinessMessageReject (35=j), malformed fields as a session Reject (35=3).

AcceptQuote

Bases: FixMessage

AcceptQuote (35=UA) — creator accepts a maker's quote.

NB: side (tag 54) is inverted for AcceptQuote per the Kalshi docs — Side.BUY_YES (1) accepts the maker's NO quote and Side.SELL_NO (2) accepts the maker's YES quote (the field reuses the shared FIX Side, so the enum member names read opposite to the accept semantics).

AcceptQuoteStatus

Bases: FixMessage

AcceptQuoteStatus (35=UC) — exchange response to an AcceptQuote.

accept_quote_status is a raw int (compare against :class:~kalshi.fix.enums.AcceptQuoteResult); accepted_quote_id may differ from the requested quote when PreferBetterQuote was set.

Quote

Bases: FixMessage

Quote (35=S) — submit a quote (maker) / quote notification (creator).

Either bid_px or offer_px may be zero, but not both (zero = no quote for that side). Use :meth:submit for the maker path.

submit classmethod

submit(
    quote_id: str,
    quote_req_id: str,
    symbol: str,
    *,
    bid_px: DollarDecimal | None = None,
    offer_px: DollarDecimal | None = None,
    alloc_account: int | None = None,
    rest_remainder: bool | None = None
) -> Quote

Build a maker quote for quote_req_id on symbol.

At least one of bid_px / offer_px is required (a quote with neither side is rejected by the exchange) — None means that side is absent, distinct from Decimal(0) (an explicit zero = no quote for that side). The maker does not send sizes — the exchange derives size from the RFQ's OrderQty — so bid_size / offer_size / order_qty are not parameters here; they appear on the model only for the inbound (Exchange -> Creator) quote notification.

QuoteCancel

Bases: FixMessage

QuoteCancel (35=Z) — maker cancels an active quote.

QuoteCancelStatus

Bases: FixMessage

QuoteCancelStatus (35=U9) — exchange response to a QuoteCancel.

quote_cancel_status is a raw int (compare against :class:~kalshi.fix.enums.QuoteCancelResult).

QuoteConfirm

Bases: FixMessage

QuoteConfirm (35=U7) — maker confirms execution after a quote is accepted.

QuoteConfirmStatus

Bases: FixMessage

QuoteConfirmStatus (35=U8) — exchange response to a QuoteConfirm.

quote_confirm_status is a raw int (compare against :class:~kalshi.fix.enums.QuoteConfirmResult).

QuoteRequest

Bases: FixMessage

QuoteRequest (35=R) — create an RFQ (creator) / RFQ notification (maker).

Fields are optional so the inbound notification parses robustly; use :meth:create to build a well-formed outbound RFQ. NoRelatedSym is always 1 (single market); Symbol xor the MVE collection+legs identify the market, and OrderQty xor CashOrderQty size it.

create classmethod

create(
    quote_req_id: str,
    symbol: str,
    *,
    order_qty: FixedPointCount | None = None,
    cash_order_qty: DollarDecimal | None = None,
    alloc_account: int | None = None,
    rest_remainder: bool | None = None,
    replace_existing: bool | None = None
) -> QuoteRequest

Create a single-market RFQ for symbol.

Requires at least one of order_qty / cash_order_qty to size it. MVE/parlay RFQs (collection ticker + legs instead of a symbol) construct :class:QuoteRequest directly.

QuoteRequestAck

Bases: FixMessage

QuoteRequestAck (35=b) — exchange ack of a creator's QuoteRequest.

quote_request_type is a raw int (compare against :class:~kalshi.fix.enums.QuoteRequestType); rfq_id is the server id.

QuoteRequestReject

Bases: FixMessage

QuoteRequestReject (35=AG) — exchange rejected/cancelled an RFQ request.

quote_request_reject_reason is a raw int (compare against :class:~kalshi.fix.enums.QuoteRequestRejectReason).

QuoteStatusReport

Bases: FixMessage

QuoteStatusReport (35=AI) — quote lifecycle update to the maker.

quote_status is a raw int (compare against :class:~kalshi.fix.enums.QuoteStatus); side (AcceptedSide) is a raw char (FIX CHAR — not an int like the status codes), present only when ACCEPTED.

RFQCancel

Bases: FixMessage

RFQCancel (35=UE) — creator cancels an active RFQ.

Identify the RFQ by either quote_req_id (client id) or rfq_id (server id); use :meth:for_req_id / :meth:for_rfq_id. The helpers cover the common case; an FCM on-behalf-of cancel sets parties via direct construction, e.g. RFQCancel(quote_req_id=..., parties=[Party(...)]).

for_req_id classmethod

for_req_id(quote_req_id: str) -> RFQCancel

Cancel by the client-assigned QuoteReqID.

for_rfq_id classmethod

for_rfq_id(rfq_id: str) -> RFQCancel

Cancel by the server-assigned RfqID.

RFQCancelStatus

Bases: FixMessage

RFQCancelStatus (35=UB) — exchange response to an RFQCancel.

rfq_cancel_status is a raw int (compare against :class:~kalshi.fix.enums.RFQCancelResult).

Heartbeat

Bases: FixMessage

Heartbeat (35=0). test_req_id echoes a TestRequest when responding to one.

Logon

Bases: FixMessage

Logon (35=A).

The raw_data field carries the base64 RSA-PSS logon signature; the codec auto-emits its RawDataLength (95) field. heartbeat_interval must be

3 (Kalshi default 30). reset_seq_num_flag must be True on non-retransmission sessions (KalshiNR / KalshiDC).

Logout

Bases: FixMessage

Logout (35=5). text carries a human-readable reason when present.

Reject

Bases: FixMessage

Reject (35=3) — session-level rejection of a message we sent.

ResendRequest

Bases: FixMessage

ResendRequest (35=2). Inclusive range; end_seq_no=0 means "through latest".

SequenceReset

Bases: FixMessage

SequenceReset (35=4). gap_fill_flag distinguishes gap-fill from reset mode.

TestRequest

Bases: FixMessage

TestRequest (35=1). The peer must echo test_req_id in its Heartbeat.

MarketSettlementReport

Bases: FixMessage

MarketSettlementReport (35=UMS) — a market's settlement detail.

market_result is a raw str (compare against :class:~kalshi.fix.enums.MarketResult). settlement_price rides the FIX Price field as a no-float Decimal (DollarDecimal is just the coercion type); the value is in cents, 2 dp — Decimal("100.00") is 100c (full YES payout) and Decimal("30.60") a scalar market — not dollars. clearing_business_date is the raw YYYYMMDD string. last_fragment is False on non-final pages of a paginated batch, True (or absent) on the last.

fixfield

fixfield(
    tag: int,
    fix_type: FixType,
    *,
    default: Any = ...,
    **kwargs: Any
) -> Any

Declare a scalar FIX field.

tag is the FIX tag number, fix_type its wire type. Omit default for a required field (Pydantic treats ... as required).

groupfield

groupfield() -> Any

Declare a repeating-group field (defaults to an empty list).

The NumInGroup tag and entry model ride on the field's :class:Annotated :class:FixGroupMeta; this only supplies the default.

decode_app_message

decode_app_message(raw: RawMessage) -> FixMessage | None

Decode an inbound application :class:RawMessage to its typed model.

Returns None for message types without a registered model (an admin message or a not-yet-implemented application flow), and also None if the payload fails schema validation — a malformed inbound message is logged and swallowed rather than raised into the consumer's on_message handler. To distinguish those two cases (e.g. to route a malformed known message to a dead-letter), use :func:decode_app_message_strict or FixSession's on_decode_error hook.

decode_app_message_strict

decode_app_message_strict(
    raw: RawMessage,
) -> FixMessage | None

Decode an inbound application :class:RawMessage, raising on a malformed one.

Returns None only for an unregistered message type (an admin message or a not-yet-implemented flow). A registered model whose payload fails schema validation raises :class:~kalshi.fix.errors.FixDecodeError (chaining the underlying error) rather than returning None, so a genuine message lost to a single off-spec field is observable. Used by FixSession's on_decode_error hook; direct callers wanting the distinction can call this.

kalshi.fix.enums

FIX enumerations for the Kalshi dialect (FIXT.1.1 / FIX50SP2).

Values are the exact on-the-wire tokens from the Kalshi FIX Dictionary v1.03. String/char enums subclass :class:enum.StrEnum so member.value is the wire string; integer enums subclass :class:enum.IntEnum and serialize via str(int(member)). The codec / message layer converts in both directions.

Defining the full enum surface up front (including order-entry and RFQ values not exercised by the foundation layer) keeps a single spec-aligned source of truth for the later message-flow phases.

MsgType

Bases: StrEnum

Tag 35 — message type. Admin (session) + application message identifiers.

EncryptMethod

Bases: IntEnum

Tag 98 — Kalshi FIX uses transport TLS, so message-level encryption is None.

ApplVerID

Bases: StrEnum

Tags 1128 / 1137 — application version. Kalshi is FIX50SP2.

Side

Bases: StrEnum

Tag 54 — order side in Kalshi's yes/no contract vocabulary.

OrdType

Bases: StrEnum

Tag 40 — Kalshi supports limit orders only.

TimeInForce

Bases: StrEnum

Tag 59 — order time in force.

OrdStatus

Bases: StrEnum

Tag 39 — order status.

ExecType

Bases: StrEnum

Tag 150 — execution report type.

ExecInst

Bases: StrEnum

Tag 18 — execution instructions (multi-value string; Kalshi uses POST_ONLY).

SelfTradePreventionType

Bases: IntEnum

Tag 2964 — self-trade prevention behaviour.

SessionRejectReason

Bases: IntEnum

Tag 373 — reason a session-level Reject (35=3) was issued.

BusinessRejectReason

Bases: IntEnum

Tag 380 — reason a BusinessMessageReject (35=j) was issued.

CxlRejReason

Bases: IntEnum

Tag 102 — order cancel/replace rejection reason.

CxlRejResponseTo

Bases: StrEnum

Tag 434 — which request an OrderCancelReject (35=9) responds to.

OrdRejReason

Bases: IntEnum

Tag 103 — order rejection reason.

MassCancelRequestType

Bases: StrEnum

Tag 530 — scope of an OrderMassCancelRequest (35=q).

MassCancelResponse

Bases: StrEnum

Tag 531 — result of an OrderMassCancelRequest.

PartyRole

Bases: IntEnum

Tag 452 — role of a party in the NoPartyIDs group.

OrderGroupAction

Bases: IntEnum

Tag 20131 — order-group lifecycle operation (35=UOG).

MarketResult

Bases: StrEnum

Tag 20107 — market outcome on a MarketSettlementReport (35=UMS).

CollateralAmountType

Bases: StrEnum

Tag 1705 — kind of collateral change in a settlement collateral entry.

EventResendRejectReason

Bases: IntEnum

Tag 21004 — why a drop-copy EventResendRequest (35=U3) was rejected.

QuoteRequestType

Bases: IntEnum

Tag 303 — QuoteRequestAck (35=b) request type.

QuoteStatus

Bases: IntEnum

Tag 297 — QuoteStatusReport (35=AI) quote status.

QuoteRequestRejectReason

Bases: IntEnum

Tag 658 — QuoteRequestReject (35=AG) reason.

QuoteConfirmResult

Bases: IntEnum

Tag 21010 — QuoteConfirmStatus (35=U8) outcome.

QuoteCancelResult

Bases: IntEnum

Tag 298 — QuoteCancelStatus (35=U9) outcome.

RFQCancelResult

Bases: IntEnum

Tag 21013 — RFQCancelStatus (35=UB) outcome.

Spelled CANCELLED to match :class:QuoteCancelResult / :class:QuoteStatus even though the dictionary describes tag 21013 value 0 as "CANCELED" — the member name is SDK-chosen and not wire-significant (the wire value is 0).

AcceptQuoteResult

Bases: IntEnum

Tag 21025 — AcceptQuoteStatus (35=UC) outcome.

SubscriptionRequestType

Bases: StrEnum

Tag 263 — market-data / security-status subscription action.

For MarketDataRequest (35=V): 0=snapshot, 1=snapshot plus updates, 2=disable a previous subscription. For SecurityStatusRequest (35=e) only 1 (subscribe) and 2 (unsubscribe) are valid.

MDEntryType

Bases: StrEnum

Tag 269 — side of a market-data book level.

MDUpdateAction

Bases: StrEnum

Tag 279 — action for an incremental market-data entry (35=X).

FIX 5.0SP2 also defines New=0; Kalshi MD does not emit it, so it is intentionally absent (an action='0' is dropped as unknown).

MDReqRejReason

Bases: StrEnum

Tag 281 — why a MarketDataRequest (35=V) was rejected (35=Y).

SecurityTradingStatus

Bases: IntEnum

Tag 326 — per-market trading-status change streamed via SecurityStatus (35=f).

Testing helpers

See Testing for the narrative version.

kalshi.testing

Record/replay mock layer for testing against the Kalshi SDK.

Drop a :class:RecordingTransport (or :class:AsyncRecordingTransport) into the client once to capture real API calls to disk, then swap in :class:ReplayTransport (or :class:AsyncReplayTransport) to run tests offline.

Requests are matched by HTTP method + URL path + sorted query parameters. The KALSHI-ACCESS-SIGNATURE and KALSHI-ACCESS-TIMESTAMP headers are ignored, so signature drift between record and replay does not cause misses.

.. warning::

Recorded fixtures contain the full response **body** AND response
**headers** returned by Kalshi. If you record against an account with
real funds you will write balances, positions, order history, and any
PII the API returns to disk; response headers may carry rate-limit
counters, request IDs, and server timestamps.

:class:`RecordingTransport` (and the async variant) scrub a small set
of high-risk response headers by default — ``Set-Cookie``,
``Authorization``, and anything matching
``(?i)^x-kalshi-.*-(id|key|account|user).*$`` — but **do not assume the
default is sufficient for your environment**. Always ``.gitignore`` the
fixture directory unless you have manually scrubbed the JSON, prefer
recording against the demo environment whenever possible, and pass
``response_header_filter=`` to extend the deny-list.

Usage::

from pathlib import Path
from kalshi import KalshiClient
from kalshi.testing import RecordingTransport, ReplayTransport

# Record once against the real API:
with KalshiClient.from_env(transport=RecordingTransport(Path("fixtures"))) as c:
    c.exchange.status()

# Replay in tests, no network:
with KalshiClient(transport=ReplayTransport(Path("fixtures"))) as c:
    c.exchange.status()  # served from fixtures/GET_trade-api_v2_exchange_status.json

AsyncRecordingTransport

AsyncRecordingTransport(
    dir_path: str | Path,
    *,
    real_transport: AsyncBaseTransport | None = None,
    response_header_filter: ResponseHeaderFilter = None
)

Bases: AsyncBaseTransport

Async equivalent of :class:RecordingTransport.

RecordingTransport

RecordingTransport(
    dir_path: str | Path,
    *,
    real_transport: BaseTransport | None = None,
    response_header_filter: ResponseHeaderFilter = None
)

Bases: BaseTransport

Sync httpx transport that proxies real requests and records each pair to disk.

Wraps any underlying httpx.BaseTransport (defaults to a fresh httpx.HTTPTransport) so real network calls go through, while every request/response pair is buffered in memory and flushed to disk on close() — see #105 for the prior O(N²) per-request rewrite this fixes.

response_header_filter controls which response headers are persisted. The default predicate drops Set-Cookie, Authorization, and any header matching (?i)^x-kalshi-.*-(id|key|account|user).*$ so common credential/identity leaks don't end up in checked-in fixtures. Supply a callable (name, value) -> bool (return True to drop) or an iterable of header names to deny — pass lambda _n, _v: False to keep every header verbatim.

AsyncReplayTransport

AsyncReplayTransport(dir_path: str | Path)

Bases: AsyncBaseTransport

Async equivalent of :class:ReplayTransport.

FixtureNotFoundError

Bases: LookupError

Raised when no recorded fixture matches an incoming request.

ReplayTransport

ReplayTransport(dir_path: str | Path)

Bases: BaseTransport

Sync httpx transport that serves responses from a directory of fixtures.

Reads JSON fixture files written by :class:RecordingTransport. Requests are matched by HTTP method, URL path, and sorted query parameters (body and auth headers are ignored).

Raises :class:FixtureNotFoundError when no matching fixture exists — never returns a synthetic 404 silently.