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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

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

## 22.1.0 — 2026-10-17

Reconciles nightly spec drift (#547). Additive and optional only — no
breaking changes.

### Added

- Perps **`transfers.transfer_instance(..., source_subaccount=,
destination_subaccount=)`** (sync + async) and the matching optional
fields on `IntraExchangeInstanceTransferRequest` (`int | None`, `>= 0`).
Upstream only supports these for event contract to event contract
transfers. Default `None` omits the keys; an explicit `0` is sent.

- Perps **`transfers.transfer_subaccount(..., exchange_index=)`** (sync +
async) and the matching optional field on the perps
`ApplySubaccountTransferRequest` (`int | None`, `>= 0`). Upstream notes
that perps subaccount transfers ignore this field. Default `None` omits
the key.

### Changed

- Re-vendored `specs/` (OpenAPI 3.34.0, perps OpenAPI, AsyncAPI, perps
AsyncAPI). Besides the fields above the upstream changes are
description, example, vendor-extension, and response-side additive
optional fields only.

## 22.0.0 — 2026-10-16

Reconciles three rounds of nightly spec drift (#540, #542, #544).
Expand Down
2 changes: 1 addition & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,4 @@
"Withdrawal",
]

__version__ = "22.0.0"
__version__ = "22.1.0"
8 changes: 8 additions & 0 deletions kalshi/perps/models/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class IntraExchangeInstanceTransferRequest(BaseModel):
Shard fields default to ``0`` (only ``0`` is currently supported); because
the body is serialized with ``exclude_none=True`` (not ``exclude_defaults``),
shard ``0`` values are emitted on the wire, which is fine.
``source_subaccount`` / ``destination_subaccount`` default to ``None`` so
``exclude_none`` drops them (an explicit ``0`` is still sent). Only
supported for event contract to event contract transfers.
``client_transfer_id`` is optional duplicate-prevention; omit (``None``)
so ``exclude_none`` drops the key. Only supported for primary accounts.
"""
Expand All @@ -52,6 +55,8 @@ class IntraExchangeInstanceTransferRequest(BaseModel):
amount: StrictInt = Field(gt=0)
source_exchange_shard: StrictInt = Field(default=0, ge=0)
destination_exchange_shard: StrictInt = Field(default=0, ge=0)
source_subaccount: StrictInt | None = Field(default=None, ge=0)
destination_subaccount: StrictInt | None = Field(default=None, ge=0)
# Client UUID for duplicate prevention. Omit (None) so exclude_none
# drops the key; each request then creates a new transfer.
client_transfer_id: UUID | None = None
Expand All @@ -69,12 +74,15 @@ class ApplySubaccountTransferRequest(BaseModel):
primary account and a positive integer for numbered subaccounts. Spec prose
says ``1-63`` but defines no JSON-schema maximum, so only the lower bound is
validated (``ge=0``) — server-assigned numbers always round-trip.
``exchange_index`` is optional (spec ``ExchangeIndex``, ``ge=0``). Perps
subaccount transfers ignore this field; omit so ``exclude_none`` drops it.
"""

client_transfer_id: UUID
from_subaccount: StrictInt = Field(ge=0)
to_subaccount: StrictInt = Field(ge=0)
amount_cents: StrictInt = Field(gt=0)
exchange_index: StrictInt | None = Field(default=None, ge=0)

model_config = {"extra": "forbid"}

Expand Down
53 changes: 48 additions & 5 deletions kalshi/perps/resources/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ def _build_instance_transfer_body(
amount: int | None,
source_exchange_shard: int | None,
destination_exchange_shard: int | None,
source_subaccount: int | None = None,
destination_subaccount: int | None = None,
client_transfer_id: UUID | str | None = None,
) -> dict[str, Any]:
# Shards are part of the kwargs path — included in the exclusivity check so
# `request=...` together with an explicit shard raises instead of silently
# dropping the shard. They default to 0 only when building from kwargs.
# Shards and optional subaccount fields are part of the kwargs path —
# included in the exclusivity check so `request=...` together with an
# explicit value raises instead of silently dropping it. Shards default
# to 0 only when building from kwargs; subaccount fields stay None
# (exclude_none drops them) unless the caller passed an explicit int.
_check_request_exclusive(
request,
source=source,
Expand All @@ -59,6 +63,8 @@ def _build_instance_transfer_body(
source_exchange_shard=source_exchange_shard,
destination_exchange_shard=destination_exchange_shard,
client_transfer_id=client_transfer_id,
source_subaccount=source_subaccount,
destination_subaccount=destination_subaccount,
)
if request is None:
if source is None or destination is None or amount is None:
Expand All @@ -81,6 +87,8 @@ def _build_instance_transfer_body(
amount=amount,
source_exchange_shard=source_exchange_shard or 0,
destination_exchange_shard=destination_exchange_shard or 0,
source_subaccount=source_subaccount,
destination_subaccount=destination_subaccount,
client_transfer_id=uid,
)
return request.model_dump(exclude_none=True, by_alias=True, mode="json")
Expand All @@ -93,13 +101,15 @@ def _build_subaccount_transfer_body(
from_subaccount: int | None,
to_subaccount: int | None,
amount_cents: int | None,
exchange_index: int | None = None,
) -> dict[str, Any]:
_check_request_exclusive(
request,
client_transfer_id=client_transfer_id,
from_subaccount=from_subaccount,
to_subaccount=to_subaccount,
amount_cents=amount_cents,
exchange_index=exchange_index,
)
if request is None:
if (
Expand All @@ -123,6 +133,7 @@ def _build_subaccount_transfer_body(
from_subaccount=from_subaccount,
to_subaccount=to_subaccount,
amount_cents=amount_cents,
exchange_index=exchange_index,
)
return request.model_dump(exclude_none=True, by_alias=True, mode="json")

Expand All @@ -146,6 +157,8 @@ def transfer_instance(
amount: int,
source_exchange_shard: int = 0,
destination_exchange_shard: int = 0,
source_subaccount: int | None = None,
destination_subaccount: int | None = None,
client_transfer_id: UUID | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> IntraExchangeInstanceTransferResponse: ...
Expand All @@ -158,13 +171,18 @@ def transfer_instance(
amount: int | None = None,
source_exchange_shard: int | None = None,
destination_exchange_shard: int | None = None,
source_subaccount: int | None = None,
destination_subaccount: int | None = None,
client_transfer_id: UUID | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> IntraExchangeInstanceTransferResponse:
"""Move funds between exchange instances.

Spec marks this endpoint "currently not available" — it is implemented
forward-compatibly and may return an error until the server enables it.
``source_subaccount`` / ``destination_subaccount`` are optional; only
supported for event contract to event contract transfers. Omit so the
keys are not sent; an explicit ``0`` is emitted.
``client_transfer_id`` is optional duplicate-prevention (UUID or str).
Omit so the key is not sent; each request then creates a new transfer.
Only supported for primary accounts.
Expand All @@ -178,6 +196,8 @@ def transfer_instance(
source_exchange_shard=source_exchange_shard,
destination_exchange_shard=destination_exchange_shard,
client_transfer_id=client_transfer_id,
source_subaccount=source_subaccount,
destination_subaccount=destination_subaccount,
)
data = self._post(
"/portfolio/intra_exchange_instance_transfer",
Expand Down Expand Up @@ -213,6 +233,7 @@ def transfer_subaccount(
from_subaccount: int,
to_subaccount: int,
amount_cents: int,
exchange_index: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> None: ...
def transfer_subaccount(
Expand All @@ -223,16 +244,22 @@ def transfer_subaccount(
from_subaccount: int | None = None,
to_subaccount: int | None = None,
amount_cents: int | None = None,
exchange_index: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> None:
"""Move funds between margin subaccounts (returns ``None``)."""
"""Move funds between margin subaccounts (returns ``None``).

``exchange_index`` is optional; perps subaccount transfers ignore this
field. Omit so the key is not sent; an explicit ``0`` is emitted.
"""
self._require_auth()
body = _build_subaccount_transfer_body(
request,
client_transfer_id=client_transfer_id,
from_subaccount=from_subaccount,
to_subaccount=to_subaccount,
amount_cents=amount_cents,
exchange_index=exchange_index,
)
self._post(
"/portfolio/margin/subaccounts/transfer",
Expand Down Expand Up @@ -260,6 +287,8 @@ async def transfer_instance(
amount: int,
source_exchange_shard: int = 0,
destination_exchange_shard: int = 0,
source_subaccount: int | None = None,
destination_subaccount: int | None = None,
client_transfer_id: UUID | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> IntraExchangeInstanceTransferResponse: ...
Expand All @@ -272,13 +301,18 @@ async def transfer_instance(
amount: int | None = None,
source_exchange_shard: int | None = None,
destination_exchange_shard: int | None = None,
source_subaccount: int | None = None,
destination_subaccount: int | None = None,
client_transfer_id: UUID | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> IntraExchangeInstanceTransferResponse:
"""Move funds between exchange instances.

Spec marks this endpoint "currently not available" — it is implemented
forward-compatibly and may return an error until the server enables it.
``source_subaccount`` / ``destination_subaccount`` are optional; only
supported for event contract to event contract transfers. Omit so the
keys are not sent; an explicit ``0`` is emitted.
``client_transfer_id`` is optional duplicate-prevention (UUID or str).
Omit so the key is not sent; each request then creates a new transfer.
Only supported for primary accounts.
Expand All @@ -292,6 +326,8 @@ async def transfer_instance(
source_exchange_shard=source_exchange_shard,
destination_exchange_shard=destination_exchange_shard,
client_transfer_id=client_transfer_id,
source_subaccount=source_subaccount,
destination_subaccount=destination_subaccount,
)
data = await self._post(
"/portfolio/intra_exchange_instance_transfer",
Expand Down Expand Up @@ -327,6 +363,7 @@ async def transfer_subaccount(
from_subaccount: int,
to_subaccount: int,
amount_cents: int,
exchange_index: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> None: ...
async def transfer_subaccount(
Expand All @@ -337,16 +374,22 @@ async def transfer_subaccount(
from_subaccount: int | None = None,
to_subaccount: int | None = None,
amount_cents: int | None = None,
exchange_index: int | None = None,
extra_headers: dict[str, str] | None = None,
) -> None:
"""Move funds between margin subaccounts (returns ``None``)."""
"""Move funds between margin subaccounts (returns ``None``).

``exchange_index`` is optional; perps subaccount transfers ignore this
field. Omit so the key is not sent; an explicit ``0`` is emitted.
"""
self._require_auth()
body = _build_subaccount_transfer_body(
request,
client_transfer_id=client_transfer_id,
from_subaccount=from_subaccount,
to_subaccount=to_subaccount,
amount_cents=amount_cents,
exchange_index=exchange_index,
)
await self._post(
"/portfolio/margin/subaccounts/transfer",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "kalshi-sdk"
version = "22.0.0"
version = "22.1.0"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
Loading
Loading