v2.7.0
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/.actionnarrowed toLiteral(#312).
Mirrors the v2.5#270narrowing onCreateOrderRequest. Previously any
string passed validation and the server rejected with a 400; now invalid
values raisepydantic.ValidationErrorat construction.KalshiConfig.extra_headersimmutable post-construction (#313). The
#298KALSHI-ACCESS-*fence ran once at construction; post-construction
mutation ofextra_headerscould reopen the auth-header forge surface.
extra_headersis now stored asMappingProxyTypeover a defensive copy
—config.extra_headers["k"] = "v"raisesTypeError. Construction-time
fence unchanged.CommunicationsResource.list_rfqs/list_quotesstatuskwarg
narrowed toLiteral(#324). NewRfqStatusLiteral/QuoteStatusLiteral
match the spec's closed set; arbitrarystris rejected by mypy at the
call site. Consistent with the existingOrdersResource.list(status=...)
pattern.to_decimal()rejectsNaN/Infinity(#325). The public helper
promised safety in its docstring but accepted anyDecimal. Now raises
ValueErroron non-finite inputs, matching the_coerce_decimalvalidator.DollarDecimalrequest-side fields reject negative prices and
sub-tick precision (#343).CreateOrderRequest,
AmendOrderRequest,CreateOrderV2Request,AmendOrderV2Requestnow
reject negativeyes_price/no_priceand sub-$0.0001-tick precision
at construction. Response-sideDollarDecimalis unchanged (servers may
emit any value).
Critical (HIGH-severity money/auth/data-integrity fixes)
KalshiClient.from_envpreserves caller ownership ofKalshiAuth
(#311). Thefrom_envclassmethod (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 signThreadPoolExecutorbecause the SDK-built auth
was flagged "not owned"; (b)from_env(auth=my_auth)with env vars set
causedclient.close()to close the caller's still-referenced auth,
raisingRuntimeError("KalshiAuth has been closed")on the next
sign_request. The fix recomputes ownership from__init__'s invariant.- WS
_wait_for_responsewrapsTimeoutErrorasKalshiSubscriptionError
(#314).asyncio.wait_forraises bareTimeoutErroron timeout, which
escapedsubscribe()/unsubscribe()unhandled because only
ConnectionClosedwas caught. Now wrapped withchannel/client_id/
opcontext so consumers branching on SDK exceptions actually see the
expected type. - WS zombie-subscription cleanup on gap-recovery failure (
#315).
broadcast_errornever popped the deadSubscription. A failed
resubscribe_onein_handle_seq_gap(and theKalshiBackpressureError
path) left a zombie sub whose closed queue persisted; the next reconnect's
resubscribe_allresurrected it on the server — silent data loss + a
server-quota leak.broadcast_errornow pops_subscriptionsand
_sid_to_clientso reconnects can't resurrect dead subs.
High-impact correctness
from_envlazytry_from_env()evaluation (#316). The classmethod
eagerly evaluatedKalshiAuth.try_from_env()even when the caller passed
auth=/key_id/private_key/private_key_path. A malformed
KALSHI_PRIVATE_KEYin the process env then raised on every CI worker
that bypassed env. Now gated oncaller_supplied_auth.sign_requeststrips 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-Afterhonored across 408 / 429 / 503 / 504 (#322). Previously
only 429 parsedRetry-After. Per RFC 7231 §7.1.3 the header applies to
all four._parse_retry_afterextracted;retry_afterlifted to
KalshiErrorbase class soKalshiServerErrorandKalshiTimeoutError
also surface the hint.Retry-Afterpath 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 atretry_max_delay. Preserves RFC compliance
(Retry-Afteris a "no sooner than" hint).- Response body size cap on success path (
#323). The#20316 KB
cap applied only to error responses; the success path was unbounded.
New_enforce_response_body_capenforces 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=0parity (#326).subaccount,
exchange_index,subaccount_numberonCreateOrderRequest,
AmendOrderRequest,BatchCancelOrderRequest, and related V1 + group
models now reject negative integers (matching V2 and the#295sweep). Page._columnshandlesNone-first nullable nested columns (#328).
Detection inspected onlycols[0]; aNone-first nullable nested
BaseModelcolumn skipped themodel_dumppass and broke
to_dataframe/to_polarsfor nullable-Struct schemas.- WS
OrderbookDeltaPayload.tstyped asAwareDatetime(#331).
Missed by the v2.5#270WS datetime sweep. Tightening across
orderbook_delta,user_orders, andcommunicationspayloads. - WS backpressure error closes connection cleanly (
#332).
KalshiBackpressureErrorin 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_orderbookpreviously re-validated every price level through
Pydantic on eachapply_delta. Data is SDK-canonical after the snapshot
validation; switched tomodel_constructto skip the redundant pass. - Zero-delta orderbook updates preserve cache (
#347). A no-op delta
(quantity unchanged) used to invalidate the memoizedOrderbookview,
defeating the#244cache. Now skipped when the price-level dict is
unchanged. - Public
apply_snapshotsingle-copy (#344). The public path
allocatedyes/nodicts twice — once via identity in
_apply_snapshot_inplace, then again viadict(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_v2paid the dict-walk serializer twice; the v2.4#223
fast-path was never extended. Now serialize viamodel_dump_jsononce
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.tracksync fast-path (#330). New public
track_synclets 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.timeoutswap (#356). Replacedasyncio.wait_for
in the recv hot path withasync 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
afteraclose()returns, not before — cancellation between the two no
longer leaks the httpx pool. Mirrors the v2.6#301sync fix._delete_with_body(params=...)symmetry (#340). The body-only
variant gains theparamskwarg that_delete_with_body_jsonalready
accepted.- Pagination cycle detector catches non-adjacent loops (
#352). The
prior detector only caught adjacent cursor repeats; anA→B→A→Bloop
from a load-balanced pod-state drift now raises before exhausting
max_pages. Memory-bounded at 1024 seen cursors. - WS
_handle_orphan_subscribedcorrelation (#354). Server
unsubscribedacks for sids the client never owned no longer clobber
unrelated state. - WS
ConnectionManager.reconnectexc_info on failure (#355). Per-
attempt failures now log withexc_info=Trueat 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 intry/finallyso sentinels always
fire even ifclose()raises. KalshiConfig.extra_headersdoubled-pipeline removed (#341).
Previously attached tohttpx.Client(headers=...)defaults AND merged
per-request via_ci_merge. Now_ci_mergeis the single source of
truth; the#298precedence contract (config defaults < per-call
extras < signed auth) is structurally enforced.orders.create()overload tightening (#350). The**kwargs
overload statically requiredactionandcountsince v2.5#242;
the type signature is now updated to match runtime so mypy catches
omissions at the call site.- Documentation drift sweep (
#318,#319,#320,#334,#336,
#337,#338,#339). Three live-data endpoint paths and the
batch()return shape corrected;positions_all()"does not exist"
claim dropped (it shipped in v2.5#269);structured_targets.get()
404 mapping corrected (raisesKalshiNotFoundError, notNone);
sync/async docstring drift normalized acrossorders,markets,
portfolio,communications;CreateOrderV2Requestdocstring
corrected (DollarDecimal, not the nonexistentFixedPointDollars);
LiveDataResource.get_typedclarified as the legacy URL form. KalshiAuth.from_pemOpenSSH-format error (#335). Detecting an
-----BEGIN OPENSSH PRIVATE KEY-----header now raises a targeted
KalshiAuthErrorwith the exactssh-keygen -p -m PKCS8 -f <path>
conversion command instead of the generic PEM parse failure.
Additive
kalshi.OrderPrice— public type alias for the request-side
boundedDollarDecimalused by order-request models (#343).kalshi.RfqStatusLiteral,kalshi.QuoteStatusLiteral— public
Literal aliases for communications status filtering (#324).SequenceTracker.track_sync— public sync entry point for the WS
recv hot path (#330).
Dependencies
pydantic>=2.4(#346). Bumped from>=2.0to ensure the
StrictInt/_coerce_decimalinvariants the SDK relies on get the
2.4+ semantics. Pydantic 2.0–2.3 also shipped JSON-parser bugs.