Skip to content

Releases: TexasCoding/kalshi-python-sdk

v4.0.0

10 Jun 01:07
fad01c0

Choose a tag to compare

Spec-drift reconciliation against the latest upstream OpenAPI (3.20.0) and AsyncAPI
specs (closes #443). Includes one breaking change: the Self-Clearing-Member
"Klear" API migrated from cookie-session login to Bearer-token auth. Everything
else is additive.

Breaking

  • Klear (SCM) auth is now a Bearer token. Upstream removed POST /log_in and
    switched the Klear API to Authorization: Bearer <admin_user_id>:<access_token>.
    KlearClient / AsyncKlearClient now require admin_user_id and access_token
    at construction (or via from_env(), which reads KALSHI_KLEAR_ADMIN_USER_ID /
    KALSHI_KLEAR_ACCESS_TOKEN). Removed: the login() method, the
    is_authenticated property, the client.auth resource, and the LogInRequest /
    LogInResponse models. KlearAuth is now a Bearer-credential holder
    (KlearAuth(admin_user_id, access_token)). Generate a token at
    https://klearing.kalshi.com (the "Security" page).

Added

  • cfbenchmarks_value WebSocket channel — stream CF Benchmarks reference index
    values (e.g. BRTI) with trailing 60-second and final-minute quarter-hour
    averages via KalshiWebSocket.subscribe_cfbenchmarks_value(index_ids=[...]). New
    models CFBenchmarksValueMessage / CFBenchmarksValuePayload /
    CFBenchmarksAvgData / CFBenchmarksIndexListMessage /
    CFBenchmarksIndexListPayload (exported from kalshi.ws.models).
  • AccountResource.upgrade()POST /account/api_usage_level/upgrade to
    request a permanent Advanced API usage-level grant.
  • AccountApiLimits.grants — the per-exchange-lane usage-level grant list, plus
    a new ApiUsageLevelGrant model (exchange_instance / level / source /
    expires_ts), exported from kalshi.
  • MarginAccountResource.api_limits()GET /account/limits/perps for the
    Perps API tier limits (reuses AccountApiLimits).
  • Perps market notional/leverage fieldsMarginMarket gains
    leverage_estimates and volume/volume_24h/open_interest notional-value
    fields; MarginMarketCandlestick and the ticker WS payload gain notional-value
    fields, all tracking the spec.

Changed

  • Re-vendored specs/openapi.yaml, specs/asyncapi.yaml, specs/perps_openapi.yaml,
    specs/perps_asyncapi.yaml, and specs/perps_scm_openapi.yaml; the subaccount
    range documented in prose is now 1–63 (no validation change).

v3.3.0

06 Jun 16:06
bff73a1

Choose a tag to compare

Adds a complete FIX protocol subsystem (FIXT.1.1 / FIX50SP2) for both the
prediction and perps products. Additive release: no changes to the existing REST,
WebSocket, or Perps surfaces.

Added

  • FIX engine (kalshi.fix) — a hand-rolled, async-first FIX client for both
    products: FixClient (prediction) and MarginFixClient (margin, in
    kalshi.perps.fix), re-exported from the top-level kalshi package alongside
    FixConfig, FixEnvironment, and FixSessionType. Covers five session types —
    order entry (NR/RT), drop copy, market data, post-trade settlement (prediction),
    and RFQ (prediction), plus order-group management over the order-entry session —
    on a session state machine with
    RSA-PSS logon (reusing the REST key), heartbeat / test-request liveness,
    sequence tracking with gap-fill / resend on the retransmission sessions, and
    AWS-style full-jitter reconnect. (Epic #402.)
  • Typed FIX messages — Pydantic v2 models for every Kalshi FIX message across
    order entry, order groups, market data, RFQ/quoting, drop copy, and market
    settlement, with DollarDecimal / FixedPointCount money types (no float
    drift) and a central inbound dispatch via decode_app_message.
  • FixOrderBook — aggregated order-book reconstruction from market-data
    snapshots + incrementals; SettlementReassembler — paginated
    settlement-report reassembly.
  • FixSession.on_decode_error hook + decode_app_message_strict — surface a
    registered-but-malformed inbound message instead of silently dropping it (#432).
  • FIX documentation — new docs/fix.md guide, plus FIX coverage in the
    README, errors, authentication, configuration, environment-variables, and the
    API reference.
  • FIX dictionary drift testspecs/kalshi-fix-dictionary.xml checked in
    with a model↔dictionary drift test, the FIX analogue of the REST contract-drift
    suite (#437).

Fixed

  • Documentation accuracy pass: fcm.positions_all() is now documented (the FCM
    page previously stated it did not exist); the portfolio reads document their
    subaccount parameter; the markets.is_block_trade version note is corrected
    to SDK v3.1.0; the OrderPrice and MultiplierDecimal types are documented;
    and the docs landing page's WebSocket channel count is corrected to 11.

v3.2.0

05 Jun 19:25
eed032d

Choose a tag to compare

Adds full SDK support for the Kalshi Perps (margin) API — a separate
perpetual-futures exchange — as standalone clients alongside the existing
prediction-API surface. Additive release: no changes to KalshiClient.

Added

  • PerpsClient / AsyncPerpsClient — standalone clients for the perps
    exchange (external-api.kalshi.com / demo external-api.demo.kalshi.co,
    /trade-api/v2), with their own PerpsConfig and a separate KALSHI_PERPS_*
    credential namespace. They reuse the prediction-API RSA-PSS signer and HTTP
    transport unchanged. The constructors and from_env() also accept
    ws_base_url (set the WS endpoint independently from REST) and password
    (passphrase for an encrypted key), and read KALSHI_PERPS_WS_BASE_URL /
    KALSHI_PERPS_PRIVATE_KEY_PASSPHRASE from the environment; passing config=
    together with demo/base_url/ws_base_url is rejected. Resource families:
    exchange (status / enabled gate /
    risk parameters), markets (list / get / orderbook / candlesticks), orders
    (create / get / list / cancel / decrease / amend + FCM), order_groups,
    portfolio (positions / fills / trades), margin (balance / risk /
    notional risk limit / fee tiers), funding (rate estimate / historical /
    history), and transfers (intra-exchange-instance + margin subaccounts).
    Margin order side is bid / ask; prices are DollarDecimal
    (FixedPointDollars), counts FixedPointCount, and number/double ratios
    (leverage, funding rate, ROE, fee tiers) are MultiplierDecimal (exact
    Decimal, string-serialized) — consistent across the REST, WS, and exchange
    surfaces. Margin-account list responses tolerate a server-returned null.
  • PerpsWebSocket — the perps margin WebSocket
    (external-api-margin-ws.kalshi.com, /trade-api/ws/v2/margin) with six typed
    channels (subscribe_orderbook_delta, subscribe_ticker — carrying
    funding_rate + next_funding_time_ms, subscribe_trade, subscribe_fill,
    subscribe_user_orders, subscribe_order_group). Reuses the event-contract
    WS connection / sequence-gap / backpressure machinery; perps WS timestamps are
    Unix epoch milliseconds (*_ms fields).
  • KlearClient / AsyncKlearClient — the Self-Clearing-Member "Klear"
    settlement API (api.klear.kalshi.com / demo demo-api.kalshi.co,
    /klear-api/v1) with a third auth model: cookie-session + MFA via
    login(email=..., password=..., code=...). Resource margin covers reports,
    active/historical obligations, settlement estimate, settlement + guaranty-fund
    balances, settlement-balance history, and settlement-balance withdrawal.
    Klear money fields are integer centicents; the single real-money write
    (withdraw_settlement_balance) validates a positive amount at construction.
    Credentials and the session cookie are never logged or shown in repr().
  • docs/perps.md (+ mkdocs nav), README "Perps (margin) trading" section, and
    runnable examples/perps_create_order.py / perps_stream_ticker.py /
    perps_balance_risk.py.

Changed

  • Prediction-API list endpoints markets.candlesticks / bulk_candlesticks /
    bulk_orderbooks now validate a typed response envelope: a missing
    spec-required array key raises ValidationError (surfacing spec drift instead
    of silently returning []), while a null array coerces to [] (Kalshi's
    empty-as-null convention — the prior data.get(...) extraction would
    TypeError on a null array). The perps markets.list / markets.candlesticks
    / funding.historical_rates / funding.history responses use the same
    NullableList envelopes, so null-handling is consistent across both surfaces.
    The optional order_groups.list stays tolerant of a missing/null array.

Fixed

  • KalshiWebSocket._stop() now retrieves an already-finished receive-loop's
    exception, so a session torn down after a permanent close no longer logs
    asyncio's "Task exception was never retrieved" on garbage collection.

Internal

  • Vendored the three perps specs (specs/perps_openapi.yaml,
    perps_asyncapi.yaml, perps_scm_openapi.yaml); scripts/sync_spec.py and the
    weekly spec-sync workflow now fetch/diff/checksum them and fold their sha256
    into the drift fingerprint (preserving the contents: read + issues: write
    security model).
  • Parameterized the contract-drift harness per spec: TestPerps*Drift /
    TestPerpsScm*Drift validate the perps REST + SCM surfaces against their own
    specs, alongside the existing prediction-API drift suites.
  • README / docs/index.md banners note the perps surface (34 REST operations,
    6 WS channels, 10 SCM operations).

v3.1.0

04 Jun 00:49
cd9ae6f

Choose a tag to compare

OpenAPI + AsyncAPI spec sync from v3.19.0 → v3.20.0 (#385). Adds the
new GET /events/fee_changes endpoint plus three smaller additive
changes upstream re-published into 3.20.0 after the drift issue was
filed (the OpenAPI checksum in #385 is therefore stale; the AsyncAPI
checksum still matches).

Added

  • events.fee_changes() / fee_changes_all() (sync + async) for
    GET /events/fee_changes — the new paginated event-level fee-override
    feed (event_ticker, limit, cursor). Returns Page[EventFeeChange]
    (and an auto-paginating iterator). EventFeeChange exposes
    fee_type_override / fee_multiplier_override, both present-but-None
    when an override is cleared.
  • is_block_trade query param on markets.list_trades /
    list_all_trades (+ deprecated list_trades_all) and
    historical.trades / trades_all. Omit for all trades, True for
    only block trades, False for only non-block.
  • Trade.is_block_trade (bool) — new spec-required, non-nullable
    response field; a missing key raises so a schema regression surfaces.
  • WebSocket event_fee_update message on the existing
    market_lifecycle_v2 channel (EventFeeUpdateMessage /
    EventFeeUpdatePayload). subscribe_market_lifecycle() now yields
    MarketLifecycleMessage | EventFeeUpdateMessage. Channel count is
    unchanged (still 11) — this is a second message type on an existing
    channel. Behavioral note for existing subscribers: discriminate on
    .type before reading payload fields — an EventFeeUpdatePayload has no
    market_ticker, so naive access raises AttributeError. See the
    migration callout in docs/websockets.md.

Internal

  • specs/openapi.yaml (sha256
    b72a2aa138695d810f6ca85096bfe19e1b66ba5e9b2ed37753be284b5288d271)
    and specs/asyncapi.yaml (sha256
    2f72d0a3fd25fe331210ed300f03ad4c1fedcb561b3ab425046b2dca6f4683ec)
    snapshots bumped; kalshi/_generated/models.py regenerated.
  • Spec also relaxed CreateOrderV2Request.client_order_id and
    EventData.product_metadata from required to optional, and added
    ApiKeyScope / FeeType enums. No SDK-facade change: the V2 order
    keeps client_order_id required by design, Event.product_metadata
    already tolerated server omission, and API-key scopes stays str
    for forward-compat.
  • README + docs/index.md banners bumped to "99 operations … OpenAPI
    v3.20.0".

v3.0.1

26 May 02:11
892943c

Choose a tag to compare

OpenAPI spec sync from v3.18.0 → v3.19.0 (#383). Single additive
request-side constraint on GET /structured_targets: the ids query
param gained maxItems: 2000. Mirrored at the SDK boundary so an
oversize filter fails fast with a clear ValueError instead of paying
a network round trip for a 400. No endpoint, channel, or response-model
changes; regenerated kalshi/_generated/models.py is byte-identical to
the v3.18.0 output.

Changed

  • StructuredTargetsResource.list / list_all and their async
    counterparts now raise ValueError("ids accepts at most 2000 entries per spec ...") when ids exceeds 2000 entries. The 2000 boundary
    is inclusive (matches spec maxItems). Mirrors the existing
    live_data.batch (milestone_ids, max 100) and
    markets.bulk_* (tickers, max 100) precedent.

Internal

  • specs/openapi.yaml snapshot bumped (sha256
    5eaeca6bb64b2ff0aa4f63f9e13381da5a8f6d8f9b34328408499a0503a3085d).
  • README + docs/index.md banners bumped to "OpenAPI v3.19.0".

v3.0.0

22 May 16:59
6837342

Choose a tag to compare

Public-API rename release. Three breaking-rename issues (#348, #349, #351)
that were deferred from the v2.7.0 audit closure now land with one-release
deprecation aliases
: both old and new spellings work in v3.0.0; old names
emit DeprecationWarning and will be removed in a future release (v3.1+).

This is the first release in the v3 line. The wire protocol is unchanged from
v2.7.0; v3 is purely a public Python API ergonomics break.

Migration

See docs/migrations/v2-to-v3.md for full BEFORE/AFTER snippets and a one-page
search-and-replace cheat sheet.

Breaking changes

All three changes ship with @typing_extensions.deprecated aliases on the old
names — existing v2.x callers continue to work, but get a DeprecationWarning
on every call until they migrate.

  • CommunicationsResource sub-namespaces (#348). Flat noun-prefixed
    methods (list_rfqs, get_rfq, create_rfq, delete_rfq, list_all_rfqs,
    list_quotes, get_quote, create_quote, delete_quote, accept_quote,
    confirm_quote, list_all_quotes) are split into two sub-resources matching
    the OpenAPI v3.18.0 tag structure:

    # v2.x (deprecated in v3.0.0)
    client.communications.list_rfqs(...)
    client.communications.create_rfq(...)
    client.communications.accept_quote(quote_id, accepted_side="yes")
    
    # v3.0.0+
    client.communications.rfqs.list(...)
    client.communications.rfqs.create(...)
    client.communications.quotes.accept(quote_id, accepted_side="yes")

    The misc client.communications.get_id(...) stays at the top level (no
    sub-noun). The new sub-resource classes RFQsResource, QuotesResource,
    AsyncRFQsResource, AsyncQuotesResource are exported from kalshi/__init__.py
    for type annotations.

  • MarketsResource.list_trades_alllist_all_trades (#349).
    Standardizes on list_all_<noun> matching the other three resources
    (CommunicationsResource.list_all_rfqs, SubaccountsResource.list_all_transfers,
    etc.). list_trades_all remains as a deprecated alias.

    # v2.x (deprecated in v3.0.0)
    for trade in client.markets.list_trades_all(ticker="..."):
        ...
    
    # v3.0.0+
    for trade in client.markets.list_all_trades(ticker="..."):
        ...
  • OrdersResource.fills / fills_allPortfolioResource.fills /
    fills_all
    (#351). The endpoint URL is /portfolio/fills; this aligns
    the SDK layout with the URL family (portfolio.settlements,
    portfolio.deposits, portfolio.withdrawals). The old OrdersResource.fills
    / fills_all remain as deprecated aliases.

    # v2.x (deprecated in v3.0.0)
    page = client.orders.fills(ticker="...")
    
    # v3.0.0+
    page = client.portfolio.fills(ticker="...")

Polish

  • _fills_params deduped to kalshi/resources/_base.py (was duplicated in
    orders.py and portfolio.py during the relocation).

Internal

  • 78 new regression tests covering the deprecation-alias delegation and warning
    emission across sync + async pairs for every renamed method (12 communications
    forwarders × 2, plus markets + fills × 2 each).
  • tests/_contract_support.py METHOD_ENDPOINT_MAP registers both old and new
    spellings for the duration of the alias window.

Deprecation removal schedule

The v3.0.0 aliases will be removed no sooner than v3.1.0. Callers should
migrate before then. Each deprecated method has a @typing_extensions.deprecated
decorator that surfaces in type checkers and IDEs per PEP 702.

v2.7.0

22 May 14:46
07d240f

Choose a tag to compare

Post-v2.6 independent multi-LLM reviewer audit closure. 47 issues filed
(#311#357) by a 9-reviewer parallel pass
combining 7 internal specialist
agents (security/auth, HTTP transport, WebSocket, models/contracts, sync/async
parity, performance, resources/API) with fresh-eyes external LLM reviews via
the Codex CLI (GPT-5) and Gemini CLI
. 44 issues closed in this release across
4 sequential waves (W0 docs, W1 HIGH integrity, W2 MEDIUM correctness, W3 LOW
polish/perf/deps); 17 PRs merged. 3 breaking-rename issues (#348, #349,
#351) deferred to v3.0.0 milestone. Main is mypy --strict clean, ruff
clean, 2876 unit tests passing (≈100 new regression tests added).

Breaking changes

Five behavioral fences; all surface bugs that were silently wrong or invariants
the SDK now enforces. Per project policy (established by v2.0–v2.6),
bug-surfacing behavioral fences ship in minor releases; intentional public-API
removals or renames are reserved for major releases — that's why the three
breaking renames (#348, #349, #351) are deferred to v3.0.0.

  • AmendOrderRequest.side / .action narrowed to Literal (#312).
    Mirrors the v2.5 #270 narrowing on CreateOrderRequest. Previously any
    string passed validation and the server rejected with a 400; now invalid
    values raise pydantic.ValidationError at construction.
  • KalshiConfig.extra_headers immutable post-construction (#313). The
    #298 KALSHI-ACCESS-* fence ran once at construction; post-construction
    mutation of extra_headers could reopen the auth-header forge surface.
    extra_headers is now stored as MappingProxyType over a defensive copy
    config.extra_headers["k"] = "v" raises TypeError. Construction-time
    fence unchanged.
  • CommunicationsResource.list_rfqs / list_quotes status kwarg
    narrowed to Literal
    (#324). New RfqStatusLiteral / QuoteStatusLiteral
    match the spec's closed set; arbitrary str is rejected by mypy at the
    call site. Consistent with the existing OrdersResource.list(status=...)
    pattern.
  • to_decimal() rejects NaN / Infinity (#325). The public helper
    promised safety in its docstring but accepted any Decimal. Now raises
    ValueError on non-finite inputs, matching the _coerce_decimal validator.
  • DollarDecimal request-side fields reject negative prices and
    sub-tick precision
    (#343). CreateOrderRequest,
    AmendOrderRequest, CreateOrderV2Request, AmendOrderV2Request now
    reject negative yes_price / no_price and sub-$0.0001-tick precision
    at construction. Response-side DollarDecimal is unchanged (servers may
    emit any value).

Critical (HIGH-severity money/auth/data-integrity fixes)

  • KalshiClient.from_env preserves caller ownership of KalshiAuth
    (#311). The from_env classmethod (sync and async) overwrote _auth_owned
    from the input kwarg instead of recomputing from what __init__ actually
    did. Two real bugs: (a) from_env(key_id=..., private_key=...) with no env
    vars set leaked the sign ThreadPoolExecutor because the SDK-built auth
    was flagged "not owned"; (b) from_env(auth=my_auth) with env vars set
    caused client.close() to close the caller's still-referenced auth,
    raising RuntimeError("KalshiAuth has been closed") on the next
    sign_request. The fix recomputes ownership from __init__'s invariant.
  • WS _wait_for_response wraps TimeoutError as KalshiSubscriptionError
    (#314). asyncio.wait_for raises bare TimeoutError on timeout, which
    escaped subscribe() / unsubscribe() unhandled because only
    ConnectionClosed was caught. Now wrapped with channel / client_id /
    op context so consumers branching on SDK exceptions actually see the
    expected type.
  • WS zombie-subscription cleanup on gap-recovery failure (#315).
    broadcast_error never popped the dead Subscription. A failed
    resubscribe_one in _handle_seq_gap (and the KalshiBackpressureError
    path) left a zombie sub whose closed queue persisted; the next reconnect's
    resubscribe_all resurrected it on the server — silent data loss + a
    server-quota leak. broadcast_error now pops _subscriptions and
    _sid_to_client so reconnects can't resurrect dead subs.

High-impact correctness

  • from_env lazy try_from_env() evaluation (#316). The classmethod
    eagerly evaluated KalshiAuth.try_from_env() even when the caller passed
    auth= / key_id / private_key / private_key_path. A malformed
    KALSHI_PRIVATE_KEY in the process env then raised on every CI worker
    that bypassed env. Now gated on caller_supplied_auth.
  • sign_request strips URL fragment (#317). path.split("?")[0]
    stripped query strings but not #fragment. httpx drops fragments before
    sending, producing a guaranteed signature mismatch (401) on any path
    containing #. Now strips both.
  • Retry-After honored across 408 / 429 / 503 / 504 (#322). Previously
    only 429 parsed Retry-After. Per RFC 7231 §7.1.3 the header applies to
    all four. _parse_retry_after extracted; retry_after lifted to
    KalshiError base class so KalshiServerError and KalshiTimeoutError
    also surface the hint.
  • Retry-After path applies jitter (#321). Synchronized clients
    hitting the same 429/503 window would stampede the rate limit at the
    exact server-suggested moment. Full Jitter is now added on top of the
    server floor, capped at retry_max_delay. Preserves RFC compliance
    (Retry-After is a "no sooner than" hint).
  • Response body size cap on success path (#323). The #203 16 KB
    cap applied only to error responses; the success path was unbounded.
    New _enforce_response_body_cap enforces the same protection on
    non-streaming success bodies via a Content-Length pre-check plus a
    post-buffer guard.
  • V1 order request models gain ge=0 parity (#326). subaccount,
    exchange_index, subaccount_number on CreateOrderRequest,
    AmendOrderRequest, BatchCancelOrderRequest, and related V1 + group
    models now reject negative integers (matching V2 and the #295 sweep).
  • Page._columns handles None-first nullable nested columns (#328).
    Detection inspected only cols[0]; a None-first nullable nested
    BaseModel column skipped the model_dump pass and broke
    to_dataframe / to_polars for nullable-Struct schemas.
  • WS OrderbookDeltaPayload.ts typed as AwareDatetime (#331).
    Missed by the v2.5 #270 WS datetime sweep. Tightening across
    orderbook_delta, user_orders, and communications payloads.
  • WS backpressure error closes connection cleanly (#332).
    KalshiBackpressureError in the recv loop previously broadcast sentinels
    and broke, but left the WS open and _running=True. The next
    subscribe_* restarted the recv loop on top of orphan server-side subs.
    Now closes the connection and clears _running / manager refs so the
    next session starts fresh.

Performance

  • Orderbook materialization via model_construct (#327).
    _BookState.to_orderbook previously re-validated every price level through
    Pydantic on each apply_delta. Data is SDK-canonical after the snapshot
    validation; switched to model_construct to skip the redundant pass.
  • Zero-delta orderbook updates preserve cache (#347). A no-op delta
    (quantity unchanged) used to invalidate the memoized Orderbook view,
    defeating the #244 cache. Now skipped when the price-level dict is
    unchanged.
  • Public apply_snapshot single-copy (#344). The public path
    allocated yes / no dicts twice — once via identity in
    _apply_snapshot_inplace, then again via dict(msg.msg.yes) to copy
    defensively. The bypass path (recv hot loop, #296) keeps identity
    adoption; the public path now copies once.
  • V2 batch endpoints use bytes-fast-path (#329). batch_create_v2 /
    batch_cancel_v2 paid the dict-walk serializer twice; the v2.4 #223
    fast-path was never extended. Now serialize via model_dump_json once
    and send the bytes.
  • RSA-PSS / MGF1 / SHA256 config cached (#345). Previously
    allocated per signature. Hoisted to module-level constants — measurable
    on the auth hot path.
  • SequenceTracker.track sync fast-path (#330). New public
    track_sync lets the recv hot loop skip the per-frame coroutine-object
    allocation; the async wrapper remains for callers that need to await on
    the gap path.
  • WS recv asyncio.timeout swap (#356). Replaced asyncio.wait_for
    in the recv hot path with async with asyncio.timeout(...) (Python 3.11+)
    — fewer TimerHandle allocations per frame.
  • method.upper() hoisted out of retry loop (#342). Computed once
    per request instead of once per retry attempt.

Polish

  • AsyncTransport.close() cancellation-safe (#333). Sets _closed
    after aclose() returns, not before — cancellation between the two no
    longer leaks the httpx pool. Mirrors the v2.6 #301 sync fix.
  • _delete_with_body(params=...) symmetry (#340). The body-only
    variant gains the params kwarg that _delete_with_body_json already
    accepted.
  • Pagination cycle detector catches non-adjacent loops (#352). The
    prior detector only caught adjacent cursor repeats; an A→B→A→B loop
    from a load-balanced pod-state drift now raises before exhausting
    max_pages. Memory-bounded at 1024 seen cursors.
  • WS _handle_orphan_subscribed correlation (#354). Server
    unsubscribed acks for sids the client never owned no longer clobber
    unrelated state.
  • WS ConnectionManager.reconnect exc_info on failure (#355). Per-
    attempt failures now log with exc_info=True at DEBUG so root cause
    (auth / TLS / DNS) survives to max-retry.
  • WS _stop() teardown order (#357). Closes the connection FIRST,
    then broadcasts sentinels — late in-flight frames no longer land on
    closed queues. close() wrapped in `try...
Read more

v2.6.0

22 May 01:29
892440e

Choose a tag to compare

Post-v2.5 independent reviewer audit closure (#273 follow-on). 7 issues
(#295#301) identified by a fresh 7-agent parallel review of v2.5.0 across
security, HTTP transport, WebSocket reliability, models/types, REST resources,
performance, and docs/testing. Executed across 3 sequential waves (W0 docs,
W1 money/correctness, W2 polish) in disjoint git worktrees. 7 PRs merged;
main is mypy --strict clean, ruff clean, 2780+ unit tests passing.

Breaking changes

Two behavioral fences; both surface bugs that were already wrong.

  • int request fields reject bool (#295). Per-field StrictInt
    annotation on every money-routing / counting integer of every Request
    model: subaccount, exchange_index, expiration_ts, reduce_by,
    reduce_to, contracts_limit, contracts, from_subaccount,
    to_subaccount, amount_cents, subaccount_number across V1 + V2.
    bool is an int subclass, so a caller passing True/False used to
    silently route to subaccount 1 / transfer 1 cent / decrease by 1 contract
    with no error. Now raises ValidationError at construction. The existing
    buy_max_cost validator (#243) is unchanged. New kalshi.StrictInt
    alias is exported for downstream models.
  • KALSHI-ACCESS-* in extra_headers is rejected (#298). Both
    KalshiClient(..., config=KalshiConfig(extra_headers=...)) at
    construction time and per-request extra_headers= kwargs now raise
    ValueError if any key (case-insensitive) starts with kalshi-access-.
    Previously a caller-supplied 'kalshi-access-key' (lowercase) co-existed
    with the SDK-signed KALSHI-ACCESS-KEY and httpx shipped both raw header
    lines — a forge surface even though the documented contract promises
    auth headers are SDK-managed.

Critical (money-risk fixes)

  • int request fields reject bool (#295, see Breaking).
  • Auth-header forge surface closed (#298, see Breaking). Companion fix:
    _post(json=...) / _put(json=...) / _delete_with_body(json=...) and
    their async mirrors now pin Content-Type: application/json explicitly,
    preventing a caller-supplied 'content-type': 'text/plain' in
    extra_headers from causing httpx to ship a JSON body labelled as
    plain text.
  • WebSocket session re-entry is rejected (#297). KalshiWebSocket._start()
    used to silently rebuild every manager on nested or re-used connect(),
    orphaning the outer session's subscriptions and recv task with no error.
    Now raises RuntimeError with a clear message. _stop() clears the
    manager refs after teardown so the same instance can be cleanly reused
    for a fresh connect() once the prior session exits. A partial connect
    failure (auth/network) also resets state cleanly via a BaseException
    cleanup block, so a failed connect no longer permanently bricks the
    instance.

High-impact correctness

  • KalshiConfig.extra_headers validated at construction (#298). Closes
    the construction-time bypass that survived the per-request guard.
  • Case-insensitive header merge (#298). New _ci_merge ensures a
    caller-supplied 'x-foo' and SDK-set 'X-Foo' collapse to one wire
    entry rather than co-existing.
  • Public OrderbookManager.apply_snapshot() keeps no-aliasing contract
    (#296). Snapshot/delta input messages remain safe to reuse after a
    public apply_snapshot() call — the manager defensively copies the
    adopted dicts. The recv loop continues to skip the copy via
    _apply_snapshot_inplace for the hot-path perf win (#263).

Performance

  • Orderbook snapshot adoption restored to identity (#296). The
    dict(msg.msg.yes) / dict(msg.msg.no) wrappers in
    _apply_snapshot_inplace were silently nullifying the ~5x speedup
    CHANGELOG #263 advertises. For a 200-level book that's 400 needless
    re-hashes/reallocs per snapshot on the recv hot path. Wrappers dropped;
    identity adoption restored on the bypass path.

Polish

  • Sync/AsyncTransport.close() explicitly idempotent (#301). New
    _closed flag matches the KalshiAuth / KalshiClient pattern; second
    and subsequent close() calls are no-ops. Documents the threading scope
    honestly: sync is sequential-safe (worst case: one redundant
    httpx.Client.close(), itself idempotent), async is fully race-free
    under cooperative scheduling.
  • docs/resources/orders.md: removed stale # ActionLiteral, defaults to "buy" inline comment that would have re-introduced the #242 footgun
    for readers (#299).
  • docs/configuration.md: reference table now lists total_timeout,
    ws_ping_interval, ws_close_timeout, and allow_unknown_host; URL
    validation prose updated for the v2.5 default-reject behavior (#300).

Additive

  • kalshi.StrictInt — new public type alias for downstream models that
    want the same bool-rejection guard (#295).
  • kalshi/_constants.py (internal) — holds AUTH_HEADER_PREFIX to
    eliminate drift between _base_client.py and config.py (#298).

v2.5.0

21 May 22:34
cc7947f

Choose a tag to compare

Post-v2.4 multi-reviewer audit closure (#273). 34 issues across security,
HTTP transport, WebSocket reliability, models/types, REST resources,
performance, and docs/testing — identified by a 7-agent parallel review on
top of the v2.4 sweep and executed across 4 sequential waves (W0 docs, W1
money-risk, W2 medium, W3 polish) in disjoint git worktrees. 20 PRs merged;
main is mypy --strict clean, ruff clean, 2742 unit tests passing.

Breaking changes

Two user-visible breakages, both fence-and-forget. Migration in
docs/migration.md v2.4 → v2.5 section.

  • orders.create() kwarg path requires count and action explicitly
    (#242). Previously client.orders.create(ticker=..., side="yes") placed
    a 1-contract live buy because action defaulted to "buy" and count to
    1. Now raises TypeError before any HTTP request. The request=...
    overload is unaffected; CreateOrderRequest.count no longer has a default.
  • Six REST + three WS model fields widened from str/float to Decimal
    (#258, #259). WS: OrderGroupPayload.contracts_limit (str
    FixedPointCount), TickerPayload.dollar_volume and dollar_open_interest
    (strDollarDecimal). REST: Market.floor_strike, Market.cap_strike,
    Event.fee_multiplier_override, MarketLifecyclePayload.floor_strike,
    Series.fee_multiplier, SeriesFeeChange.fee_multiplier (bare Decimal
    or floatDollarDecimal / Decimal via _coerce_decimal). Wire
    format unchanged; consumers must adopt Decimal arithmetic.

Critical (money-risk fixes)

  • WS: validation failure on a sequenced frame no longer silently advances
    the seq watermark
    (#241). Before: a malformed orderbook_delta /
    order_group_update frame was logged + skipped but seq tracking had
    already advanced, so the next legitimate frame matched expected-seq and
    gap detection never fired — local orderbook silently corrupted with no
    resync trigger. After: pre-validate + apply + dispatch is wrapped in a
    try/except that rolls back the watermark on any exception, so the next
    delta triggers a real gap-recovery resubscribe.
  • REST/WS split-environment combinations rejected at construction
    (#239). Before: KalshiClient(demo=True, base_url="https://api.elections.kalshi.com/...") (or the env-var
    equivalent) silently produced a config where REST hit production but WS
    hit demo — a WS-driven strategy could trade real money against a demo
    book. After: KalshiConfig.__post_init__ rejects mismatched REST/WS
    hosts and the constructors raise ValueError with both inputs named.
  • CreateOrderRequest.buy_max_cost validator rejects bool (#243).
    Before: buy_max_cost=True slipped through as 1 (1¢ cap) because
    bool is an int subclass. After: explicit rejection, matching the
    _coerce_decimal invariant set by v2.4's #225.
  • orders.create() no longer silently defaults to 1-contract buy
    (#242, see Breaking changes).
  • Transport retries network-level httpx errors on idempotent verbs
    (#240). ConnectError / NetworkError / RemoteProtocolError /
    ReadError / WriteError now participate in the same RETRYABLE_METHODS
    • backoff + total-timeout loop the timeout branch already uses for
      GET/HEAD/OPTIONS. ConnectError on POST/DELETE is also safe (request
      never reached the wire, mirroring v2.4's #204 PoolTimeout carve-out).
      Other transport errors on non-idempotent verbs still surface immediately
      so the caller can reconcile via client_order_id. New
      KalshiNetworkError raised when retries are exhausted.

High-impact correctness

  • Errors 408 and 504 route to KalshiTimeoutError (#251). Carries
    the "may or may not have committed" semantic from v2.4's #226; callers
    can branch on except KalshiTimeoutError and reconcile.
  • Suppressed error bodies preserve the typed exception class (#252).
    A 429/401/409/422/504 whose body exceeds the 16KB Content-Length cap
    still routes to the right subclass instead of degrading to
    KalshiError. 429 Retry-After is still populated.
  • WS: resubscribe-window stash drained on every gap recovery (#254).
    Frames captured between unsubscribe-ack and the new subscribe-ack are
    replayed through _process_frame for the new sid (filtered by sid;
    others dropped with a debug log). Was previously only drained on full
    reconnect, so per-gap stashes accumulated until the next disconnect.
  • WS: stale orderbook frames no longer mutate the local book (#255).
    Snapshot/delta apply is gated on subscription-existence at the configured
    sid; frames arriving after teardown short-circuit before validation,
    fixing a race where the high-level subscribe_book iterator could read a
    stale book.
  • WS: _OrderbookIterator raises KalshiOrderbookUnavailableError
    instead of yielding an empty Orderbook
    (#257). Before: if
    mgr.get(ticker) returned None mid-resync, the iterator yielded an
    empty book — indistinguishable from a real zero-liquidity market. After:
    typed error surfaces the race so the caller can reattach to a fresh
    iterator after the resync snapshot lands.
  • WS: KalshiBackpressureError carries structured channel / sid /
    client_id / maxsize
    (#256). Matches the structured-error contract
    v2.4's #213 established for KalshiSequenceGapError /
    KalshiSubscriptionError.
  • WS: orphan subscribed acks released (#268). If a subscribe task
    is cancelled between send and ack, the server still completes the
    subscription; the dispatcher now detects the orphan ack (sid present, no
    client_id mapping) and emits unsubscribe so the sid doesn't leak.
  • WS: snapshot payload yes/no required (#268). A malformed
    snapshot now raises ValidationError (which pairs cleanly with #241's
    seq rollback) instead of silently materializing an empty book.
  • Auth: conflicting key inputs rejected explicitly (#249).
    KalshiClient(private_key_path=..., private_key=...) and dual env-var
    (KALSHI_PRIVATE_KEY + KALSHI_PRIVATE_KEY_PATH) now raise instead of
    silently preferring one source.
  • Unknown base_url host fails closed by default (#250). New
    KalshiConfig.allow_unknown_host=False rejects hosts outside
    {api.elections.kalshi.com, demo-api.kalshi.co, localhost, 127.0.0.1, ::1} so a typo like kalsi.com no longer delivers signed requests to
    an attacker. Opt-in via the field or KALSHI_ALLOW_UNKNOWN_HOST=1.
  • Decimal('NaN') / Infinity rejected at the boundary (#270).
    _coerce_decimal calls is_finite() after coercion so a downstream
    arithmetic NaN never ships "NaN" to a real-money order endpoint.
  • Sign-executor close race fixed (#267). _get_sign_executor
    rechecks _closed under the lock so a racing close() can't leave a
    freshly-instantiated ThreadPoolExecutor dangling.

Performance

  • WS recv loop drops per-frame asyncio.Task + asyncio.shield
    (#245). Cooperative pause via Event + 50 ms poll instead of
    allocating a Task / Future / contextvars copy per frame. Inline
    dispatch on the hot path.
  • subscribe_book iterator caches the materialized Orderbook
    (#244). Memoized on _BookState, invalidated by the in-place apply
    helpers; eliminates the O(n log n) sort + 2N OrderbookLevel
    validations per delta that the high-level iterator was paying on top of
    v2.4's #199 in-place fast path.
  • WS snapshot apply collapses to a single dict walk (#263).
    OrderbookSnapshotPayload.yes / .no validate directly into
    dict[Decimal, Decimal] via a BeforeValidator; _apply_snapshot_inplace
    adopts the dict in identity (no rebuild). ~5× faster on a 200-level book.
  • Page.to_dataframe / to_polars built column-oriented (#264).
    Replaces per-row model_dump(mode="python") with a single getattr-driven
    column build. Preserves the v2.4 Decimal contract; nested-model cells
    still dumped per-column so polars Struct inference works.
  • REST has a pluggable JSON loader (#260). New
    KalshiConfig.rest_json_loads mirrors the existing ws_json_loads; set
    to orjson.loads for ~2–3× faster list-endpoint parsing.
  • Signing path skips regex on the common case (#261).
    _normalize_percent_encoding short-circuits when no % appears.
  • Header merge hoisted out of the retry loop (#262). Only
    auth_headers changes per attempt; the 3-way config_extra + per_call_extra + body_headers merge is now precomputed once per
    request, saving N-1 dict copies across retries.
  • MessageQueue._size counter dropped (#271). qsize() derives from
    len(self._buffer) adjusted for the sentinel; two ints saved per put/get
    on the WS hot path.
  • import asyncio hoisted out of AsyncTransport.request (#271).
    Stray per-request sys.modules lookup eliminated.

Configuration knobs (additive)

  • KalshiConfig.rest_json_loads (#260).
  • KalshiConfig.allow_unknown_host + KALSHI_ALLOW_UNKNOWN_HOST env var
    (#250).
  • extra_headers= plumbed through every public REST resource method —
    302 method signatures via codemod (#253). KALSHI-ACCESS-* signing
    headers always win, so callers cannot forge them via this surface.

Typed-exception expansion

  • New KalshiNetworkError (#240) — exhausted retries on a network-level
    httpx error.
  • New KalshiOrderbookUnavailableError (#257) — _OrderbookIterator
    race where mgr.get(ticker) returned None mid-resync.
  • KalshiBackpressureError gains channel / sid / client_id /
    maxsize keyword-only fields (#256).

Models, types, request shape

  • WS user_orders + communications payloads use pydantic.AwareDatetime
    (#270). Closes the gap left by v2.4's #234 REST sweep — naive RFC3339
    strings now raise ValidationError on WS too.
  • V1 CreateOrderRequest enum-style fields narrowed to Literal[...]
    (#270). Closes the V1/V2 strictness gap; ...
Read more

v2.4.0 — multi-reviewer SDK audit closure

21 May 04:22

Choose a tag to compare

Comprehensive multi-reviewer audit (#224) — 33 issues across security, HTTP
transport, WebSocket reliability, models/types, resources, performance,
testing, and documentation. Identified by an 8-agent parallel review; executed
across 4 sequential waves of disjoint git worktrees. The most impactful items
by category:

Critical (silent data loss / silent money corruption fixes)

  • WS orderbook resync after sequence gap (#189). Before: a single dropped
    frame cleared the local book and never asked the server for a fresh
    snapshot — the consumer kept receiving deltas against a permanently-empty
    book. After: _handle_seq_gap drives a real unsubscribe+resubscribe with
    per-sid ticker tracking so all-markets subscriptions are also covered.
  • Page.to_dataframe / Page.to_polars Decimal preservation (#190).
    Before: DollarDecimal / FixedPointCount serializers ran for
    mode='python' too, so DataFrame columns held str and df['price'].sum()
    returned concatenated strings instead of a numeric sum. After: serializers
    use when_used='json'; live Decimal flows through pandas/polars.

High-impact correctness (HTTP + WS + Decimal + V1 orders)

  • DollarDecimal serialization is positional (#191). _decimal_to_str
    uses f'{v:f}' so values like Decimal('1E+10') never reach the wire as
    scientific notation that Kalshi would reject.
  • Retry policy widened (#192). RETRYABLE_STATUS_CODES now includes
    408, 425, and the Cloudflare 5xx range (520–524). POST/DELETE still never
    retry, preserving idempotency.
  • Total wall-clock retry budget (#193). New KalshiConfig.total_timeout
    caps cumulative time spent inside a single request including retries.
    None (default) preserves the legacy unbounded behavior.
  • V1 batch order endpoints surface typed per-leg responses (#194,
    BREAKING). orders.batch_create now returns
    BatchCreateOrdersResponse (was list[Order] that crashed on any failed
    leg). orders.batch_cancel now returns BatchCancelOrdersResponse (was
    None) exposing per-order reduced_by_fp. Migration: upgrade reads from
    response[i] to response.orders[i].order (and check .error).
  • WS generic subscribe() rejects unknown param keys (#195). Was
    silently dropping typos like params={'tickerz': [...]} and subscribing
    the consumer to a much broader stream than intended.
  • WS server-side seq reset detection (#196). SequenceTracker.track
    now distinguishes seq == last (drop) from seq < last (reset → gap
    recovery); was silently dispatching the reset window with no signal.
  • WS fast-fail on permanent close codes (#197). ConnectionClosed with
    codes 1002/3/7-10 or 4xxx now raises KalshiConnectionError immediately
    instead of burning the 10-retry budget on doomed reconnect attempts.
  • WS payload type alignment with REST (#198). *_fp count/size/volume
    fields on every WS payload model now type as FixedPointCount; RFC3339
    timestamps type as datetime. Eliminates silent str+int TypeErrors when
    consumer code mixes REST and WS data.
  • order_group_updates sequence gap recovery (#205). Same resubscribe
    helper as orderbook gaps; was missed events with no signal.
  • WS unsubscribe drops orderbook state (#206). Long-running
    subscribe/unsubscribe cycles no longer leak _BookState entries.
  • ERROR backpressure strategy raises through iterator (#207). Consumer
    async for now raises KalshiBackpressureError instead of terminating
    silently (indistinguishable from a clean close).

Performance

  • WS recv loop stops rebuilding+discarding orderbook snapshots (#199).
    New _apply_*_inplace variants on OrderbookManager skip the O(n log n)
    sort + ~2N OrderbookLevel allocations on the per-frame hot path.
  • Pluggable JSON loader/dumper (#209). KalshiConfig.ws_json_loads /
    ws_json_dumps allow opt-in to orjson / ujson for high-rate
    streaming (default: stdlib json).
  • WS reconnect uses AWS Full Jitter (#221 polish). Matches the REST
    policy; eliminates the thundering-herd window at the capped-delay end.
  • Batch order bodies serialized once (#223 polish). Resource layer
    routes batch_create/batch_cancel through new _post_json / _delete_with_body_json
    bytes helpers that use model_dump_json + httpx content=, skipping one
    full dict-walk per call.
  • _list_all cursor-loop guard is O(1) (#223 polish). Switched from
    unbounded set[str] to single last_cursor (only catches realistic
    server-replay shape).

Security & robustness

  • Response-body buffering bounded (#203). _map_error caps via
    Content-Length (16KB) and truncates the exception message to 1024
    chars. Prevents memory + log-volume blowup on hostile error payloads.
  • base_url validated to include /trade-api/v2 (#202). Misconfigs
    fail at construction instead of producing silent 401s from a corrupted
    signing path.
  • Passphrase-protected PEMs supported (#217). KalshiAuth.from_pem /
    from_key_path / from_env accept password= (str/bytes/callable);
    KALSHI_PRIVATE_KEY_PASSPHRASE env var. Users no longer need to write
    plaintext keys to disk.
  • URL-encoded path segments (#211). _seg() helper applied across
    every resource — user-supplied IDs with /, ?, .. etc. are encoded
    or rejected at the SDK boundary.
  • RecordingTransport scrubs response headers (#220 polish).
    Set-Cookie, Authorization, and X-Kalshi-*-(id|key|account|user) headers
    filtered by default (user-overridable).

Typed-exception expansion

  • New KalshiConflictError (409), KalshiTimeoutError, KalshiPoolExhaustedError
    (#201, #204). 422 routes to KalshiValidationError. httpx.PoolTimeout
    raises KalshiPoolExhaustedError and IS safe to retry on POST/DELETE
    (request never reached the wire) — httpx.TimeoutException raises
    KalshiTimeoutError and preserves the existing POST/DELETE never-retry
    policy (server may have committed).
  • KalshiSequenceGapError + KalshiSubscriptionError carry structured
    channel / sid / last_seq / next_seq / op context (#213).
  • AuthRequiredError default message mentions both
    KALSHI_PRIVATE_KEY_PATH and KALSHI_PRIVATE_KEY (#215).

Configuration knobs (additive, all opt-in)

  • total_timeout (#193)
  • ws_ping_interval, ws_close_timeout (#208)
  • ws_json_loads, ws_json_dumps (#209)
  • http2 install extra (#220 polish; pip install kalshi-sdk[http2])
  • Per-request extra_headers plumbed through transport (#220 polish)

Documentation

  • docs/migration.md now has continuous coverage v1 → v2.3 (was missing
    v2.1→v2.2 and v2.2→v2.3 sections; #200) plus a v2.3→v2.4 section
    documenting #194's breaking shape and the new typed exceptions.
  • README + docs/websockets.md agree on channel count + use real SDK
    method names (#218).
  • New docs/websockets.md Performance section: queue sizing, overflow
    strategy, orjson example, recv-loop threading (#222 polish).
  • docs/configuration.md, docs/environment-variables.md, cancel/delete
    docstrings, and stale audit/predecessor refs cleaned up (#222 polish).
  • pydantic.AwareDatetime adopted on REST response model datetime fields;
    new datetime-semantics note in docs/concepts.md (#221 polish).

Testing

  • WS hardening: 27+ new tests across orderbook resync, seq reset, close
    codes, backpressure signal, unsubscribe cleanup (#231).
  • Phantom-kwarg behavioral coverage parametrized across all 23 Request
    models (#219).
  • Three new bench harnesses: scripts/bench_ws_recv.py,
    scripts/bench_orderbook_delta.py, scripts/bench_request_hot_path.py
    (#223).
  • Integration conftest.py env-bridging moved from import-time mutation
    to a session-scoped fixture for clean test isolation (#223).

Breaking changes summary

Only one user-visible breaking change: orders.batch_create and
orders.batch_cancel return typed response models instead of list[Order]
and None respectively (#194). The V2 family (batch_create_v2 /
batch_cancel_v2) was already shaped this way; the V1 fix brings parity.
Migration in docs/migration.md v2.3→v2.4 section.