Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,51 @@

All notable changes to kalshi-sdk will be documented in this file.

## 4.0.0 — 2026-06-09

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 fields** — `MarginMarket` 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).

## 3.3.0 — 2026-06-06

Adds a complete **FIX protocol** subsystem (FIXT.1.1 / FIX50SP2) for both the
Expand Down
6 changes: 4 additions & 2 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ with KalshiClient.from_env(config=config) as client:

## Klear (SCM) environment variables

`KlearClient` / `AsyncKlearClient` use cookie-session auth (no RSA keys), so they
read only environment selectors:
`KlearClient` / `AsyncKlearClient` use Bearer-token auth (no RSA keys).
`KlearClient.from_env()` reads the credentials plus the routing selectors:

| Variable | Default | Effect |
|---|---|---|
| `KALSHI_KLEAR_ADMIN_USER_ID` | unset | Klear admin user id (the Bearer `admin_user_id`). Required by `from_env()`. |
| `KALSHI_KLEAR_ACCESS_TOKEN` | unset | Klear access token (the Bearer `access_token`). Required by `from_env()`. Secret. |
| `KALSHI_KLEAR_DEMO` | `false` | `true` (case-insensitive) selects the demo Klear endpoint. |
| `KALSHI_KLEAR_API_BASE_URL` | unset | Overrides the Klear `base_url`. |
| `KALSHI_KLEAR_ALLOW_UNKNOWN_HOST` | unset | Set to `1` to allow non-Kalshi Klear hosts. |
Expand Down
17 changes: 9 additions & 8 deletions docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ recommends **separate API keys** for it.
|---|---|---|---|
| Perps REST | `PerpsClient` / `AsyncPerpsClient` | `external-api.kalshi.com` / `external-api.demo.kalshi.co` (`/trade-api/v2`) | RSA-PSS (same signer as `KalshiClient`) |
| Perps WebSocket | `PerpsWebSocket` | `external-api-margin-ws.kalshi.com` / `external-api-margin-ws.demo.kalshi.co` (`/trade-api/ws/v2/margin`) | RSA-PSS apiKey |
| SCM "Klear" | `KlearClient` / `AsyncKlearClient` | `api.klear.kalshi.com` / `demo-api.kalshi.co` (`/klear-api/v1`) | cookie-session + MFA |
| SCM "Klear" | `KlearClient` / `AsyncKlearClient` | `api.klear.kalshi.com` / `demo-api.kalshi.co` (`/klear-api/v1`) | Bearer `admin_user_id:access_token` |

