All notable changes to kalshi-sdk will be documented in this file.
-
Order.typerenamed toOrder.order_type. Wire format is unchanged (validation_alias=AliasChoices("type", "order_type")accepts both names on deserialization), but any user code reading.typeon anOrderinstance must migrate to.order_type. Rationale: matches the project's existing builtin-shadow-avoidance convention (milestone_type,target_type,incentive_type). Spec v3.13.0 still definestypeas required, so the field is preserved on the wire — only the Python attribute name changed (#91).# Before order = client.portfolio.orders.get(order_id="...") print(order.type) # AttributeError after upgrade # After print(order.order_type)
Version-bump decision (v1.2 vs v2.0) deferred to release cut.
Post-1.0 enhancements and polish. 17 issues closed across four parallel waves of work. No breaking changes to runtime behavior; two mypy-level type-system tightenings are called out below.
-
Model-first request API. Every POST/PUT/DELETE-with-body resource method now accepts a pre-built request model as an alternative to individual kwargs. Backward-compatible — the existing kwarg form continues to work unchanged (#56).
client.orders.amend(request=AmendOrderRequest(order_id=..., yes_price=...)) client.orders.amend(order_id=..., yes_price=...) # also works
Each method has typed
@overloadstubs (32 methods × 2 = 64 stubs) so mypy catches misuse at type-check time, not runtime. -
DataFrame integration.
Page[T]gained.to_dataframe()and.to_polars()methods with optional dependency extras (#12):pip install 'kalshi-sdk[pandas]' # or [polars] or [all]
page = client.markets.list(limit=100) df = page.to_dataframe()
Lazy imports;
Decimalanddatetimepreserved as native types viamodel_dump(mode="python"). -
Record / replay mock transport (
kalshi.testing) for offline integration testing (#13).RecordingTransportproxies real calls and saves request/response pairs as JSON;ReplayTransportserves the fixtures with no network. Sync and async transports both supported; signature/timestamp headers are excluded from fingerprinting so signatures can drift between record and replay.with KalshiClient.from_env(transport=RecordingTransport("fixtures")) as c: c.exchange.status() # records once with KalshiClient(transport=ReplayTransport("fixtures")) as c: c.exchange.status() # offline replay
-
Typed
Literalaliases for fixed-enum kwargs (#50). 13 new aliases exported fromkalshiandkalshi.models:SideLiteral,ActionLiteral,TimeInForceLiteral,SelfTradePreventionTypeLiteral,OrderStatusLiteral,EventStatusLiteral,MarketStatusLiteral,MveFilterLiteral,MveHistoricalFilterLiteral,MultivariateCollectionStatusLiteral,IncentiveProgramStatusLiteral,IncentiveProgramTypeLiteral,SettlementStatusLiteral. mypy now catches typos in resource kwargs at authoring time. -
MkDocs documentation site (#14, #57). Material theme + mkdocstrings; Getting Started, Authentication, WebSockets, Resources, Errors, Migration guides plus auto-generated API reference. GitHub Pages deploy workflow at
.github/workflows/docs.yml. -
Constructor-variant integration tests (#54). Exercises every supported
KalshiClientconstruction path against the demo API (from_env,key_id + private_key_path, in-memory PEM string, pre-builtKalshiAuth,demo=True). Includes an async sibling test to catch signing-path drift. -
Per-method auth guards on
markets.orderbook(#49). Spec walk confirmed this was the only public-resource GET endpoint missing_require_auth(); unauthenticated callers now get a clearAuthRequiredErrorinstead of a confusing 401 from Kalshi.
-
Sync/async dedup refactor (#46). Extracted shared body-builder, query-param-builder, and response-parser helpers across 13 resource modules. Dispatcher logic and request-model construction now exist once per method-pair instead of twice. Sync/async signatures,
@overloadstubs, and theasync for item in client.markets.list_all():ergonomic are all preserved. -
Async
OrdersResource.batch_cancelrouted through shared_delete_with_bodyhelper (#47). Previously called the transport directly, bypassing the sync path's helper; any future retry / error-mapping change to the helper now applies to both transports symmetrically. -
Sync
test_list_alliteration idiom standardized (#48). Sync tests now use the same manual-counter loop as their async siblings; cosmetic only.
- Async
multivariate.lookup_tickers204 spec-drift guard (#72). Sync had a clearRuntimeError("spec drift: ...")on an unexpected 204; async would surface a confusingTypeErrorfrommodel_validate(None). Extracted a shared_parse_lookup_tickers_responsehelper so both paths share the guard.
-
Typed
ExclusionKinddiscriminator on contract drift exclusions (#51). Replaces the free-textreasonsubstring matching intest_exclusion_map_is_currentwith explicitLiteral["body_param", "spec_deprecated", "paginator_handled", "wire_normalization", "kwarg_rename"]classification. All 47 existing exclusions reclassified. -
Nested-model body-drift detection (#52). The drift test now recurses into nested
BaseModelfields (e.g.TickerPairinsideCreateMarketInMultivariateEventCollectionRequest.selected_markets).TickerPair.extra="allow"intentionally preserved becauseLookupPointresponses echo provider keys; the existing pin test documents the carve-out. -
kwarg_renameExclusionKindsplit out ofwire_normalization(#68). Python-naming-hygiene renames (milestone_type,target_type,incentive_type— spec fieldtypeshadows the builtin) are now classified separately from wire-format normalization.
-
Weekly OpenAPI + AsyncAPI spec sync CI (#16). Cron + manual dispatch workflow snapshots both specs, regenerates models, runs ruff / mypy / pytest, opens a PR with version + endpoint diff if any changes detected. Third-party actions pinned to commit SHAs because the workflow holds
contents: write+pull-requests: write. Concurrency-guarded against same-branch races. -
Nightly integration CI against the demo API (#55). Secret-gated, skips cleanly on forks, dedupes failure issues by stable title.
These tighten types without changing runtime behavior. Existing valid calls are unaffected; the type system will now reject calls that would have failed at runtime anyway (or that were always wrong but mypy couldn't see it).
Page[T]TypeVar tightened fromTypeVar("T")toTypeVar("T", bound=BaseModel). Matches the bound already used insidekalshi/resources/_base.pyand the actual usage pattern across the SDK (every concretePage[X]parameterizes with aBaseModelsubclass).- Resource-method kwargs for the 13 enum fields above are now
Literal[...]instead ofstr | None. Callers passing arbitrary strings (typos, in-flight variable values) now fail at mypy-check time.
- 89/89 REST endpoints implemented (sync + async); auth-guard audit complete across public resources.
- 11 WebSocket channels with sequence-gap detection, configurable backpressure, and automatic reconnection.
- 1407 unit tests passing (1455 collected, 48 skipped — up from 899 at
1.0.0), mypy
--strictclean (76 source files), ruff clean. - Drift tests now detect query/path/body/WS-payload schema drift and nested body-model drift.
- Optional extras:
pandas,polars,all,docs.
First stable release. No behavioral changes from 0.15.0 — this release marks the public API surface as stable for semantic versioning.
LICENSEfile (MIT) — declared inpyproject.tomlsince 0.1.0 but not shipped as a file; now included at repo root and in both wheel (dist-info/licenses/LICENSE) and sdist artifacts (#42).README.md— install, sync + async quickstart, env-var auth, demo vs production, public/unauthenticated usage, order placement, WebSocket streaming, error hierarchy, retry policy, pagination (#41).docs/RELEASING.md— one-time PyPI trusted-publisher setup runbook plus cut-a-release procedure..github/workflows/release.yml— tag-triggered release pipeline: tag/version drift check,uv build,twine check, PyPI publish via trusted publishing (OIDC, no token in repo secrets), GitHub Release with CHANGELOG-extracted body and artifacts attached (#15).[project.urls]extended from 3 → 6 keys (Issues,Changelog,PyPI);Documentationretargeted to README anchor (#44).
Development Statusclassifier bumped from3 - Alphato5 - Production/Stable(#43).
- 89/89 REST endpoints implemented (sync + async). 67 with live integration tests; 20 SDK+unit only; 2 auth-gated (demo cannot authenticate); 1 demo-broken (server-side).
- 12 WebSocket message types dispatched with spec-aligned envelope and payload types. 14 integration tests; 3 frame types live-verified.
- 899 unit tests, mypy
--strictclean, ruff clean, contract drift tests on query/path/body/WS-payload schemas.
Closes the payload-type class of bug surfaced (but not resolved) during v0.14.0. v0.14.0 fixed the envelope-type drift (dispatcher routing) but left a parallel payload-type drift intact: SDK modeled _dollars-aliased fields as int and several ts fields as int | None, while demo sends dollar-decimal strings ("0.0100") and RFC3339 date-time strings ("2026-04-19T23:14:30.160405Z"). Pydantic rejected real frames at model_validate, so the dispatcher continued to silently drop every orderbook_delta frame and every user_order frame. Live-captured evidence on demo, fix verified via provoke probes (1 orderbook_snapshot + 1 orderbook_delta + 2 user_order frames round-trip cleanly after the fix).
OrderbookDeltaPayload—price: int→DollarDecimal;delta: int→FixedPointCount(Decimal-backed, specdelta_fpformat);ts: int | None→str | None(RFC3339).OrderbookSnapshotPayload—yes: list[list[int]]/no: list[list[int]]→list[tuple[str, str]](specyes_dollars_fp/no_dollars_fpis[price_in_dollars, count_fp]string pairs withminItems: 2, maxItems: 2). Tuple type enforces the exact-2-element arity that list-of-list silently tolerated; a malformed 3-element row now fails atmodel_validateinstead of crashing the downstream iterator.UserOrdersPayload—yes_price: int | None→DollarDecimal | None;taker_fill_cost,maker_fill_cost,taker_fees,maker_feespromoted fromstr | NonetoDollarDecimal | None(CLAUDE.md price convention).MarketPositionsPayload—position_cost,realized_pnl,fees_paid,position_fee_costpromoted fromstr | NonetoDollarDecimal | None.FillPayload.fee_cost— promoted fromstr | NonetoDollarDecimal | None.MarketLifecyclePayload.settlement_value— promoted fromstr | NonetoDollarDecimal | None.RfqCreatedPayload.target_cost,QuoteCreatedPayload.{yes_bid,no_bid},QuoteAcceptedPayload.{yes_bid,no_bid}— promoted fromstr | NonetoDollarDecimal | None. Completes the CLAUDE.md price convention across every_dollars-aliased WS payload field — downstream consumers doing Decimal math no longer hitTypeError: unsupported operand type(s) for +: 'str' and 'Decimal'.TickerPayload—yes_bid,yes_ask,no_bid,no_askfromint | NonetoDollarDecimal | None.TradePayload—yes_price,no_pricefromint | NonetoDollarDecimal | None.FillPayload—yes_price: int | None→DollarDecimal | None.RfqCreatedPayload.created_ts,RfqDeletedPayload.deleted_ts,QuoteCreatedPayload.created_ts,QuoteExecutedPayload.executed_ts—int | None→str | None(spec saysstring, format: date-time). Caveat: spec-aligned, no live capture. The communications channel was quiet on demo during v0.15.0 work. If demo follows the v0.14.0user_ordersprecedent (emitscreated_ts_msas integer milliseconds instead of the spec'd ISO string), these fields will reject the frame. Monitor with the drift test — a future live capture can confirm or defer toextra="allow"pickup. Matches the v0.14.0 pattern formarket_position/multivariate_lookupenvelope types (spec-inferred, no live evidence).
OrderbookManager.apply_snapshot / apply_delta previously assumed cents integers on the wire and divided by 100 to produce dollar Decimals, and treated quantity as dollar-denominated. With the payload fix, both wire values are already decimal strings (price_dollars for price, delta_fp for count). Manager now uses the Decimal directly, no conversion. Quantity is now correctly a contract count (e.g. Decimal("100")), not a dollar amount.
test_ws_payload_field_type_driftintests/test_contracts.py— parametrized overWS_CONTRACT_MAP, hard-fails if an SDK field's Python type conflicts with the AsyncAPI spec schema type for three specific patterns:_dollars-aliased string field typed asint,date-timestring field typed asint, or array-of-strings field typed aslist[list[int]]. Would have blocked the v0.14.0 envelope-only PR; reduces the blast radius of this class of drift to one parametrized test case per model.- Helpers
_unwrap_annotation,_sdk_type_kind,_spec_property_kind,_ws_field_type_violations— type-kind comparison infrastructure reusable for future spec-alignment work.
Live-captured on demo 2026-04-19: orderbook_snapshot (quiet market, empty rows) and orderbook_delta (price=Decimal('0.0100'), delta='1.00', ts='2026-04-19T23:14:30.160405Z') parse cleanly; user_order placement + cancel frames both parse (yes_price=Decimal('0.0100')). All 1378 unit tests green; mypy strict clean on kalshi/.
- WebSocket dispatcher silently dropped
user_orders/market_positions/multivariateframes becauseMESSAGE_MODELSkeyed on the channel name (plural) instead of the envelopetypeconst (singular per AsyncAPI spec). Confirmed live on demo foruser_ordersvia a provoke probe — demo emits"type":"user_order"singular. Resolved by aligning SDK to spec across all three channels.
scripts/ws_capture.py— one-shot demo WS frame dumper used for evidence gathering. Autoloads.env; refuses non-demo URLs; prints raw JSONL to stdout.scripts/ws_provoke_user_order.py— order-lifecycle probe that subscribes touser_ordersraw WS, places a non-marketable limit order via REST, and captures the resulting frames.- 11 new WebSocket integration tests covering every currently-dispatched message type end-to-end. Each skips cleanly on demo silence within its respective timeout window.
- Hard-assertion drift guard:
test_ws_envelope_type_driftnow fails on any new spec/SDK envelope-type mismatch not on the (currently-empty)_DEMO_DIVERGENCE_ALLOWLIST.
- Envelope-type drift is fixed (the dispatcher now routes
user_order/market_position/multivariate_lookupframes to the correct Message class), but a separate payload-type drift remains.OrderbookDeltaPayload.priceandUserOrdersPayload.yes_priceare typedint— demo sends dollar-decimal strings ("0.0200","0.0100").tsfields across payloads are typedint | None— demo sends ISO datetime strings. Pydantic rejects these frames atmodel_validate, so the dispatcher continues to silently droporderbook_deltaanduser_orderframes. Net user-visible behavior for those channels is unchanged until v0.15.0 fixes the payload types. Of the 14 WS integration tests shipped, onlytest_ws_connect_and_authcurrently passes against live demo; 13 skip on timeout because the dispatcher drops every subscribed payload. Tracked as v0.15.0 in TODOS.md.
Final push to close the North Star goal (every OpenAPI REST operation has SDK + unit + integration tests). Adds 10 endpoints across 5 new resources plus 2 extensions to existing ones.
New resources (5):
AccountResource—GET /account/limitsreturns API tier limits (usage_tier, read_limit, write_limit) for the authenticated user. Wired asclient.account.StructuredTargetsResource—GET /structured_targets(paginated list with ids/type/competition filters, page_size 1–2000) andGET /structured_targets/{id}. Structured targets are external entities (players, teams, tournaments) markets can anchor to;detailsis flexible JSON keyed by target type. Wired asclient.structured_targets.typequery param renamed totarget_type(avoid Python built-in shadow).FcmResource—GET /fcm/ordersandGET /fcm/positions, both filtered by requiredsubtrader_id. FCM-only endpoints; response envelopes reuse existingOrderandPositionsResponseshapes. Wired asclient.fcm.SearchResource—GET /search/tags_by_categories(category → tag-list mapping) andGET /search/filters_by_sport(sport → filter/competition mapping + display ordering). Both unauthenticated. Wired asclient.search.IncentiveProgramsResource—GET /incentive_programs(status/type/limit filters up to 10 000) withIncentiveProgrammodel (centi-centperiod_reward, nullablediscount_factor_bpsandtarget_size_fp). Unique wire shape: this endpoint paginates onnext_cursor(notcursorlike every other Kalshi endpoint); resource hand-rolls Page wrapping to handle the difference.typequery param renamed toincentive_type. Wired asclient.incentive_programs.
Extensions (2):
exchange.user_data_timestamp()—GET /exchange/user_data_timestampreports the upper bound of lag between exchange state and user-scoped REST endpoints (GetBalance, GetOrders, GetFills, GetPositions). Combine with WebSocket feeds for a live view. NewUserDataTimestampmodel.portfolio.total_resting_order_value()—GET /portfolio/summary/total_resting_order_value. FCM-member only (spec: "intended for FCM members, rare"); non-FCM accounts receive 403 on both demo and prod. Integration test marked@pytest.mark.integration_real_api_only— skipped under the default run, opt-in viaKALSHI_ENABLE_REAL_API_ONLY=1.
- Coverage harness resource count: 14 → 19. All new resources register in
RESOURCE_MODULESandSCENARIO_REGISTRY. - Contract map entries: +7 (UserDataTimestamp, AccountApiLimits, StructuredTarget, SportFilterDetails, ScopeList, IncentiveProgram, TotalRestingOrderValue).
- METHOD_ENDPOINT_MAP entries: +13 covering all new sync resource methods; async siblings auto-derived.
- EXCLUSIONS entries: +16 (type-rename shadow avoidance on structured_targets/incentive_programs, paginator-handled cursors on new list_all methods).
- Demo verification. All 10 endpoints reach demo; 2 are auth-gated (total_resting_order_value + FCM endpoints with non-FCM account). Verified against Path B audit (2026-04-18).
- 45 new unit tests across 7 new/extended test modules.
- 25 new integration tests (22 run on demo; 3 gated behind
integration_real_api_only). - Total suite: 827 → ~900 tests.
- FULL-covered REST endpoints: 57 → 67 (75%+); the remaining gaps are all WebSocket (deferred to v0.14.0).
include_latest_before_startis now tri-state —candlesticksandbulk_candlesticks(sync + async) previously mappedFalsetoNone(dropped), which meant callers explicitly opting out silently accepted whatever the server default happened to be. Now:True → "true",False → "false",None → drop. Same patternlive_dataalready uses;_bool_parampromoted tokalshi/resources/_base.pyas the shared helper. Two new wire-shape tests cover theFalse→"false"case (sync and async)._orderbook_from_itemraises on missing per-item ticker — previously returnedOrderbook(ticker=""), silently corrupting caller-side lookups when the server response omitted the field. Now raisesValueErrorwith the offending item. Regression test added.- Upper-bound validation on bulk methods —
bulk_candlesticks,bulk_orderbooks, andlive_data.batchnow raiseValueErrorwhen passed > 100 entries (specmaxItems). Saves a wasted round-trip on a request the server would reject. Two new tests per resource. Followup fix:bulk_candlesticksoriginally only validatedlist | tupleinputs; a pre-joined comma-separated string with 150 tickers bypassed the guard. Validation now counts commas on the joined form and catches both input shapes uniformly. New test:test_bulk_candlesticks_rejects_over_100_string. - API key leak sweep moved to
tests/integration/conftest.py— ascope="session", autouse=Truefixture insidetest_api_keys.pyonly applies to tests collected from that module. Moved to the integrationconftest.pyso the sweep runs on every integration session regardless of which test files are selected.API_KEY_LEAK_PREFIXalso lives in conftest now; test_api_keys.py imports it. _delete_with_retrydocstring — said "3 attempts," but the loop iterates 4 times ([0.0, 0.25, 0.5, 1.0]). Docstring and module header now accurately say "4 attempts (immediate + 0.25s/0.5s/1.0s backoff)."- Minor —
import timemoved to the top oftests/integration/test_markets.py; asyncLiveDataResource.batch/get_typed/game_statsnow have docstrings matching their sync counterparts; added a comment explaining themilestones.get()data.get("milestone", data)fallback.
Followup polish (second review round):
_orderbook_from_itemdict fallback is now key-presence, not truthiness — the previousitem.get("orderbook_fp") or item.get("orderbook", {}) or {}treated an empty-dict"orderbook_fp": {}as falsy and fell through to the legacy"orderbook"key, quietly blending two different server shapes. Now checks"orderbook_fp" in itemfirst and only uses the legacy key whenorderbook_fpis actually absent.LiveDataResource.get_typedparameter rename —type→milestone_type(sync + async). The former shadowed the Python built-in and bit in closures/lambdas. Value still populates the{type}path segment. Breaking for pre-release callers using the kwarg form (live_data.get_typed(type=...)); positional callers are unaffected. Drift test exclusions updated.- Async
AsyncMarketsResource.bulk_candlesticksdocstring — sync had the spec-constraint + wire-format note; async was missing it. Added. _delete_with_retry/_async_delete_with_retrylast_exc sentinel —last_excwas assigned only inside theexceptbranch, technically unbound on an empty loop. SentinelRuntimeError("no delete attempts executed")assigned pre-loop.
Followup polish (third review round):
_orderbook_from_itemerror wording —not tickercatches both missing-key and empty-string cases. Error message now says "has empty or missing 'ticker' field" instead of "missing required 'ticker' field" to match both paths. Regression-test match string updated.MilestonesResource.list/list_alltyperename — same built-in-shadow fix asget_typed:type→milestone_type(sync + async). Wire still sends?type=.... Drift-test EXCLUSIONS updated for both methods. Internal unit testtest_list_sends_filtersupdated to use the new kwarg name.GetMilestonesResponse.milestonesnow usesNullableList[Milestone]— envelope-level list was a plainlist[Milestone]while nested lists onMilestoneitself usedNullableList. Consistency fix: if Kalshi ever returns{"milestones": null}during an outage or empty result, parsing coerces to[]instead of raising Pydantic validation error.AsyncMarketsResource.bulk_orderbooksdocstring — sync had the spec-constraint + wire-format note; async was missing it. Added.live_milestonefixture exception collapse —except (KalshiNotFoundError, KalshiError)had a dead first branch (KalshiNotFoundErroris a subclass ofKalshiError). Collapsed toexcept KalshiErrorwith a comment explaining both paths are caught.
Followup polish (fourth review round):
GetApiKeysResponse.api_keysnow usesNullableList[ApiKey]— last remaining envelope-level list in this PR using plainlist[ApiKey]. Brings the API Keys envelope in line withGetMilestonesResponse,GetLiveDatasResponse, and the Milestone/LiveData nested lists, so a server-sent{"api_keys": null}coerces to[]instead of raising a PydanticValidationError. Regression test added:test_list_handles_null_api_keys.CreateApiKeyRequest.public_keydocstring — now specifies "PEM-encoded RSA public key" so callers know the expected format without having to round-trip to the server.
Followup polish (fifth review round):
ApiKey.scopesandMarketCandlesticks.candlesticksnow useNullableList— last two remaining barelist[T]fields on response models in this PR. Swept for consistency with the rest of the SDK. Server-sentnullfor either field now coerces to[]instead of raising PydanticValidationError. Two new regression tests:test_list_handles_null_scopesandtest_bulk_candlesticks_handles_null_candlesticks.
Followup polish (sixth review round):
GenerateApiKeyResponse.private_keyis nowpydantic.SecretStr— the PEM private key field is returned once and never retrievable again. Plainstrwould appear verbatim in anyrepr(),str(), or incidental log call of the response model. Wrapping withSecretStrmasks it as'**********'in those contexts; callers retrieve the PEM viaresponse.private_key.get_secret_value(). Docstring updated with usage note. Breaking for pre-release callers accessingresponse.private_keydirectly as a string (integration test + 3 unit tests updated in this PR)._orderbook_from_itemredundantor []removed —ob.get("yes", [])already returned[]on missing key, making the thirdor []clause dead. Simplified toob.get("yes_dollars") or ob.get("yes") or []for cleaner reading._iso()docstring clarifies string passthrough — callers passing pre-stringified dates must ensure RFC3339 compliance themselves. Onlydatetimeinputs get the UTC coercion guarantee.
Followup polish (seventh review round):
MilestonesResource.get()now usesGetMilestoneResponse— the envelope model existed only for contract-map purposes; the resource bypassed it viadata.get("milestone", data). Now usesGetMilestoneResponse.model_validate(data).milestonefor consistency with every other envelope-in-use pattern. A server response missing"milestone"now raises PydanticValidationErrornaming the field — clearer than the old fallback's silent whole-dict revalidation._orderbook_from_itemnow raisesKalshiErrorinstead ofValueError— malformed server response is a protocol violation, not a user error. Matches the SDK-wide "catchKalshiErrorto handle SDK errors" contract. Regression test updated; three new direct unit tests for the helper cover missing-key, empty-string-ticker, and happy-path shapes.bulk_candlesticksticker count now splits + filters empty segments — a pre-joined string like"A,B,,"previously counted as 4 (comma count + 1); now counts 2 real tickers. Tightens the 100-ticker cap against trailing/consecutive comma bypasses without waiting on the deferred_join_tickersvalidation work.MilestonesResource.listRFC3339 docstring — call-site now surfaces the_iso()string-passthrough limitation: passdatetimefor guaranteed UTC, strings travel verbatim.
Followup polish (eighth review round):
- [MED] Async
bulk_candlesticksticker-count bug — the split+filter ticker-count fix from the seventh round only landed on the sync path; the async counterpart still used the naivejoined.count(",") + 1formula. Trailing-comma strings like"A,B,,"would spuriously fail async calls withValueError(counted as 4, not 2);,.join([""] * 99) + "A,B"would wrongly pass (100 commas, 2 real tickers). Now sync and async share the samesum(1 for t in joined.split(",") if t.strip())counter. Three new async regression tests inTestAsyncMarketsBulkCandlesticksValidation` cover over-100 list, over-100 string, and trailing-comma happy path.
- API Keys resource —
ApiKeysResource+AsyncApiKeysResourcecovering all 4/api_keysendpoints for programmatic credential management:GET /api_keys— list keys registered on the accountPOST /api_keys— register a caller-minted RSA public keyPOST /api_keys/generate— have Kalshi mint a fresh key pair; private key is returned ONCE and cannot be retrieved againDELETE /api_keys/{api_key}— remove a key
- Bulk / batch market endpoints on
MarketsResource— three multi-ticker read paths:list_trades+list_trades_all—GET /markets/trades(paginated Trade listing across all markets; reuses the existinghistorical.Trademodel since the schema is shared)bulk_candlesticks—GET /markets/candlesticks(up to 100 tickers per call, comma-joined on wire per spectype: string)bulk_orderbooks—GET /markets/orderbooks(auth-required;tickersserialized as repeated params per specstyle: form, explode: true)
- Milestones resource —
MilestonesResource+AsyncMilestonesResource:GET /milestones— paginated listing with filters for category, competition, type, related_event_ticker, source_id, minimum_start_date (RFC3339), min_updated_ts (Unix seconds).limitis required (1-500) per specGET /milestones/{milestone_id}— single milestone lookuplist_allpaginator helper
- Live Data resource —
LiveDataResource+AsyncLiveDataResourcecovering 4 endpoints keyed bymilestone_id:get—GET /live_data/milestone/{milestone_id}(preferred shape)get_typed—GET /live_data/{type}/milestone/{milestone_id}(legacy shape, retained for spec-completeness; docstring recommendsget)batch—GET /live_data/batch(up to 100 milestone_ids; wire format?milestone_ids=a&milestone_ids=bvia httpx list-explosion)game_stats—GET /live_data/milestone/{milestone_id}/game_stats(returnspbp: Nonefor unsupported milestone types without a Sportradar ID)
- 11 new Pydantic models —
ApiKey+ 5 API-key request/response envelopes;Milestone+ 2 response envelopes;LiveData,PlayByPlay,PlayByPlayPeriod, and 3 live-data response envelopes;MarketCandlesticks(per-market bundle in the bulk candlesticks response). Request models useextra="forbid"; response models useextra="allow". Milestonedetailsand LiveDatadetailsaredict[str, Any]per specadditionalProperties: true(shape varies by milestone type). - 82 new unit tests — 25 for API Keys, 12 for Milestones, 16 for LiveData, 7 for bulk markets, 2 client-wiring additions. Plus real-lifecycle integration coverage: API Keys mints a throwaway RSA keypair in-test and runs
create → list → deleteon demo with try/finally cleanup; bulk methods + Milestones + LiveData all exercise against demo inventory.
- Test coverage — FULL-covered endpoints 44 → 57 (64%). Meta-coverage test now expects 14 resource classes (was 11). Three new resources (
ApiKeysResource,MilestonesResource,LiveDataResource) + 4 new methods onMarketsResourceregistered inMETHOD_ENDPOINT_MAP(13 new entries),BODY_MODEL_MAP(2 new request-body entries forCreateApiKeyRequest/GenerateApiKeyRequest),_contract_map.py(8 new response-side entries),coverage_harness.RESOURCE_MODULES(3 new modules), andtest_coverage.pyimport list. - EXCLUSIONS expanded — 2 new
cursorpaginator entries forMarketsResource.list_trades_allandMilestonesResource.list_all(paginator-handled; not caller-facing). - Live-demo finding documented in the integration suite:
GET /milestones?category=Sportsreturns milestones withcategory="sports"(lowercase) in the response body even though the filter accepted the title-cased input and the spec example shows"Sports".test_list_with_categoryasserts case-insensitively so future server-side case fixes don't regress.
- Communications / RFQ resource —
CommunicationsResource+AsyncCommunicationsResourcecovering all 11 endpoints of the RFQ + Quote subsystem (OTC market access):GET /communications/id— caller's public communications IDGET /communications/rfqs,POST /communications/rfqs,GET /communications/rfqs/{rfq_id},DELETE /communications/rfqs/{rfq_id}— RFQ lifecycle (pluslist_all_rfqspaginator)GET /communications/quotes,POST /communications/quotes,GET /communications/quotes/{quote_id},DELETE /communications/quotes/{quote_id}— Quote lifecycle (pluslist_all_quotespaginator)PUT /communications/quotes/{quote_id}/accept,PUT /communications/quotes/{quote_id}/confirm— two-party workflow
- Subaccounts resource —
SubaccountsResource+AsyncSubaccountsResourcecovering all 6 endpoints for multi-account workflows:POST /portfolio/subaccounts— spin up the next numbered subaccount (empty body; demo requires explicitContent-Type: application/json, SDK sendsjson={}to force it)POST /portfolio/subaccounts/transfer— move cents between subaccounts with client-side idempotency IDGET /portfolio/subaccounts/balances,GET /portfolio/subaccounts/transfers(+list_all_transfers) — read statePUT /portfolio/subaccounts/netting,GET /portfolio/subaccounts/netting— netting configuration
- New Pydantic models — 13 for Communications (
RFQ,Quote,MveSelectedLeg, 5 response envelopes, 3 request models, 2 id wrappers) + 8 for Subaccounts (SubaccountBalance,SubaccountTransfer,SubaccountNettingConfig, 3 response envelopes, 2 request models). Request models useextra="forbid"so phantom keys fail at construction time; response models useextra="allow". integration_real_api_onlypytest marker — new marker for endpoints the demo server cannot service (auth-gated role requirements, demo-broken routes). Thepytest_collection_modifyitemshook intests/integration/conftest.pyauto-skips these tests unlessKALSHI_ENABLE_REAL_API_ONLY=1is set. Applied to 4 tests spanning Communications (list_quotes_unfiltered,list_all_quotes,list_quotes_by_rfq,accept_and_confirm_quote) + Subaccounts (get_netting— demo returns 500).- 103 new tests — 64 unit tests for Communications (
tests/test_communications.py: model aliases, request wire-shape, happy/error paths per method, async, auth guards, client wiring) + 39 unit tests for Subaccounts (tests/test_subaccounts.py: same matrix). Plus 16 integration tests for Communications + 14 for Subaccounts against the demo server.
- Test coverage — FULL-covered endpoints 31 → 44 (52%); partial coverage (SDK + unit, no integration) expanded across the v0.11.0 scope. Meta-coverage test now expects 11 resource classes (was 9).
CommunicationsResourceandSubaccountsResourceboth registered inMETHOD_ENDPOINT_MAP(20 new entries),BODY_MODEL_MAP(5 new request-body entries),_contract_map.py(10 new response-side entries), andcoverage_harness.RESOURCE_MODULES. - EXCLUSIONS expanded — 3 new entries covering
CreateRFQRequest.contracts_fp(integer form only, matching thecount_fpprecedent),CreateRFQRequest.target_cost_centi_cents(deprecated in spec), and thecursorpaginator kwargs on the 3 newlist_all_*methods (2 communications + 1 subaccounts). - Live-demo findings refined the v0.11.0 audit:
GET /communications/quotesrequirescreator_user_idORrfq_creator_user_ideven whenrfq_idis provided — demo returns400 "Either creator_user_id or rfq_creator_user_id must be filled". Supersedes the audit's "403 unless filtered by rfq_id" note; alllist_quotesvariants areintegration_real_api_only.- Demo rejects malformed IDs with
400 invalid_parametersbefore the route-level 404 lookup, so the 404 regression tests assert the baseKalshiErrorclass to tolerate either shape. - Demo refuses self-quoting (RFQ creator responding to their own RFQ) with
400—test_quote_lifecycleskips with a descriptive reason rather than failing, so a future demo-server change surfaces organically.
_put()now handles 204 No Content.SyncResource._put/AsyncResource._putpreviously calledresponse.json()unconditionally and raisedJSONDecodeErroron empty-body responses. Mirrors the_delete()pattern — returnsNoneon 204. Required by the newaccept_quote/confirm_quoteendpoints, which return204on success per spec. Closes the P3 reliability item flagged on PR #33.
- Order Groups resource —
OrderGroupsResource+AsyncOrderGroupsResourcecovering 7 endpoints for rolling 15-second contracts-limit groups (OCO/if-then strategies):GET /portfolio/order_groups— list groups on the account (plainlist[OrderGroup], no pagination)GET /portfolio/order_groups/{order_group_id}— full group including member order IDsPOST /portfolio/order_groups/create— create a new group withcontracts_limit: intDELETE /portfolio/order_groups/{order_group_id}— cancel all member orders and delete the groupPUT /portfolio/order_groups/{order_group_id}/reset— reset the matched-contracts counterPUT /portfolio/order_groups/{order_group_id}/trigger— cancel all member orders, block new ones until resetPUT /portfolio/order_groups/{order_group_id}/limit— update the rolling-15s limit (nosubaccountkwarg — spec explicitly omits the query param on this endpoint)
- 5 new Pydantic models —
OrderGroup,GetOrderGroupResponse,CreateOrderGroupResponse(responses withextra="allow"),CreateOrderGroupRequest,UpdateOrderGroupLimitRequest(request models withextra="forbid").GetOrderGroupResponse.ordersusesNullableList[str]to handle Kalshi's intermittentnull-vs-array responses on spec-required list fields. - 9 integration tests against the demo server — 5 sync + 4 async, exercising create → get → update_limit → reset → trigger → delete flow with
ephemeral_grouptry/finally cleanup fixture. Demo probing during the audit surfaced two real SDK bugs that were fixed before ship: (1)reset/triggerPUT requests were missingContent-Type: application/jsonbecause httpx omits the header when no body is passed; (2) asynccreate → getneeded a 0.5s sleep for demo eventual consistency (matches the existingtest_orders.pypattern). - 41 new unit tests (
tests/test_order_groups.py) — wire-shape coverage across all 7 methods sync + async, 5 response-model alias tests, 6 request-model serialization/validation tests, 7 auth-guard regression tests, 2 client-wiring tests. Unit tests explicitly assertrequest.content == b"{}"onreset/triggerto lock in the httpxContent-Typefix. - Path B demo-feasibility audit — new reusable script
scripts/audit_demo_feasibility.pythat probes every spec endpoint not yet inMETHOD_ENDPOINT_MAPagainst demo and classifies each asdemo-supported/demo-501/auth-gated/demo-broken. The audit informed the corrected v0.10-v0.13 scope in TODOS.md (path corrections:POST /create,PUTfor reset/trigger/limit; API Keys is 4 endpoints not 5; RFQ quotes list is auth-gated on demo; subaccounts/netting GET is demo-broken with a 500).
- Test coverage — FULL-covered endpoints 24 → 31 (35%), not-implemented 53 → 46 (52%). Meta-coverage test now expects 9 resource classes (was 8). New
OrderGroupsResourceregistered inMETHOD_ENDPOINT_MAP(7 entries),BODY_MODEL_MAP(2 entries for request bodies), and the integration coverage harness. - EXCLUSIONS expanded — 2 new entries for
contracts_limit_fpon both order-group request models. The SDK commits to the integercontracts_limitwire form (same precedent ascount_fpon order requests); the string FixedPointCount variant is deliberately absent from the SDK surface. - TODOS.md drift corrections — v0.11 Communications/RFQ block now lists all 11 endpoints with per-endpoint demo classification;
POST /portfolio/subaccountsdocumented as returning 201 on empty body (audit probe created subaccount #1 with $0 on demo — integration tests will need a cleanup fixture); API Keys v0.12 count corrected from 5 to 4.
- Version drift —
pyproject.tomlbumped from 0.9.1 to 0.10.0 to trackkalshi/__init__.py. The 0.9.1 release shipped with the same drift; this release fixes both together.
NullableList[T]— new reusable Pydantic type alias inkalshi.typesfor response-model list fields that the live API may return as JSON null. Applied across 24 list-default fields in response models (events, exchange, markets, multivariate, portfolio, series). Replaces a one-offfield_validatorpattern with a systematic opt-in: any new response field that could be null from the server usesNullableList[X] = []instead oflist[X] = [].- Integration test coverage for Series + Multivariate Collections resources (v0.9.0 scope). 11 previously-unregistered methods now have real tests against the Kalshi demo server:
SeriesResource:list,get,fee_changes,event_candlesticks,forecast_percentile_historyMultivariateCollectionsResource:list,list_all,get,create_market,lookup_tickers,lookup_historyEventsResource:list_multivariate,list_all_multivariate
- Meta-coverage test (
tests/integration/test_coverage.py) now discovers all 8 resource classes (was 6) and fails on any public method that lacks an integration scenario. FULL-covered endpoints: 13 → 24. NullableListregression tests — 7 new unit tests intests/test_series_models.pycovering null coercion on Series (tags,settlement_sources,additional_prohibitions),EventCandlesticks(market_tickers,market_candlesticks), andForecastPercentilesPoint(percentile_points).- Annotation-aware assertion oracle tests — 6 new tests in
tests/integration/test_assertions.pypinning_annotation_containssemantics across bare types,Optional, PEP 604 unions,list[T], andNoneannotations. Plus 2 positive tests confirming float-annotated fields no longer misfire the DollarDecimal check.
- Semantic oracle (
tests/integration/assertions.py) is now annotation-aware. The oracle previously rejected any float value on a Pydantic model as "DollarDecimal parsing failed", which misfired on legitimately-typed fields likeSeries.fee_multiplier: float(spec typenumber/double). It now only flags floats where the field's type annotation actually resolves toDecimal, via a new_annotation_contains()helper that walks__args__throughOptional,Union,Annotated, and generic aliases. tests/integration/test_multivariate.py— tightened except clauses ontest_create_marketandtest_lookup_tickers(sync + async). Previously caughtKalshiServerErroraspytest.skip, which masked real SDK regressions (body serialization, PUT/POST auth) as demo flakiness. Now only swallowsKalshiValidationErrorandKalshiNotFoundError; 5xx fails loud so the integration suite actually serves its north-star purpose of surfacing real SDK issues.
- Stale
__version__inkalshi/__init__.py(was0.7.0, now0.9.1).pyproject.tomlwas bumped to0.8.0in the previous release without updating the package__version__. Both now track together. TODOS.mdrestructured around the north-star goal: 100% endpoint coverage (SDK + unit + integration test for every REST operation and WebSocket channel). New phased roadmap v0.9 → v0.13.BACKLOG.mdadded as the parking lot for valuable-but-off-path items.
orders.create()— removed phantomtypekwarg. Thetypefield was never in the OpenAPI spec; Kalshi silently ignored it. Callers passingtype="limit"(or"market"etc.) now get aTypeErrorat call time. Remove the kwarg from your call sites.orders.create()—buy_max_costtype changed. Nowint | Nonerepresenting cents (e.g.,buy_max_cost=500for a $5.00 cap). Previously typedDollarDecimal. Spec says cents atcomponents.schemas.CreateOrderRequest. Passing aDecimalorfloatraisesValidationError(via afield_validator). Passing a fractional string like"5.5"raises; integer strings like"500"coerce as before.orders.batch_cancel()— signature change. Previously:batch_cancel(order_ids: list[str]). Now:batch_cancel(orders: list[BatchCancelOrdersRequestOrder] | list[str]). Callers passing a plain list of order-id strings still work via the convenience path — each string is wrapped internally as aBatchCancelOrdersRequestOrder. Callers passingorder_ids=[...]as a kwarg must rename toorders=[...].- Wire body normalization —
count_fpreplacescount.orders.create()andorders.batch_create()now emitcount_fp(Decimal string) instead ofcount(int) on the wire, matching the convention already used byorders.amend(). Kalshi accepts both keys per spec; the SDK standardizes oncount_fpfor a single wire shape across methods. MITM proxy tests inspecting wire bytes need to update expectations. orders.batch_cancel()wire field flip. Previously SDK sentbody={"ids": [...]}— the spec-deprecated field. Now sendsbody={"orders": [{"order_id": "..."}, ...]}— the spec-preferred field that also supports per-order subaccount routing.- Every POST/PUT/DELETE request body is now a Pydantic model with
extra="forbid".orders.create,orders.amend,orders.decrease,orders.batch_create,orders.batch_cancel,multivariate.create_market,multivariate.lookup_tickersroute body construction throughCreateOrderRequest,AmendOrderRequest,DecreaseOrderRequest,BatchCreateOrdersRequest,BatchCancelOrdersRequest,CreateMarketInMultivariateEventCollectionRequest,LookupTickersForMarketInMultivariateEventCollectionRequestrespectively. Existing method signatures are unchanged for all non-removed kwargs.- Exception type note: unknown kwargs on the resource METHOD raise Python's built-in
TypeError(e.g.,orders.create(foo='bar')→TypeError: ... unexpected keyword argument 'foo'). Unknown kwargs when constructing a REQUEST MODEL directly (e.g.,CreateOrderRequest(foo='bar')) raisepydantic.ValidationError. The latter is NOT wrapped in the SDK'sKalshiValidationError(which is reserved for HTTP 400 responses). If you catchKalshiErrorbroadly in your wrapper code and also construct request models directly, addpydantic.ValidationErrorto your except clause.
- Exception type note: unknown kwargs on the resource METHOD raise Python's built-in
- 7 new kwargs on
orders.create():time_in_force("fill_or_kill"/"good_till_canceled"/"immediate_or_cancel"),post_only,reduce_only,self_trade_prevention_type,order_group_id,cancel_order_on_pause,subaccount. All match speccomponents.schemas.CreateOrderRequestproperties that were previously unreachable from the SDK.subaccountwas already supported oncancel/amend/decrease/list/fills— this closes the inconsistency. buy_max_costnow wired throughorders.create(). The field existed on the model since v0.1 but was never exposed on the method. Now accepted as an integer cents value.- Per-order
subaccountrouting onorders.batch_cancel(). The preferred spec field (orders: list[BatchCancelOrdersRequestOrder]) carries optionalsubaccountper entry; the SDK now exposes this capability. TestRequestParamDriftandTestRequestBodyDriftintests/test_contracts.py. Parametrized overMETHOD_ENDPOINT_MAPentries (47 GET/DELETE + 7 POST/PUT/DELETE-with-body). Hard-fail on spec/SDK divergence not covered by theEXCLUSIONSallowlist. Complements the existing response-sideTestSpecDrift(which warns rather than fails — intentional asymmetry: request drift is a user-facing capability gap).test_exclusion_map_is_currentlint test — flagsEXCLUSIONSentries whose claimed deviation no longer exists.- 6 new Pydantic request models exported from
kalshi.modelsandkalshi:AmendOrderRequest,DecreaseOrderRequest,BatchCreateOrdersRequest,BatchCancelOrdersRequest,BatchCancelOrdersRequestOrder,CreateMarketInMultivariateEventCollectionRequest,LookupTickersForMarketInMultivariateEventCollectionRequest. Users can construct these directly for advanced use cases (e.g., passinglist[BatchCancelOrdersRequestOrder]tobatch_cancel()with per-order subaccount).
CreateOrderRequest— 7 field additions, 1 field removal (type), 1 type change (buy_max_cost→int). Added afield_validatorthat rejectsDecimalandfloatinputs onbuy_max_costto prevent silent migration hazards.MethodEndpointEntry(test infrastructure) gains optionalrequest_body_schema: str | None = None.EXCLUSIONSallowlist intests/_contract_support.py— bootstrapped with 16 entries (5 model-side + 11cursorpaginator-handled). Task 3 appended 2 more (AmendOrderRequestcent-form). Task 7 scope expansion appended 1 more (batch_cancel'sordersbody-param). Task 13 appended 6 more (countwire normalization on CreateOrderRequest + AmendOrderRequest,reduce_by_fp/reduce_to_fpdeferred on DecreaseOrderRequest, deprecatedidson BatchCancelOrdersRequest). Total: 25.
Major release. Resource method query/path parameter surface aligned to OpenAPI spec v3.13.0. 5 BREAKING changes (2 phantom kwargs removed, 3 renamed) and 32 new query params added across 6 resources.
MarketsResource.list/list_all:tickers(list[str] | str, comma-joined perTickersQueryspec),mve_filter,min_created_ts,max_created_ts,min_updated_ts,min_close_ts,max_close_ts,min_settled_ts,max_settled_tsMarketsResource.orderbook:depthMarketsResource.candlesticks:include_latest_before_start(bool, "true or omit" rule)
HistoricalResource.markets/markets_all:mve_filterHistoricalResource.fills/fills_all:max_tsHistoricalResource.orders/orders_all:max_tsHistoricalResource.trades/trades_all:min_ts,max_ts
OrdersResource.cancel:subaccountOrdersResource.list/list_all:event_ticker,min_ts,max_ts,subaccountOrdersResource.fills/fills_all:min_ts,max_ts,subaccount
PortfolioResource.balance:subaccountPortfolioResource.positions:count_filter(filters by which numeric fields are non-zero — NOT asettlement_statusreplacement),ticker,subaccountPortfolioResource.settlements/settlements_all:event_ticker,min_ts,max_ts,subaccount
OrdersResource.list/list_all(sync + async) standardized to use_params()helper. Behavior change: empty-string values forticker="",status="", ANDcursor=""now reach the wire (previously dropped silently by truthiness check). If your code constructs the cursor via expressions likepage.cursor or "", you may now get a 400 from Kalshi where the previous version silently swallowed it; passcursor=None(or omit) to drop the param._join_tickers()helper lifted frommarkets.pyto_base.pyfor cross-resource reuse. Now accepts list, tuple, or pre-joined string. Empty list/tuple/string returnsNoneso_params()drops the key entirely (sending?tickers=has undefined server semantics).OrdersResource.queue_positions(sync + async) refactored to use the shared helper instead of duplicating the join logic inline._delete()(sync + async) extended to accept optionalparams=kwarg (needed forOrdersResource.cancel(subaccount=...)). Backward compatible: defaults toNone.
MarketsResource.list/list_all:market_typeremoved. Migration: drop the kwarg from caller code.PortfolioResource.positions:settlement_statusremoved. NO direct replacement. The kwarg was not a valid/portfolio/positionsquery param per spec lines 1055-1090 (only/fcm/positionsaccepts it). The spec paramcount_filteris unrelated semantically (filters by non-zero numeric fields, not by settlement state — verified spec lines 2206-2221). Migration: filter by settlement state client-side, OR use/fcm/positionsif you are an FCM member.
HistoricalResource.markets/markets_all:ticker→tickers. Spec usesTickersQuery($ref'd, type:string, comma-separated). Migration:historical.markets(ticker="X")→historical.markets(tickers="X")ORhistorical.markets(tickers=["X", "Y"]).
SeriesResource.event_candlesticks(series_ticker, event_ticker, ...)→event_candlesticks(series_ticker, ticker, ...). Spec path:/series/{series_ticker}/events/{ticker}/candlesticks(verifiedspecs/openapi.yaml:1486). Migration: positional callers (X, Y, ...) work unchanged. Kwarg callers (event_ticker=...) must switch toticker=....SeriesResource.forecast_percentile_history(series_ticker, event_ticker, ...)→forecast_percentile_history(series_ticker, ticker, ...). Same migration as above.
- 60+ new unit tests across
tests/test_orders.py,tests/test_async_orders.py,tests/test_markets.py,tests/test_async_markets.py,tests/test_historical.py,tests/test_portfolio.py,tests/test_series.py. - 5 BREAKING regression tests assert
TypeErroron the removed/renamed kwargs. - 4 dedicated
tickerscomma-join serialization tests (markets + historical, sync + async). - 2 dedicated
percentilesexplode:trueserialization tests verify wire format?percentiles=25&percentiles=50(NOT comma-joined per spec line 1832). - 4 regression tests for the
_params()standardization onorders.list(empty-stringtickerandstatusfor both sync and async). - 2 dedicated
markets.candlesticks(include_latest_before_start=True)"true or omit" bool serialization tests.
- Internal test infrastructure for upcoming v0.7.0 resource/spec alignment work:
tests/_contract_support.pyintroducesMethodEndpointEntry,METHOD_ENDPOINT_MAP(53 sync methods across 8 resources),_resolve_refwith recursion cap, and_resolve_path_paramshelper that walks path-level and operation-level OpenAPI parameters with$refand JSON Pointer escape (~0/~1) resolution. docs/AUDIT-resource-params.mdcataloging 37 actionable rows of SDK↔spec drift: 2 phantom kwargs flagged for removal (market_typeonmarkets.list,settlement_statusonportfolio.positions), 3 breaking renames (historical.markets.ticker→tickers, series pathevent_ticker→tickeron 2 methods), and 32 missing params to add (subaccount, timestamp filters,depth,mve_filter,count_filter, etc.).- 25 unit tests covering the new contract helpers, including reverse-completeness (every mapped path must resolve in
specs/openapi.yaml) and tautological-pass guards.
- No user-facing behavior changes. This is an infrastructure release preparing for v0.7.0.
amend()method on OrdersResource and AsyncOrdersResource for amending order price and/or quantity. ReturnsAmendOrderResponsewith both pre and post-amendment order state.decrease()method on OrdersResource and AsyncOrdersResource for reducing order quantity by amount (reduce_by) or to amount (reduce_to)queue_positions()method for bulk queue position lookup across all resting orders, with optionalmarket_tickersandevent_tickerfiltersqueue_position()method for single-order queue position lookup, returnsDecimalAmendOrderResponsemodel containingold_orderandorderfieldsOrderQueuePositionmodel withorder_id,market_ticker, andqueue_position(FixedPointCount)- Contract map entries for
AmendOrderResponseandOrderQueuePositionfor spec drift detection - 29 new tests: sync/async happy paths, error paths, serialization verification, and auth guards for all 4 new methods
- Integration coverage harness registration for amend, decrease, queue_position, queue_positions
- WS spec drift pipeline: contract tests verify all 15 WebSocket payload models against the AsyncAPI spec
AliasChoiceson all WS payload fields where AsyncAPI spec names differ from SDK names (26 fields across 8 model files)WS_CONTRACT_MAPwith 15 entries in_contract_map.py, reusing the existingContractEntrydataclassTestWsSpecDriftclass with 5 tests: additive drift, required drift, schema coverage, contract map completeness, and envelope type drift- Envelope type drift test that detects dispatch key mismatches between spec and SDK (found 3:
user_ordervsuser_orders,market_positionvsmarket_positions,multivariate_lookupvsmultivariate) extra = "allow"onOrderbookSnapshotPayloadandOrderbookDeltaPayload(the only two WS models missing it)- P3 TODO for investigating WS dispatch type mismatch (spec vs SDK)
- WS payload models now accept both spec-named fields (e.g.,
yes_bid_dollars) and SDK-named fields (e.g.,yes_bid) via PydanticAliasChoices
- Unauthenticated client access for public endpoints:
KalshiClient(demo=True)works without RSA credentials KalshiAuth.try_from_env()classmethod that returnsNoneinstead of raising when credentials are missingAuthRequiredErrorexception (extendsKalshiAuthError) raised when unauthenticated clients call private endpointsis_authenticatedproperty onSyncTransportandAsyncTransport- Auth guards on all private resource methods (orders, portfolio, historical fills/orders) and
.wsproperty - Empty-string
key_idvalidation in client constructors (raisesValueErrorinstead of silently degrading) - Warning log when
KALSHI_KEY_IDis set but no private key is configured
- Breaking:
KalshiClient()andAsyncKalshiClient()no longer raiseValueErrorwithout credentials (they create unauthenticated clients) - Breaking:
KalshiClient.from_env()andAsyncKalshiClient.from_env()return unauthenticated clients when no env vars are set (previously raisedKalshiAuthError)
If you relied on from_env() raising as a startup check, use KalshiAuth.from_env() directly:
# Before (raises at startup if no credentials):
client = KalshiClient.from_env()
# After (raises only when a private endpoint is called):
client = KalshiClient.from_env()
client.orders.list() # AuthRequiredError here
# Migration — if you need fast-fail behavior:
from kalshi import KalshiAuth
auth = KalshiAuth.from_env() # still raises if missing
client = KalshiClient(auth=auth)- Full WebSocket client supporting all 11 Kalshi channels: orderbook_delta, ticker, trade, fill, market_positions, user_orders, order_group_updates, market_lifecycle_v2, multivariate, multivariate_market_lifecycle, communications
KalshiWebSocketclient with async context manager:async with client.ws.connect() as session- Per-channel typed subscribe methods (
subscribe_ticker(),subscribe_fill(), etc.) for mypy strict compatibility - Generic
subscribe(channel, **params)for dynamic use cases - Callback API via
@session.on("channel")decorator, mutually exclusive per channel with async iterators ws.orderbook("TICKER")convenience yields fullOrderbookstate on every delta updateConnectionManagerwith 6-state machine (DISCONNECTED, CONNECTING, CONNECTED, STREAMING, RECONNECTING, CLOSED)- Auto-reconnect with exponential backoff + jitter, configurable via
ws_max_retries(default 10) - RSA-PSS auth during WebSocket handshake (reuses existing
KalshiAuth) SubscriptionManagerwith durable client-side subscription IDs that survive reconnection (server sids are remapped transparently)update_subscription()for adding/removing tickers from live subscriptions without re-subscribingSequenceTrackerfor gap detection on channels that supportseq(orderbook_delta, order_group_updates)- Sequence gap triggers automatic resync (re-subscribe with fresh snapshot)
OrderbookManagermaintains local in-memory orderbook from WS snapshots + deltasMessageQueuewith configurable overflow strategies:DROP_OLDEST(default for ticker/trade) andERROR(default for orderbook_delta)FixedPointCountPydantic type for_fpsuffix fields (contract counts, volumes)- 5 new WebSocket exception classes:
KalshiWebSocketError,KalshiConnectionError,KalshiSequenceGapError,KalshiBackpressureError,KalshiSubscriptionError ws_base_urlandws_max_retriesfields onKalshiConfig- Typed Pydantic models for all 11 channel message payloads (24 model classes total)
- Fake WebSocket test server for integration testing (simulates subscribe, broadcast, disconnect, auth rejection)
- 306 new tests (149 existing + 306 new = 455 total)
- BREAKING:
Order.count,initial_count,remaining_count,fill_countchanged frominttoFixedPointCount(Decimal). Accepts bothintand_fpstring formats. - BREAKING:
CreateOrderRequest.countchanged fromint = 1toFixedPointCount = Decimal("1") websockets>=14,<17added as a dependency
- OpenAPI spec drift detection pipeline: contract tests compare hand-written SDK models against the Kalshi OpenAPI spec
kalshi/_contract_map.py: explicit manifest mapping 15 SDK models to OpenAPI schema componentstests/test_contracts.py: 32 contract tests (additive drift, required drift, schema coverage, map completeness)scripts/sync_spec.py: downloads latest OpenAPI + AsyncAPI specs with retry/backoffscripts/generate.py: local dev tool to generate reference Pydantic models via datamodel-code-generator.github/workflows/spec-drift.yml: CI workflow (PRs use pinned spec, nightly downloads fresh)- Pinned
specs/openapi.yamlsnapshot for deterministic PR builds - New dev dependencies:
datamodel-code-generator,pyyaml - P1 TODO: endpoint-level contract tests for resource method validation
- Exchange resource:
client.exchange.status(),schedule(),announcements()for checking exchange operational state - Portfolio resource:
client.portfolio.balance(),positions(),settlements(),settlements_all()for account and position management - Events resource:
client.events.list(),list_all(),get(),metadata()for browsing event containers - Historical resource:
client.historical.cutoff(),markets(),market(),candlesticks(),fills(),orders(),trades()plus_all()auto-paginators for backtesting data fills_all()auto-paginator on OrdersResource and AsyncOrdersResource_params()helper for DRY query parameter building across all resources- New models:
Event,EventMetadata,ExchangeStatus,Schedule,Announcement,Balance,MarketPosition,EventPosition,PositionsResponse,Settlement,HistoricalCutoff,Trade,BidAskDistribution,PriceDistribution PositionsResponse.has_nextproperty for pagination consistency- New Market fields:
market_type,yes_sub_title,no_sub_title,settlement_value,yes_bid_size,yes_ask_size,no_bid_size,no_ask_size,created_time,updated_time,latest_expiration_time,fractional_trading_enabled,settlement_timer_seconds - New Fill fields:
fill_id,market_ticker,fee_cost(with_dollarsalias) - 72 new tests (149 to 221 total) covering all new resources, async parity, and model validation
- BREAKING:
MarketsResource.list()andget()now hit/marketsendpoint (was/events). Response keys changed fromevents/eventtomarkets/market - BREAKING:
Market.volume,Market.volume_24h,Market.open_interestchanged frominttoDollarDecimal(API returns FixedPointCount_fpstrings) - BREAKING:
Fill.countchanged frominttoDollarDecimal(API returnscount_fpas FixedPointCount) - BREAKING:
Candlestickmodel redesigned with nestedBidAskDistribution/PriceDistributionobjects matching the real API schema (was flat OHLC fields) CreateOrderRequestnow usesextra="forbid"to reject unknown fields (catches typos)Settlement.fee_costandFill.fee_costnow acceptfee_cost_dollarsalias
- Full async test coverage: 45 new tests mirroring every sync test for AsyncTransport, AsyncKalshiClient, AsyncMarketsResource, and AsyncOrdersResource
- Tests cover async retry logic (502, 429), POST/DELETE not retried, constructor branches,
from_env(), context manager, auto-pagination, orderbook, candlesticks, batch operations, and fills
- Price fields now correctly map to Kalshi API
_dollarssuffix names (e.g.,yes_bid_dollars) via PydanticAliasChoices, fixing silentNonevalues on all price fields when parsing real API responses - CreateOrderRequest now sends
yes_price_dollars/no_price_dollarskeys instead ofyes_price/no_price(the API expects FixedPointDollars strings, not integer cents) - Orderbook parsing now reads from
orderbook_fp.yes_dollars/no_dollars(the current API response format) - Candlestick OHLC fields now accept
open_dollars/close_dollars/etc. from the API - OrderbookLevel.quantity changed from
inttoDollarDecimalto support fractional contracts (FixedPointCount strings)
- 24 new tests: price format regression tests, auth percent-encoding behavior tests, KalshiClient constructor and
from_env()coverage (80 → 104 tests) - New Market fields:
previous_yes_bid,previous_yes_ask,previous_price,notional_value - Auth percent-encoding limitation documented in code and tests (issue #2)
DollarDecimaldocstring updated to reflect FixedPointDollars format (strings with up to 6 decimal places)- CLAUDE.md updated with price format documentation and alias conventions
KalshiClientandAsyncKalshiClientwith sync and async support for the Kalshi prediction markets API- RSA-PSS authentication (
KalshiAuth) with key file, PEM string, and environment variable loading - Markets resource: list, list_all (auto-pagination), get, orderbook, candlesticks
- Orders resource: create, get, cancel, list, batch_create, batch_cancel, fills
Page[T]generic pagination model with cursor support and lazy auto-pagination iteratorsDollarDecimalcustom Pydantic v2 type for safe bidirectional price conversion (no float intermediaries)- Exception hierarchy:
KalshiAuthError,KalshiNotFoundError,KalshiValidationError,KalshiRateLimitError,KalshiServerError - Automatic retry with exponential backoff + jitter for GET requests on 429/502/503/504
- Retry-After header support with configurable max delay cap
KalshiConfigwith production and demo environment helpers- stdlib logging via
logging.getLogger("kalshi")for request/response debugging - PEP 561
py.typedmarker for downstream type checking - 80 tests covering auth, transport, retry, error mapping, pagination, markets, orders, and models
- GitHub Actions CI: lint (ruff) + type check (mypy strict) + test on Python 3.12 and 3.13
- Claude Code project configuration with scoped permissions