The RSA-PSS signer and the HTTP transport are **reused unchanged** from the
prediction API — only the host, config, and credentials differ.
Expand Down Expand Up @@ -135,21 +135,22 @@ The equities-only channels (`market_positions`, `multivariate*`, `communications
## Self-Clearing-Member "Klear" API

The Klear API (settlement balances, obligations, margin reports, withdrawals) is a
third surface with a **completely different auth model** — email + password (+ MFA)
via `POST /log_in`, which sets a session cookie replayed on every subsequent
request. It is exposed via `KlearClient` / `AsyncKlearClient`:
third surface with a **different auth model** — a pre-generated Bearer token passed
as `Authorization: Bearer <admin_user_id>:<access_token>` on every request. Generate
the token and find your admin user id at <https://klearing.kalshi.com> (the
"Security" page). It is exposed via `KlearClient` / `AsyncKlearClient`:

```python
from kalshi import KlearClient

with KlearClient(demo=True) as klear:
resp = klear.login(email="...", password="...")
if resp.required_mfa_method: # MFA challenge
klear.login(email="...", password="...", code="123456")
with KlearClient(admin_user_id="...", access_token="...", demo=True) as klear:
reports = klear.margin.margin_reports(start_date="2026-01-01", end_date="2026-02-01")
bal = klear.margin.settlement_balance()
```

Credentials can also come from the environment via `KlearClient.from_env()` (reads
`KALSHI_KLEAR_ADMIN_USER_ID` / `KALSHI_KLEAR_ACCESS_TOKEN`).

Money fields on the Klear margin schemas are integer **centicents** (`1 USD =
10,000 centicents`); only the withdrawal `amount` is a fixed-point dollar string.
`klear.margin.withdraw_settlement_balance(amount="500.00")` validates the amount as
Expand Down
8 changes: 3 additions & 5 deletions kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
AmendOrderV2Response,
Announcement,
ApiKey,
ApiUsageLevelGrant,
ApplySubaccountTransferRequest,
AssociatedEvent,
Balance,
Expand Down Expand Up @@ -178,8 +179,6 @@
KlearAuth,
KlearClient,
KlearConfig,
LogInRequest,
LogInResponse,
)
from kalshi.resources.communications import (
AsyncQuotesResource,
Expand All @@ -201,6 +200,7 @@
"AmendOrderV2Response",
"Announcement",
"ApiKey",
"ApiUsageLevelGrant",
"ApplySubaccountTransferRequest",
"AssociatedEvent",
"AsyncKalshiClient",
Expand Down Expand Up @@ -310,8 +310,6 @@
"KlearClient",
"KlearConfig",
"LiveData",
"LogInRequest",
"LogInResponse",
"LookupPoint",
"LookupTickersForMarketInMultivariateEventCollectionRequest",
"LookupTickersResponse",
Expand Down Expand Up @@ -379,4 +377,4 @@
"Withdrawal",
]

__version__ = "3.3.0"
__version__ = "4.0.0"
28 changes: 27 additions & 1 deletion kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,15 @@ class ContractEntry:
notes=(
"SPEC DRIFT: spec declares int 'read_limit'/'write_limit' but the "
"live server returns nested 'read'/'write' token-bucket objects. "
"SDK model follows the server."
"SDK model follows the server. 'grants' is the per-exchange-lane "
"usage-level grant list (NullableList: null -> [])."
),
),
ContractEntry(
sdk_model="kalshi.models.account.ApiUsageLevelGrant",
spec_schema="ApiUsageLevelGrant",
notes="exchange_instance/level/source required; expires_ts nullable (None = permanent).",
),
ContractEntry(
sdk_model="kalshi.models.structured_targets.StructuredTarget",
spec_schema="StructuredTarget",
Expand Down Expand Up @@ -502,6 +508,19 @@ class ContractEntry:
spec_schema="tickerPayload",
notes="Spec has price_dollars and time fields not in SDK (expected additive drift)",
),
ContractEntry(
sdk_model="kalshi.ws.models.cfbenchmarks.CFBenchmarksValuePayload",
spec_schema="cfbenchmarksValuePayload",
notes=(
"avg_60s_data required; last_60s_windowed_average_15min present only "
"near quarter-hour close."
),
),
ContractEntry(
sdk_model="kalshi.ws.models.cfbenchmarks.CFBenchmarksIndexListPayload",
spec_schema="cfbenchmarksIndexListPayload",
notes="indexlist response: msg.index_ids only.",
),
ContractEntry(
sdk_model="kalshi.ws.models.fill.FillPayload",
spec_schema="fillPayload",
Expand Down Expand Up @@ -735,6 +754,13 @@ class ContractEntry:
sdk_model="kalshi.perps.models.margin_account.GetMarginFeeTiersResponse",
spec_schema="GetMarginFeeTiersResponse",
),
ContractEntry(
# /account/limits/perps returns the same shape as the prediction API's
# /account/limits, so the perps resource reuses kalshi.models.account.AccountApiLimits.
sdk_model="kalshi.models.account.AccountApiLimits",
spec_schema="GetAccountApiLimitsResponse",
notes="Reused prediction-API model; perps spec schema is field-identical.",
),
# ── perps funding (#395) ──
ContractEntry(
sdk_model="kalshi.perps.models.funding.MarginFundingRate",
Expand Down
2 changes: 1 addition & 1 deletion kalshi/fix/messages/order_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class NewOrderSingle(FixMessage):
Tag.MAX_EXECUTION_COST, FixType.DECIMAL, default=None
)
# Kalshi dictionary v1.03 types AllocAccount (79) as INT — the subaccount
# number (0 primary, 1-32) — not the FIX-standard STRING.
# number (0 primary, 1-63) — not the FIX-standard STRING.
alloc_account: int | None = fixfield(Tag.ALLOC_ACCOUNT, FixType.INT, default=None)


Expand Down
2 changes: 1 addition & 1 deletion kalshi/fix/messages/order_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class OrderGroupRequest(FixMessage):
Tag.ORDER_GROUP_CONTRACTS_LIMIT, FixType.INT, default=None
)
# Kalshi types AllocAccount (79) as INT — the subaccount number (0 primary,
# 1-32). Scopes the action to the group owned by that subaccount.
# 1-63). Scopes the action to the group owned by that subaccount.
alloc_account: int | None = fixfield(Tag.ALLOC_ACCOUNT, FixType.INT, default=None)

@classmethod
Expand Down
2 changes: 2 additions & 0 deletions kalshi/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from kalshi.models.account import (
AccountApiLimits,
AccountEndpointCosts,
ApiUsageLevelGrant,
EndpointTokenCost,
RateLimit,
)
Expand Down Expand Up @@ -190,6 +191,7 @@
"AmendOrderV2Response",
"Announcement",
"ApiKey",
"ApiUsageLevelGrant",
"ApplySubaccountTransferRequest",
"AssociatedEvent",
"Balance",
Expand Down
24 changes: 24 additions & 0 deletions kalshi/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from pydantic import BaseModel

from kalshi.types import NullableList


class RateLimit(BaseModel):
"""Per-direction (read/write) token-bucket rate limit.
Expand Down Expand Up @@ -42,17 +44,39 @@ class AccountEndpointCosts(BaseModel):
model_config = {"extra": "allow"}


class ApiUsageLevelGrant(BaseModel):
"""One API usage-level grant for a single exchange lane.

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

exchange_instance: str
level: str
source: str
expires_ts: int | None = None

model_config = {"extra": "allow"}


class AccountApiLimits(BaseModel):
"""Rate limits associated with the authenticated user's API tier.

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

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

usage_tier: str
read: RateLimit
write: RateLimit
grants: NullableList[ApiUsageLevelGrant]

model_config = {"extra": "allow"}
2 changes: 1 addition & 1 deletion kalshi/models/subaccounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ApplySubaccountTransferRequest(BaseModel):
a Decimal. ``from_subaccount`` / ``to_subaccount`` use ``0`` for the
primary account and a positive integer for numbered subaccounts. The
server is the source of truth for the upper bound: spec describes
``1-32`` in prose but defines no JSON-schema maximum, and demo has
``1-63`` in prose but defines no JSON-schema maximum, and demo has
been observed allocating values above 32. The SDK validates only the
lower bound (``ge=0``) so server-assigned numbers always round-trip.
"""
Expand Down
4 changes: 0 additions & 4 deletions kalshi/perps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
KlearAuth,
KlearClient,
KlearConfig,
LogInRequest,
LogInResponse,
)
from kalshi.perps.klear.models.margin import (
GetActiveMarginObligationResponse,
Expand Down Expand Up @@ -264,8 +262,6 @@
"KlearConfig",
"LastUpdateReason",
"LastUpdateReasonLiteral",
"LogInRequest",
"LogInResponse",
"MaintenanceMarginDetail",
"MarginAccountResource",
"MarginEnabledResponse",
Expand Down
20 changes: 8 additions & 12 deletions kalshi/perps/klear/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""Kalshi Self-Clearing-Member (SCM) "Klear" API.

The Klear API (``klear-api/v1``) authenticates with **email + password (+ MFA)
via** ``POST /log_in``, which sets a ``session`` cookie that is replayed on every
subsequent request — a completely different auth model from the RSA-PSS signing
used by the prediction and perps trade-api surfaces. It is therefore exposed via
standalone :class:`KlearClient` / :class:`AsyncKlearClient` with their own
:class:`KlearConfig` and a lightweight :class:`KlearAuth` session holder.
The Klear API (``klear-api/v1``) authenticates with a pre-generated Bearer token
passed as ``Authorization: Bearer <admin_user_id>:<access_token>`` on every
request — a different auth model from the RSA-PSS signing used by the prediction
and perps trade-api surfaces. It is therefore exposed via standalone
:class:`KlearClient` / :class:`AsyncKlearClient` with their own
:class:`KlearConfig` and a :class:`KlearAuth` Bearer-credential holder.

Security: ``email`` / ``password`` / ``code`` and the session cookie are secrets
— they are never logged, never placed in exception messages, and redacted from
``repr()``.
Security: the ``access_token`` is a secret — it is never logged, never placed in
exception messages, and redacted from ``repr()``.
"""

from __future__ import annotations
Expand All @@ -22,7 +21,6 @@
PRODUCTION_KLEAR_URL,
KlearConfig,
)
from kalshi.perps.klear.models.auth import LogInRequest, LogInResponse
from kalshi.perps.klear.models.common import Error

__all__ = [
Expand All @@ -33,6 +31,4 @@
"KlearAuth",
"KlearClient",
"KlearConfig",
"LogInRequest",
"LogInResponse",
]
Loading