diff --git a/.gitignore b/.gitignore index 84b0580..e453acf 100644 --- a/.gitignore +++ b/.gitignore @@ -29,9 +29,8 @@ uv.lock # Generated models (build artifact, not committed) kalshi/_generated/models.py -# AsyncAPI spec (downloaded for future WebSocket support, not committed) -# Note: specs/openapi.yaml IS committed as a pinned snapshot for deterministic PR builds. -specs/asyncapi.yaml +# Note: specs/openapi.yaml AND specs/asyncapi.yaml are both committed as +# pinned snapshots for deterministic PR builds + correct drift detection. .gitnexus # MkDocs build output diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f18101..d8e85d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,81 @@ All notable changes to kalshi-sdk will be documented in this file. +## 2.1.0 — 2026-05-18 + +OpenAPI spec sync from v3.13.0 → v3.18.0. Adds the V2 event-market +orders family, deposits/withdrawals history, account endpoint-cost +introspection, and several optional query / body fields. Also fixes +a recurring false-alarm in the weekly spec-sync workflow. + +### Added + +- **V2 event-market orders** (`/portfolio/events/orders/*`). Legacy + `/portfolio/orders` will be deprecated no earlier than May 6, 2026. + - `orders.create_v2(request=CreateOrderV2Request(...))` + - `orders.cancel_v2(order_id, subaccount=..., exchange_index=...)` + - `orders.amend_v2(order_id, request=AmendOrderV2Request(...))` + - `orders.decrease_v2(order_id, request=DecreaseOrderV2Request(...))` + - `orders.batch_create_v2(request=BatchCreateOrdersV2Request(...))` + - `orders.batch_cancel_v2(request=BatchCancelOrdersV2Request(...))` +- `portfolio.deposits()` / `portfolio.deposits_all()` — deposit history. +- `portfolio.withdrawals()` / `portfolio.withdrawals_all()` — withdrawal history. +- `account.endpoint_costs()` — lists endpoints whose token cost differs from the default. +- Optional `exchange_index` on `CreateOrderRequest`, `AmendOrderRequest`, + `DecreaseOrderRequest`, `BatchCancelOrdersRequestOrder`, `CreateOrderGroupRequest`, + and as a query kwarg on `orders.cancel` / `order_groups.delete`. +- Optional `user_filter` on `communications.list_rfqs` / `list_all_rfqs`; + `user_filter` + `rfq_user_filter` on `communications.list_quotes` / `list_all_quotes`. +- Optional `incentive_description` on `incentive_programs.list` / `list_all`. +- Optional `post_only` on `CreateQuoteRequest`. +- `Balance.balance_dollars` (required, `DollarDecimal`) — the same value as + `balance` (cents) rendered as a fixed-point dollar string. Required in + the v3.18.0 spec for `GetBalanceResponse`. +- `Balance.balance_breakdown` (optional, `list[IndexedBalance]`) — splits + the balance across exchange shards when present. +- New `IndexedBalance` model (`exchange_index`, `balance`) exposed from + `kalshi` and `kalshi.models`. + +### Changed + +- ``Balance`` gained a required ``balance_dollars: DollarDecimal`` field + (added by spec v3.18.0). Callers who construct ``Balance`` from API + responses are unaffected — the server now guarantees the field. But + callers who build ``Balance(...)`` instances directly in their own + tests or mocks will hit ``ValidationError`` until they add it. This is + a soft breaking change at the model-construction surface; the field is + required because the spec marks it ``required``, not optional-in-practice. + + ```python + # before v2.1.0 + Balance(balance=50000, portfolio_value=75000, updated_ts=ts) + + # v2.1.0+ + Balance( + balance=50000, + balance_dollars=Decimal("500.00"), # new required field + portfolio_value=75000, + updated_ts=ts, + ) + ``` + +### Migration note + +- ``CreateOrderV2Request.client_order_id`` is **required**. V1's + ``CreateOrderRequest`` made it optional, so callers migrating from + ``orders.create()`` to ``orders.create_v2()`` must generate a unique + client-order-id per call (UUID4 is the common choice). The server uses + this field as the V2 idempotency key, so reusing a value will cause + the server to return the original order rather than placing a new one. + +### Fixed + +- `specs/asyncapi.yaml` is now committed as a pinned snapshot, matching the + long-standing intent on `specs/openapi.yaml`. The weekly spec-sync workflow + no longer reports a bogus "AsyncAPI 0 → 13 channels" delta every Monday + (the file was previously gitignored, so each cron diffed the fresh + download against a non-existent old file). + ## 2.0.0 — 2026-05-17 Audit-driven hardening release. 30 audit-findings landed across five diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 6d58fdc..5c5a968 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -22,9 +22,12 @@ RFQ, AcceptQuoteRequest, AccountApiLimits, + AccountEndpointCosts, ActionLiteral, AmendOrderRequest, AmendOrderResponse, + AmendOrderV2Request, + AmendOrderV2Response, Announcement, ApiKey, ApplySubaccountTransferRequest, @@ -32,8 +35,17 @@ Balance, BatchCancelOrdersRequest, BatchCancelOrdersRequestOrder, + BatchCancelOrdersV2Request, + BatchCancelOrdersV2RequestOrder, + BatchCancelOrdersV2Response, + BatchCancelOrdersV2ResponseEntry, BatchCreateOrdersRequest, + BatchCreateOrdersV2Request, + BatchCreateOrdersV2Response, + BatchCreateOrdersV2ResponseEntry, BidAskDistribution, + BookSideLiteral, + CancelOrderV2Response, Candlestick, CreateApiKeyRequest, CreateApiKeyResponse, @@ -42,6 +54,8 @@ CreateOrderGroupRequest, CreateOrderGroupResponse, CreateOrderRequest, + CreateOrderV2Request, + CreateOrderV2Response, CreateQuoteRequest, CreateQuoteResponse, CreateRFQRequest, @@ -49,6 +63,10 @@ CreateSubaccountResponse, DailySchedule, DecreaseOrderRequest, + DecreaseOrderV2Request, + DecreaseOrderV2Response, + Deposit, + EndpointTokenCost, Event, EventCandlesticks, EventMetadata, @@ -82,6 +100,7 @@ IncentiveProgram, IncentiveProgramStatusLiteral, IncentiveProgramTypeLiteral, + IndexedBalance, LiveData, LookupPoint, LookupTickersForMarketInMultivariateEventCollectionRequest, @@ -105,6 +124,8 @@ OrderQueuePosition, OrderStatusLiteral, Page, + PaymentStatusLiteral, + PaymentTypeLiteral, PercentilePoint, PlayByPlay, PlayByPlayPeriod, @@ -133,7 +154,9 @@ UpdateOrderGroupLimitRequest, UpdateSubaccountNettingRequest, UserDataTimestamp, + UserFilterLiteral, WeeklySchedule, + Withdrawal, ) from kalshi.types import NullableList @@ -141,9 +164,12 @@ "RFQ", "AcceptQuoteRequest", "AccountApiLimits", + "AccountEndpointCosts", "ActionLiteral", "AmendOrderRequest", "AmendOrderResponse", + "AmendOrderV2Request", + "AmendOrderV2Response", "Announcement", "ApiKey", "ApplySubaccountTransferRequest", @@ -153,8 +179,17 @@ "Balance", "BatchCancelOrdersRequest", "BatchCancelOrdersRequestOrder", + "BatchCancelOrdersV2Request", + "BatchCancelOrdersV2RequestOrder", + "BatchCancelOrdersV2Response", + "BatchCancelOrdersV2ResponseEntry", "BatchCreateOrdersRequest", + "BatchCreateOrdersV2Request", + "BatchCreateOrdersV2Response", + "BatchCreateOrdersV2ResponseEntry", "BidAskDistribution", + "BookSideLiteral", + "CancelOrderV2Response", "Candlestick", "CreateApiKeyRequest", "CreateApiKeyResponse", @@ -163,6 +198,8 @@ "CreateOrderGroupRequest", "CreateOrderGroupResponse", "CreateOrderRequest", + "CreateOrderV2Request", + "CreateOrderV2Response", "CreateQuoteRequest", "CreateQuoteResponse", "CreateRFQRequest", @@ -170,6 +207,10 @@ "CreateSubaccountResponse", "DailySchedule", "DecreaseOrderRequest", + "DecreaseOrderV2Request", + "DecreaseOrderV2Response", + "Deposit", + "EndpointTokenCost", "Event", "EventCandlesticks", "EventMetadata", @@ -203,6 +244,7 @@ "IncentiveProgram", "IncentiveProgramStatusLiteral", "IncentiveProgramTypeLiteral", + "IndexedBalance", "KalshiAuth", "KalshiAuthError", "KalshiBackpressureError", @@ -241,6 +283,8 @@ "Orderbook", "OrderbookLevel", "Page", + "PaymentStatusLiteral", + "PaymentTypeLiteral", "PercentilePoint", "PlayByPlay", "PlayByPlayPeriod", @@ -269,7 +313,9 @@ "UpdateOrderGroupLimitRequest", "UpdateSubaccountNettingRequest", "UserDataTimestamp", + "UserFilterLiteral", "WeeklySchedule", + "Withdrawal", ] -__version__ = "2.0.0" +__version__ = "2.1.0" diff --git a/kalshi/models/__init__.py b/kalshi/models/__init__.py index 1e627e4..956261d 100644 --- a/kalshi/models/__init__.py +++ b/kalshi/models/__init__.py @@ -1,6 +1,11 @@ """Kalshi SDK data models.""" -from kalshi.models.account import AccountApiLimits, RateLimit +from kalshi.models.account import ( + AccountApiLimits, + AccountEndpointCosts, + EndpointTokenCost, + RateLimit, +) from kalshi.models.api_keys import ( ApiKey, CreateApiKeyRequest, @@ -24,6 +29,7 @@ GetRFQsResponse, MveSelectedLeg, Quote, + UserFilterLiteral, ) from kalshi.models.events import ( Event, @@ -94,11 +100,26 @@ ActionLiteral, AmendOrderRequest, AmendOrderResponse, + AmendOrderV2Request, + AmendOrderV2Response, BatchCancelOrdersRequest, BatchCancelOrdersRequestOrder, + BatchCancelOrdersV2Request, + BatchCancelOrdersV2RequestOrder, + BatchCancelOrdersV2Response, + BatchCancelOrdersV2ResponseEntry, BatchCreateOrdersRequest, + BatchCreateOrdersV2Request, + BatchCreateOrdersV2Response, + BatchCreateOrdersV2ResponseEntry, + BookSideLiteral, + CancelOrderV2Response, CreateOrderRequest, + CreateOrderV2Request, + CreateOrderV2Response, DecreaseOrderRequest, + DecreaseOrderV2Request, + DecreaseOrderV2Response, Fill, Order, OrderQueuePosition, @@ -109,12 +130,17 @@ ) from kalshi.models.portfolio import ( Balance, + Deposit, EventPosition, + IndexedBalance, MarketPosition, + PaymentStatusLiteral, + PaymentTypeLiteral, PositionsResponse, Settlement, SettlementStatusLiteral, TotalRestingOrderValue, + Withdrawal, ) from kalshi.models.search import ( GetFiltersBySportsResponse, @@ -149,9 +175,12 @@ "RFQ", "AcceptQuoteRequest", "AccountApiLimits", + "AccountEndpointCosts", "ActionLiteral", "AmendOrderRequest", "AmendOrderResponse", + "AmendOrderV2Request", + "AmendOrderV2Response", "Announcement", "ApiKey", "ApplySubaccountTransferRequest", @@ -159,8 +188,17 @@ "Balance", "BatchCancelOrdersRequest", "BatchCancelOrdersRequestOrder", + "BatchCancelOrdersV2Request", + "BatchCancelOrdersV2RequestOrder", + "BatchCancelOrdersV2Response", + "BatchCancelOrdersV2ResponseEntry", "BatchCreateOrdersRequest", + "BatchCreateOrdersV2Request", + "BatchCreateOrdersV2Response", + "BatchCreateOrdersV2ResponseEntry", "BidAskDistribution", + "BookSideLiteral", + "CancelOrderV2Response", "Candlestick", "CreateApiKeyRequest", "CreateApiKeyResponse", @@ -169,6 +207,8 @@ "CreateOrderGroupRequest", "CreateOrderGroupResponse", "CreateOrderRequest", + "CreateOrderV2Request", + "CreateOrderV2Response", "CreateQuoteRequest", "CreateQuoteResponse", "CreateRFQRequest", @@ -176,6 +216,10 @@ "CreateSubaccountResponse", "DailySchedule", "DecreaseOrderRequest", + "DecreaseOrderV2Request", + "DecreaseOrderV2Response", + "Deposit", + "EndpointTokenCost", "Event", "EventCandlesticks", "EventMetadata", @@ -209,6 +253,7 @@ "IncentiveProgram", "IncentiveProgramStatusLiteral", "IncentiveProgramTypeLiteral", + "IndexedBalance", "LiveData", "LookupPoint", "LookupTickersForMarketInMultivariateEventCollectionRequest", @@ -232,6 +277,8 @@ "Orderbook", "OrderbookLevel", "Page", + "PaymentStatusLiteral", + "PaymentTypeLiteral", "PercentilePoint", "PlayByPlay", "PlayByPlayPeriod", @@ -260,5 +307,7 @@ "UpdateOrderGroupLimitRequest", "UpdateSubaccountNettingRequest", "UserDataTimestamp", + "UserFilterLiteral", "WeeklySchedule", + "Withdrawal", ] diff --git a/kalshi/models/account.py b/kalshi/models/account.py index 566861f..4cd8f5e 100644 --- a/kalshi/models/account.py +++ b/kalshi/models/account.py @@ -19,6 +19,29 @@ class RateLimit(BaseModel): model_config = {"extra": "allow"} +class EndpointTokenCost(BaseModel): + """Configured token cost for a single API endpoint.""" + + method: str + path: str + cost: int + + model_config = {"extra": "allow"} + + +class AccountEndpointCosts(BaseModel): + """Response from GET /account/endpoint_costs. + + Lists API v2 endpoints whose configured token cost differs from + ``default_cost``. Endpoints using the default are omitted. + """ + + default_cost: int + endpoint_costs: list[EndpointTokenCost] + + model_config = {"extra": "allow"} + + class AccountApiLimits(BaseModel): """Rate limits associated with the authenticated user's API tier. diff --git a/kalshi/models/communications.py b/kalshi/models/communications.py index e925535..f7f9e30 100644 --- a/kalshi/models/communications.py +++ b/kalshi/models/communications.py @@ -9,6 +9,9 @@ from kalshi.types import DollarDecimal, FixedPointCount +UserFilterLiteral = Literal["self"] +"""Filter for items created by the authenticated user. Spec: UserFilter enum.""" + class MveSelectedLeg(BaseModel): """A selected leg within a multivariate event collection RFQ.""" @@ -189,6 +192,7 @@ class CreateQuoteRequest(BaseModel): no_bid: DollarDecimal rest_remainder: bool subaccount: int | None = Field(default=None, ge=0, le=32) + post_only: bool | None = None model_config = {"extra": "forbid"} diff --git a/kalshi/models/order_groups.py b/kalshi/models/order_groups.py index dd3c311..58c909d 100644 --- a/kalshi/models/order_groups.py +++ b/kalshi/models/order_groups.py @@ -46,6 +46,7 @@ class CreateOrderGroupRequest(BaseModel): contracts_limit: int = Field(..., ge=1) subaccount: int | None = Field(default=None, ge=0) + exchange_index: int | None = None model_config = {"extra": "forbid"} diff --git a/kalshi/models/orders.py b/kalshi/models/orders.py index 5f3a160..cc17dde 100644 --- a/kalshi/models/orders.py +++ b/kalshi/models/orders.py @@ -30,6 +30,9 @@ OrderStatusLiteral = Literal["resting", "canceled", "executed"] """Order status filter for GET /portfolio/orders and /fcm/orders. Spec: OrderStatus enum.""" +BookSideLiteral = Literal["bid", "ask"] +"""Side of the book for V2 event-market orders. Spec: BookSide enum.""" + class Order(BaseModel): """A Kalshi order. @@ -183,6 +186,7 @@ class CreateOrderRequest(BaseModel): order_group_id: str | None = None cancel_order_on_pause: bool | None = None subaccount: int | None = None + exchange_index: int | None = None model_config = {"extra": "forbid"} @@ -247,6 +251,7 @@ class AmendOrderRequest(BaseModel): client_order_id: str | None = None updated_client_order_id: str | None = None subaccount: int | None = None + exchange_index: int | None = None model_config = {"extra": "forbid"} @@ -268,6 +273,7 @@ class DecreaseOrderRequest(BaseModel): reduce_by: int | None = None reduce_to: int | None = None subaccount: int | None = None + exchange_index: int | None = None model_config = {"extra": "forbid"} @@ -311,6 +317,7 @@ class BatchCancelOrdersRequestOrder(BaseModel): order_id: str subaccount: int | None = None + exchange_index: int | None = None model_config = {"extra": "forbid"} @@ -354,3 +361,219 @@ class OrderQueuePosition(BaseModel): ) model_config = {"extra": "allow", "populate_by_name": True} + + +# --------------------------------------------------------------------------- +# V2 event-market order models (spec v3.18.0). The legacy /portfolio/orders +# endpoints will be deprecated no earlier than May 6, 2026; the V2 family +# uses single-book bid/ask sides and fixed-point dollar prices. +# --------------------------------------------------------------------------- + + +class CreateOrderV2Request(BaseModel): + """Body for POST /portfolio/events/orders. + + Differences from v1 ``CreateOrderRequest`` worth knowing: + + - ``side`` is ``BookSideLiteral`` (``bid``/``ask``), not ``yes``/``no``. + V2 narrows the type on the model itself since there is no kwarg + overload at the resource-method boundary (see model_only V2 surface). + - ``client_order_id`` is **required** in V2, unlike V1 where it is + optional. The server uses it for idempotency in V2. + - Price is a single ``price: FixedPointDollars`` field rather than the + paired ``yes_price`` / ``no_price`` from V1. + """ + + ticker: str + client_order_id: str + side: BookSideLiteral + count: FixedPointCount + price: DollarDecimal + time_in_force: TimeInForceLiteral + self_trade_prevention_type: SelfTradePreventionTypeLiteral + expiration_time: int | None = None + post_only: bool | None = None + cancel_order_on_pause: bool | None = None + reduce_only: bool | None = None + subaccount: int | None = Field(default=None, ge=0, le=32) + order_group_id: str | None = None + exchange_index: int | None = None + + model_config = {"extra": "forbid"} + + +class CreateOrderV2Response(BaseModel): + """Response from POST /portfolio/events/orders.""" + + order_id: str + fill_count: FixedPointCount + remaining_count: FixedPointCount + ts_ms: int + client_order_id: str | None = None + average_fill_price: DollarDecimal | None = None + average_fee_paid: DollarDecimal | None = None + + model_config = {"extra": "allow"} + + +class CancelOrderV2Response(BaseModel): + """Response from DELETE /portfolio/events/orders/{order_id}.""" + + order_id: str + reduced_by: FixedPointCount + ts_ms: int + client_order_id: str | None = None + + model_config = {"extra": "allow"} + + +class DecreaseOrderV2Request(BaseModel): + """Body for POST /portfolio/events/orders/{order_id}/decrease. + + Spec marks all fields optional but server requires exactly one of + ``reduce_by`` or ``reduce_to``. Enforced at construction. + """ + + reduce_by: FixedPointCount | None = None + reduce_to: FixedPointCount | None = None + exchange_index: int | None = None + + model_config = {"extra": "forbid"} + + # DecreaseOrderV2Request has no subaccount field — the V2 spec routes + # that as a query param on the resource method, not on the request body. + + @model_validator(mode="after") + def _enforce_reduce_xor(self) -> DecreaseOrderV2Request: + if self.reduce_by is not None and self.reduce_to is not None: + raise ValueError( + "DecreaseOrderV2Request accepts reduce_by or reduce_to, not both" + ) + if self.reduce_by is None and self.reduce_to is None: + raise ValueError( + "DecreaseOrderV2Request requires either reduce_by or reduce_to" + ) + return self + + +class DecreaseOrderV2Response(BaseModel): + """Response from POST /portfolio/events/orders/{order_id}/decrease.""" + + order_id: str + remaining_count: FixedPointCount + ts_ms: int + client_order_id: str | None = None + + model_config = {"extra": "allow"} + + +class AmendOrderV2Request(BaseModel): + """Body for POST /portfolio/events/orders/{order_id}/amend.""" + + ticker: str + side: BookSideLiteral + price: DollarDecimal + count: FixedPointCount + client_order_id: str | None = None + updated_client_order_id: str | None = None + exchange_index: int | None = None + + model_config = {"extra": "forbid"} + + +class AmendOrderV2Response(BaseModel): + """Response from POST /portfolio/events/orders/{order_id}/amend.""" + + order_id: str + ts_ms: int + client_order_id: str | None = None + remaining_count: FixedPointCount | None = None + fill_count: FixedPointCount | None = None + average_fill_price: DollarDecimal | None = None + average_fee_paid: DollarDecimal | None = None + + model_config = {"extra": "allow"} + + +class BatchCreateOrdersV2Request(BaseModel): + """Body for POST /portfolio/events/orders/batched.""" + + orders: list[CreateOrderV2Request] + + model_config = {"extra": "forbid"} + + +class BatchCreateOrdersV2ResponseEntry(BaseModel): + """Single entry in BatchCreateOrdersV2Response — may carry an error per-order. + + All fields are optional because each entry can be either a success + (``order_id`` + ``fill_count`` + ``remaining_count`` + ``ts_ms`` set) + or an error (only ``error`` set, others omitted). Note this differs + from :class:`CreateOrderV2Response` where ``ts_ms`` is required — + that response is for a single order which either succeeds with a + timestamp or raises an HTTP error, never a per-entry error block. + """ + + order_id: str | None = None + client_order_id: str | None = None + fill_count: FixedPointCount | None = None + remaining_count: FixedPointCount | None = None + average_fill_price: DollarDecimal | None = None + average_fee_paid: DollarDecimal | None = None + ts_ms: int | None = None + error: dict[str, object] | None = None + + model_config = {"extra": "allow"} + + +class BatchCreateOrdersV2Response(BaseModel): + """Response from POST /portfolio/events/orders/batched.""" + + orders: list[BatchCreateOrdersV2ResponseEntry] + + model_config = {"extra": "allow"} + + +class BatchCancelOrdersV2RequestOrder(BaseModel): + """Single entry in BatchCancelOrdersV2Request.orders.""" + + order_id: str + subaccount: int | None = Field(default=None, ge=0, le=32) + exchange_index: int | None = None + + model_config = {"extra": "forbid"} + + +class BatchCancelOrdersV2Request(BaseModel): + """Body for DELETE /portfolio/events/orders/batched.""" + + orders: list[BatchCancelOrdersV2RequestOrder] + + model_config = {"extra": "forbid"} + + +class BatchCancelOrdersV2ResponseEntry(BaseModel): + """Single entry in BatchCancelOrdersV2Response — may carry an error per-order. + + Spec invariant (v3.18.0): when ``error`` is null, ``reduced_by`` is the + count canceled. When ``error`` is set, ``reduced_by`` is still present + and is ``0``. Both ``order_id`` and ``reduced_by`` are marked + ``required`` in the spec, so they are non-optional on this model — + Pydantic will raise ``ValidationError`` if upstream ever omits them. + """ + + order_id: str + reduced_by: FixedPointCount + client_order_id: str | None = None + ts_ms: int | None = None + error: dict[str, object] | None = None + + model_config = {"extra": "allow"} + + +class BatchCancelOrdersV2Response(BaseModel): + """Response from DELETE /portfolio/events/orders/batched.""" + + orders: list[BatchCancelOrdersV2ResponseEntry] + + model_config = {"extra": "allow"} diff --git a/kalshi/models/portfolio.py b/kalshi/models/portfolio.py index 543b627..67cf261 100644 --- a/kalshi/models/portfolio.py +++ b/kalshi/models/portfolio.py @@ -13,12 +13,40 @@ """Position settlement status filter for GET /fcm/positions. Spec: settlement_status query enum.""" +class IndexedBalance(BaseModel): + """Balance for a single exchange shard. Added by spec v3.18.0 alongside + the ``balance_breakdown`` field on :class:`Balance`. + + Currently only ``exchange_index=0`` is supported per spec. + + **Type note:** ``balance`` here is ``DollarDecimal`` (fixed-point + dollar string per spec), unlike :attr:`Balance.balance` which is + integer cents. Same field name, different semantics — be deliberate + when reading from ``balance.balance_breakdown[i].balance`` versus + ``balance.balance``. The :attr:`Balance.balance_dollars` field + rendered in dollars matches the breakdown's units. + """ + + exchange_index: int + balance: DollarDecimal + + model_config = {"extra": "allow"} + + class Balance(BaseModel): - """Account balance. Values are integer cents.""" + """Account balance. + + ``balance`` is integer cents (legacy field). ``balance_dollars`` is the + same value as a fixed-point dollar string, added by spec v3.18.0 and + now required on every response. ``balance_breakdown`` (optional) splits + the total across exchange shards when present. + """ balance: int + balance_dollars: DollarDecimal portfolio_value: int updated_ts: int + balance_breakdown: list[IndexedBalance] | None = None model_config = {"extra": "allow"} @@ -107,6 +135,44 @@ def has_next(self) -> bool: model_config = {"extra": "allow"} +PaymentStatusLiteral = Literal["pending", "applied", "failed", "returned"] +"""Status of a Deposit/Withdrawal. Spec defines two structurally-identical +inline enums (Deposit.status, Withdrawal.status); the SDK shares one alias +since the values are identical. +""" + +PaymentTypeLiteral = Literal["ach", "wire", "crypto", "debit", "apm"] +"""Payment method used for a deposit/withdrawal.""" + + +class Deposit(BaseModel): + """A single deposit history entry. Amounts are integer cents.""" + + id: str + status: PaymentStatusLiteral + type: PaymentTypeLiteral + amount_cents: int + fee_cents: int + created_ts: int + finalized_ts: int | None = None + + model_config = {"extra": "allow"} + + +class Withdrawal(BaseModel): + """A single withdrawal history entry. Amounts are integer cents.""" + + id: str + status: PaymentStatusLiteral + type: PaymentTypeLiteral + amount_cents: int + fee_cents: int + created_ts: int + finalized_ts: int | None = None + + model_config = {"extra": "allow"} + + class Settlement(BaseModel): """A settled market position.""" diff --git a/kalshi/resources/_base.py b/kalshi/resources/_base.py index 8a780c4..ccac58b 100644 --- a/kalshi/resources/_base.py +++ b/kalshi/resources/_base.py @@ -98,9 +98,13 @@ def _get(self, path: str, *, params: dict[str, Any] | None = None) -> dict[str, return result def _post( - self, path: str, *, json: dict[str, Any] | None = None + self, + path: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, ) -> dict[str, Any]: - response = self._transport.request("POST", path, json=json) + response = self._transport.request("POST", path, params=params, json=json) result: dict[str, Any] = response.json() return result @@ -224,9 +228,15 @@ async def _get(self, path: str, *, params: dict[str, Any] | None = None) -> dict return result async def _post( - self, path: str, *, json: dict[str, Any] | None = None + self, + path: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, ) -> dict[str, Any]: - response = await self._transport.request("POST", path, json=json) + response = await self._transport.request( + "POST", path, params=params, json=json, + ) result: dict[str, Any] = response.json() return result diff --git a/kalshi/resources/account.py b/kalshi/resources/account.py index fac7d29..d2242dc 100644 --- a/kalshi/resources/account.py +++ b/kalshi/resources/account.py @@ -2,7 +2,7 @@ from __future__ import annotations -from kalshi.models.account import AccountApiLimits +from kalshi.models.account import AccountApiLimits, AccountEndpointCosts from kalshi.resources._base import AsyncResource, SyncResource @@ -14,6 +14,12 @@ def limits(self) -> AccountApiLimits: data = self._get("/account/limits") return AccountApiLimits.model_validate(data) + def endpoint_costs(self) -> AccountEndpointCosts: + """List API v2 endpoints with non-default token costs.""" + self._require_auth() + data = self._get("/account/endpoint_costs") + return AccountEndpointCosts.model_validate(data) + class AsyncAccountResource(AsyncResource): """Async account API.""" @@ -22,3 +28,9 @@ async def limits(self) -> AccountApiLimits: self._require_auth() data = await self._get("/account/limits") return AccountApiLimits.model_validate(data) + + async def endpoint_costs(self) -> AccountEndpointCosts: + """List API v2 endpoints with non-default token costs.""" + self._require_auth() + data = await self._get("/account/endpoint_costs") + return AccountEndpointCosts.model_validate(data) diff --git a/kalshi/resources/communications.py b/kalshi/resources/communications.py index bf5296f..fc58856 100644 --- a/kalshi/resources/communications.py +++ b/kalshi/resources/communications.py @@ -18,6 +18,7 @@ GetQuoteResponse, GetRFQResponse, Quote, + UserFilterLiteral, ) from kalshi.resources._base import ( AsyncResource, @@ -29,18 +30,29 @@ def _require_quote_filter( - quote_creator_user_id: str | None, rfq_creator_user_id: str | None, + quote_creator_user_id: str | None, + rfq_creator_user_id: str | None, + user_filter: UserFilterLiteral | None, + rfq_user_filter: UserFilterLiteral | None, ) -> None: """Spec + demo require one of these filters on GET /communications/quotes. rfq_id alone is NOT sufficient (verified against demo during v0.11.0). Fail fast locally instead of paying a network round trip for a 400. + The ``user_filter`` / ``rfq_user_filter`` params added in spec v3.18.0 + accept ``"self"`` as a server-side shorthand for the caller's user-id, + so either of them also satisfies the filter requirement. """ - if quote_creator_user_id is None and rfq_creator_user_id is None: + if ( + quote_creator_user_id is None + and rfq_creator_user_id is None + and user_filter is None + and rfq_user_filter is None + ): raise ValueError( - "list_quotes requires one of quote_creator_user_id or " - "rfq_creator_user_id (server-side requirement; rfq_id alone " - "is not sufficient)." + "list_quotes requires one of quote_creator_user_id, " + "rfq_creator_user_id, user_filter, or rfq_user_filter " + "(server-side requirement; rfq_id alone is not sufficient)." ) @@ -56,6 +68,7 @@ def _list_rfqs_params( subaccount: int | None, status: str | None, creator_user_id: str | None, + user_filter: UserFilterLiteral | None, ) -> dict[str, Any]: return _params( cursor=cursor, @@ -65,6 +78,7 @@ def _list_rfqs_params( subaccount=subaccount, status=status, creator_user_id=creator_user_id, + user_filter=user_filter, ) @@ -79,6 +93,8 @@ def _list_quotes_params( rfq_creator_user_id: str | None, rfq_creator_subtrader_id: str | None, rfq_id: str | None, + user_filter: UserFilterLiteral | None, + rfq_user_filter: UserFilterLiteral | None, ) -> dict[str, Any]: return _params( cursor=cursor, @@ -90,6 +106,8 @@ def _list_quotes_params( rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, ) @@ -137,10 +155,12 @@ def _build_create_quote_body( no_bid: Decimal | str | float | int | None, rest_remainder: bool | None, subaccount: int | None, + post_only: bool | None, ) -> dict[str, Any]: _check_request_exclusive( request, rfq_id=rfq_id, yes_bid=yes_bid, no_bid=no_bid, rest_remainder=rest_remainder, subaccount=subaccount, + post_only=post_only, ) if request is None: if ( @@ -157,6 +177,7 @@ def _build_create_quote_body( no_bid=no_bid, # type: ignore[arg-type] rest_remainder=rest_remainder, subaccount=subaccount, + post_only=post_only, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -195,12 +216,14 @@ def list_rfqs( subaccount: int | None = None, status: str | None = None, creator_user_id: str | None = None, + user_filter: UserFilterLiteral | None = None, ) -> Page[RFQ]: self._require_auth() params = _list_rfqs_params( cursor=cursor, limit=limit, event_ticker=event_ticker, market_ticker=market_ticker, subaccount=subaccount, status=status, creator_user_id=creator_user_id, + user_filter=user_filter, ) return self._list("/communications/rfqs", RFQ, "rfqs", params=params) @@ -213,6 +236,7 @@ def list_all_rfqs( subaccount: int | None = None, status: str | None = None, creator_user_id: str | None = None, + user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, ) -> Iterator[RFQ]: self._require_auth() @@ -221,6 +245,7 @@ def list_all_rfqs( cursor=None, limit=limit, event_ticker=event_ticker, market_ticker=market_ticker, subaccount=subaccount, status=status, creator_user_id=creator_user_id, + user_filter=user_filter, ) return self._list_all( "/communications/rfqs", RFQ, "rfqs", @@ -285,9 +310,14 @@ def list_quotes( rfq_creator_user_id: str | None = None, rfq_creator_subtrader_id: str | None = None, rfq_id: str | None = None, + user_filter: UserFilterLiteral | None = None, + rfq_user_filter: UserFilterLiteral | None = None, ) -> Page[Quote]: self._require_auth() - _require_quote_filter(quote_creator_user_id, rfq_creator_user_id) + _require_quote_filter( + quote_creator_user_id, rfq_creator_user_id, + user_filter, rfq_user_filter, + ) params = _list_quotes_params( cursor=cursor, limit=limit, event_ticker=event_ticker, market_ticker=market_ticker, status=status, @@ -295,6 +325,8 @@ def list_quotes( rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, ) return self._list("/communications/quotes", Quote, "quotes", params=params) @@ -309,10 +341,15 @@ def list_all_quotes( rfq_creator_user_id: str | None = None, rfq_creator_subtrader_id: str | None = None, rfq_id: str | None = None, + user_filter: UserFilterLiteral | None = None, + rfq_user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, ) -> Iterator[Quote]: self._require_auth() - _require_quote_filter(quote_creator_user_id, rfq_creator_user_id) + _require_quote_filter( + quote_creator_user_id, rfq_creator_user_id, + user_filter, rfq_user_filter, + ) _validate_max_pages(max_pages) params = _list_quotes_params( cursor=None, limit=limit, event_ticker=event_ticker, @@ -321,6 +358,8 @@ def list_all_quotes( rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, ) return self._list_all( "/communications/quotes", Quote, "quotes", @@ -343,6 +382,7 @@ def create_quote( no_bid: Decimal | str | float | int, rest_remainder: bool, subaccount: int | None = ..., + post_only: bool | None = ..., ) -> CreateQuoteResponse: ... def create_quote( self, @@ -353,11 +393,13 @@ def create_quote( no_bid: Decimal | str | float | int | None = None, rest_remainder: bool | None = None, subaccount: int | None = None, + post_only: bool | None = None, ) -> CreateQuoteResponse: self._require_auth() body = _build_create_quote_body( request, rfq_id=rfq_id, yes_bid=yes_bid, no_bid=no_bid, rest_remainder=rest_remainder, subaccount=subaccount, + post_only=post_only, ) data = self._post("/communications/quotes", json=body) return CreateQuoteResponse.model_validate(data) @@ -409,12 +451,14 @@ async def list_rfqs( subaccount: int | None = None, status: str | None = None, creator_user_id: str | None = None, + user_filter: UserFilterLiteral | None = None, ) -> Page[RFQ]: self._require_auth() params = _list_rfqs_params( cursor=cursor, limit=limit, event_ticker=event_ticker, market_ticker=market_ticker, subaccount=subaccount, status=status, creator_user_id=creator_user_id, + user_filter=user_filter, ) return await self._list("/communications/rfqs", RFQ, "rfqs", params=params) @@ -427,6 +471,7 @@ def list_all_rfqs( subaccount: int | None = None, status: str | None = None, creator_user_id: str | None = None, + user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, ) -> AsyncIterator[RFQ]: # Plain `def` so _require_auth + _validate_max_pages run at call time. @@ -436,6 +481,7 @@ def list_all_rfqs( cursor=None, limit=limit, event_ticker=event_ticker, market_ticker=market_ticker, subaccount=subaccount, status=status, creator_user_id=creator_user_id, + user_filter=user_filter, ) return self._list_all( "/communications/rfqs", RFQ, "rfqs", @@ -500,9 +546,14 @@ async def list_quotes( rfq_creator_user_id: str | None = None, rfq_creator_subtrader_id: str | None = None, rfq_id: str | None = None, + user_filter: UserFilterLiteral | None = None, + rfq_user_filter: UserFilterLiteral | None = None, ) -> Page[Quote]: self._require_auth() - _require_quote_filter(quote_creator_user_id, rfq_creator_user_id) + _require_quote_filter( + quote_creator_user_id, rfq_creator_user_id, + user_filter, rfq_user_filter, + ) params = _list_quotes_params( cursor=cursor, limit=limit, event_ticker=event_ticker, market_ticker=market_ticker, status=status, @@ -510,6 +561,8 @@ async def list_quotes( rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, ) return await self._list( "/communications/quotes", Quote, "quotes", params=params, @@ -526,10 +579,15 @@ def list_all_quotes( rfq_creator_user_id: str | None = None, rfq_creator_subtrader_id: str | None = None, rfq_id: str | None = None, + user_filter: UserFilterLiteral | None = None, + rfq_user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, ) -> AsyncIterator[Quote]: self._require_auth() - _require_quote_filter(quote_creator_user_id, rfq_creator_user_id) + _require_quote_filter( + quote_creator_user_id, rfq_creator_user_id, + user_filter, rfq_user_filter, + ) _validate_max_pages(max_pages) params = _list_quotes_params( cursor=None, limit=limit, event_ticker=event_ticker, @@ -538,6 +596,8 @@ def list_all_quotes( rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, ) return self._list_all( "/communications/quotes", Quote, "quotes", @@ -562,6 +622,7 @@ async def create_quote( no_bid: Decimal | str | float | int, rest_remainder: bool, subaccount: int | None = ..., + post_only: bool | None = ..., ) -> CreateQuoteResponse: ... async def create_quote( self, @@ -572,11 +633,13 @@ async def create_quote( no_bid: Decimal | str | float | int | None = None, rest_remainder: bool | None = None, subaccount: int | None = None, + post_only: bool | None = None, ) -> CreateQuoteResponse: self._require_auth() body = _build_create_quote_body( request, rfq_id=rfq_id, yes_bid=yes_bid, no_bid=no_bid, rest_remainder=rest_remainder, subaccount=subaccount, + post_only=post_only, ) data = await self._post("/communications/quotes", json=body) return CreateQuoteResponse.model_validate(data) diff --git a/kalshi/resources/incentive_programs.py b/kalshi/resources/incentive_programs.py index ae8280e..1fa6f07 100644 --- a/kalshi/resources/incentive_programs.py +++ b/kalshi/resources/incentive_programs.py @@ -35,12 +35,14 @@ def list( *, status: IncentiveProgramStatusLiteral | None = None, incentive_type: IncentiveProgramTypeLiteral | None = None, + incentive_description: str | None = None, limit: int | None = None, cursor: str | None = None, ) -> Page[IncentiveProgram]: params = _params( status=status, type=incentive_type, + incentive_description=incentive_description, limit=limit, cursor=cursor, ) @@ -57,11 +59,17 @@ def list_all( *, status: IncentiveProgramStatusLiteral | None = None, incentive_type: IncentiveProgramTypeLiteral | None = None, + incentive_description: str | None = None, limit: int | None = None, max_pages: int | None = None, ) -> Iterator[IncentiveProgram]: _validate_max_pages(max_pages) - params = _params(status=status, type=incentive_type, limit=limit) + params = _params( + status=status, + type=incentive_type, + incentive_description=incentive_description, + limit=limit, + ) return self._list_all( "/incentive_programs", IncentiveProgram, @@ -80,12 +88,14 @@ async def list( *, status: IncentiveProgramStatusLiteral | None = None, incentive_type: IncentiveProgramTypeLiteral | None = None, + incentive_description: str | None = None, limit: int | None = None, cursor: str | None = None, ) -> Page[IncentiveProgram]: params = _params( status=status, type=incentive_type, + incentive_description=incentive_description, limit=limit, cursor=cursor, ) @@ -102,12 +112,18 @@ def list_all( *, status: IncentiveProgramStatusLiteral | None = None, incentive_type: IncentiveProgramTypeLiteral | None = None, + incentive_description: str | None = None, limit: int | None = None, max_pages: int | None = None, ) -> AsyncIterator[IncentiveProgram]: """Returns an async iterator — use ``async for``.""" _validate_max_pages(max_pages) - params = _params(status=status, type=incentive_type, limit=limit) + params = _params( + status=status, + type=incentive_type, + incentive_description=incentive_description, + limit=limit, + ) return self._list_all( "/incentive_programs", IncentiveProgram, diff --git a/kalshi/resources/order_groups.py b/kalshi/resources/order_groups.py index 1acf763..ab361a4 100644 --- a/kalshi/resources/order_groups.py +++ b/kalshi/resources/order_groups.py @@ -27,9 +27,11 @@ def _build_create_order_group_body( *, contracts_limit: int | None, subaccount: int | None, + exchange_index: int | None, ) -> dict[str, Any]: _check_request_exclusive( request, contracts_limit=contracts_limit, subaccount=subaccount, + exchange_index=exchange_index, ) if request is None: if contracts_limit is None: @@ -38,6 +40,7 @@ def _build_create_order_group_body( ) request = CreateOrderGroupRequest( contracts_limit=contracts_limit, subaccount=subaccount, + exchange_index=exchange_index, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -83,7 +86,11 @@ def create( ) -> CreateOrderGroupResponse: ... @overload def create( - self, *, contracts_limit: int, subaccount: int | None = ..., + self, + *, + contracts_limit: int, + subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> CreateOrderGroupResponse: ... def create( self, @@ -91,18 +98,26 @@ def create( request: CreateOrderGroupRequest | None = None, contracts_limit: int | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> CreateOrderGroupResponse: # POST path is /order_groups/create, not /order_groups. self._require_auth() body = _build_create_order_group_body( request, contracts_limit=contracts_limit, subaccount=subaccount, + exchange_index=exchange_index, ) data = self._post("/portfolio/order_groups/create", json=body) return CreateOrderGroupResponse.model_validate(data) - def delete(self, order_group_id: str, *, subaccount: int | None = None) -> None: + def delete( + self, + order_group_id: str, + *, + subaccount: int | None = None, + exchange_index: int | None = None, + ) -> None: self._require_auth() - params = _params(subaccount=subaccount) + params = _params(subaccount=subaccount, exchange_index=exchange_index) self._delete(f"/portfolio/order_groups/{order_group_id}", params=params) def reset(self, order_group_id: str, *, subaccount: int | None = None) -> None: @@ -170,7 +185,11 @@ async def create( ) -> CreateOrderGroupResponse: ... @overload async def create( - self, *, contracts_limit: int, subaccount: int | None = ..., + self, + *, + contracts_limit: int, + subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> CreateOrderGroupResponse: ... async def create( self, @@ -178,19 +197,25 @@ async def create( request: CreateOrderGroupRequest | None = None, contracts_limit: int | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> CreateOrderGroupResponse: self._require_auth() body = _build_create_order_group_body( request, contracts_limit=contracts_limit, subaccount=subaccount, + exchange_index=exchange_index, ) data = await self._post("/portfolio/order_groups/create", json=body) return CreateOrderGroupResponse.model_validate(data) async def delete( - self, order_group_id: str, *, subaccount: int | None = None, + self, + order_group_id: str, + *, + subaccount: int | None = None, + exchange_index: int | None = None, ) -> None: self._require_auth() - params = _params(subaccount=subaccount) + params = _params(subaccount=subaccount, exchange_index=exchange_index) await self._delete(f"/portfolio/order_groups/{order_group_id}", params=params) async def reset( diff --git a/kalshi/resources/orders.py b/kalshi/resources/orders.py index fe97ae3..021c288 100644 --- a/kalshi/resources/orders.py +++ b/kalshi/resources/orders.py @@ -13,11 +13,22 @@ ActionLiteral, AmendOrderRequest, AmendOrderResponse, + AmendOrderV2Request, + AmendOrderV2Response, BatchCancelOrdersRequest, BatchCancelOrdersRequestOrder, + BatchCancelOrdersV2Request, + BatchCancelOrdersV2Response, BatchCreateOrdersRequest, + BatchCreateOrdersV2Request, + BatchCreateOrdersV2Response, + CancelOrderV2Response, CreateOrderRequest, + CreateOrderV2Request, + CreateOrderV2Response, DecreaseOrderRequest, + DecreaseOrderV2Request, + DecreaseOrderV2Response, Fill, Order, OrderQueuePosition, @@ -64,6 +75,7 @@ def _build_create_order_body( order_group_id: str | None, cancel_order_on_pause: bool | None, subaccount: int | None, + exchange_index: int | None, ) -> dict[str, Any]: _check_request_exclusive( request, @@ -76,6 +88,7 @@ def _build_create_order_body( order_group_id=order_group_id, cancel_order_on_pause=cancel_order_on_pause, subaccount=subaccount, + exchange_index=exchange_index, ) if request is None: if ticker is None or side is None: @@ -99,6 +112,7 @@ def _build_create_order_body( order_group_id=order_group_id, cancel_order_on_pause=cancel_order_on_pause, subaccount=subaccount, + exchange_index=exchange_index, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -149,6 +163,7 @@ def _build_amend_body( client_order_id: str | None, updated_client_order_id: str | None, subaccount: int | None, + exchange_index: int | None, ) -> dict[str, Any]: _check_request_exclusive( request, @@ -157,6 +172,7 @@ def _build_amend_body( client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, subaccount=subaccount, + exchange_index=exchange_index, ) if request is None: if ticker is None or side is None or action is None: @@ -178,6 +194,7 @@ def _build_amend_body( client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, subaccount=subaccount, + exchange_index=exchange_index, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -188,9 +205,11 @@ def _build_decrease_body( reduce_by: int | None, reduce_to: int | None, subaccount: int | None, + exchange_index: int | None, ) -> dict[str, Any]: _check_request_exclusive( - request, reduce_by=reduce_by, reduce_to=reduce_to, subaccount=subaccount, + request, reduce_by=reduce_by, reduce_to=reduce_to, + subaccount=subaccount, exchange_index=exchange_index, ) if request is None: # Method-level guards mirror DecreaseOrderRequest._enforce_reduce_xor @@ -204,6 +223,7 @@ def _build_decrease_body( reduce_by=reduce_by, reduce_to=reduce_to, subaccount=subaccount, + exchange_index=exchange_index, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -302,6 +322,7 @@ def create( order_group_id: str | None = ..., cancel_order_on_pause: bool | None = ..., subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> Order: ... def create( self, @@ -323,6 +344,7 @@ def create( order_group_id: str | None = None, cancel_order_on_pause: bool | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> Order: """Place a new order. @@ -351,6 +373,7 @@ def create( order_group_id=order_group_id, cancel_order_on_pause=cancel_order_on_pause, subaccount=subaccount, + exchange_index=exchange_index, ) data = self._post("/portfolio/orders", json=body) order_data = data.get("order", data) @@ -362,9 +385,15 @@ def get(self, order_id: str) -> Order: order_data = data.get("order", data) return Order.model_validate(order_data) - def cancel(self, order_id: str, *, subaccount: int | None = None) -> None: + def cancel( + self, + order_id: str, + *, + subaccount: int | None = None, + exchange_index: int | None = None, + ) -> None: self._require_auth() - params = _params(subaccount=subaccount) + params = _params(subaccount=subaccount, exchange_index=exchange_index) self._delete(f"/portfolio/orders/{order_id}", params=params) def list( @@ -530,6 +559,7 @@ def amend( client_order_id: str | None = ..., updated_client_order_id: str | None = ..., subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> AmendOrderResponse: ... def amend( self, @@ -545,6 +575,7 @@ def amend( client_order_id: str | None = None, updated_client_order_id: str | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> AmendOrderResponse: self._require_auth() body = _build_amend_body( @@ -554,6 +585,7 @@ def amend( client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, subaccount=subaccount, + exchange_index=exchange_index, ) data = self._post(f"/portfolio/orders/{order_id}/amend", json=body) return AmendOrderResponse.model_validate(data) @@ -570,6 +602,7 @@ def decrease( reduce_by: int | None = ..., reduce_to: int | None = ..., subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> Order: ... def decrease( self, @@ -579,11 +612,12 @@ def decrease( reduce_by: int | None = None, reduce_to: int | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> Order: self._require_auth() body = _build_decrease_body( request, reduce_by=reduce_by, reduce_to=reduce_to, - subaccount=subaccount, + subaccount=subaccount, exchange_index=exchange_index, ) data = self._post(f"/portfolio/orders/{order_id}/decrease", json=body) order_data = data.get("order", data) @@ -611,6 +645,104 @@ def queue_position(self, order_id: str) -> Decimal: data = self._get(f"/portfolio/orders/{order_id}/queue_position") return _parse_queue_position(data) + # ------------------------------------------------------------------ + # V2 event-market orders (spec v3.18.0, paths /portfolio/events/orders). + # Model-only API surface — pass a fully-constructed request model. + # ------------------------------------------------------------------ + + def create_v2(self, *, request: CreateOrderV2Request) -> CreateOrderV2Response: + self._require_auth() + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = self._post("/portfolio/events/orders", json=body) + return CreateOrderV2Response.model_validate(data) + + def cancel_v2( + self, + order_id: str, + *, + subaccount: int | None = None, + exchange_index: int | None = None, + ) -> CancelOrderV2Response: + self._require_auth() + params = _params(subaccount=subaccount, exchange_index=exchange_index) + data = self._delete( + f"/portfolio/events/orders/{order_id}", params=params, + ) + if data is None: + raise KalshiError( + "Expected CancelOrderV2Response body, got 204 No Content." + ) + return CancelOrderV2Response.model_validate(data) + + def amend_v2( + self, + order_id: str, + *, + request: AmendOrderV2Request, + subaccount: int | None = None, + ) -> AmendOrderV2Response: + """Amend an event-market order (V2). + + Per OpenAPI spec v3.18.0, this endpoint's ``subaccount`` is a + **query** parameter while ``exchange_index`` lives in the request + **body** (on ``AmendOrderV2Request``). The asymmetry mirrors the + spec exactly — do not move ``exchange_index`` into ``params``. + """ + self._require_auth() + params = _params(subaccount=subaccount) + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = self._post( + f"/portfolio/events/orders/{order_id}/amend", + params=params, json=body, + ) + return AmendOrderV2Response.model_validate(data) + + def decrease_v2( + self, + order_id: str, + *, + request: DecreaseOrderV2Request, + subaccount: int | None = None, + ) -> DecreaseOrderV2Response: + """Decrease an event-market order (V2). + + Same spec-driven asymmetry as :meth:`amend_v2`: ``subaccount`` is + a query param; ``exchange_index`` lives on the body model. Note + also that ``cancel_v2`` carries both as query params (no body) — + that endpoint declares ``ExchangeIndexQuery`` in its parameters + list, while amend/decrease declare only ``SubaccountQueryDefaultPrimary``. + """ + self._require_auth() + params = _params(subaccount=subaccount) + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = self._post( + f"/portfolio/events/orders/{order_id}/decrease", + params=params, json=body, + ) + return DecreaseOrderV2Response.model_validate(data) + + def batch_create_v2( + self, *, request: BatchCreateOrdersV2Request, + ) -> BatchCreateOrdersV2Response: + self._require_auth() + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = self._post("/portfolio/events/orders/batched", json=body) + return BatchCreateOrdersV2Response.model_validate(data) + + def batch_cancel_v2( + self, *, request: BatchCancelOrdersV2Request, + ) -> BatchCancelOrdersV2Response: + self._require_auth() + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = self._delete_with_body( + "/portfolio/events/orders/batched", json=body, + ) + if data is None: + raise KalshiError( + "Expected BatchCancelOrdersV2Response body, got 204 No Content." + ) + return BatchCancelOrdersV2Response.model_validate(data) + class AsyncOrdersResource(AsyncResource): """Async orders API.""" @@ -637,6 +769,7 @@ async def create( order_group_id: str | None = ..., cancel_order_on_pause: bool | None = ..., subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> Order: ... async def create( self, @@ -658,6 +791,7 @@ async def create( order_group_id: str | None = None, cancel_order_on_pause: bool | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> Order: """Place a new order. @@ -686,6 +820,7 @@ async def create( order_group_id=order_group_id, cancel_order_on_pause=cancel_order_on_pause, subaccount=subaccount, + exchange_index=exchange_index, ) data = await self._post("/portfolio/orders", json=body) order_data = data.get("order", data) @@ -697,9 +832,15 @@ async def get(self, order_id: str) -> Order: order_data = data.get("order", data) return Order.model_validate(order_data) - async def cancel(self, order_id: str, *, subaccount: int | None = None) -> None: + async def cancel( + self, + order_id: str, + *, + subaccount: int | None = None, + exchange_index: int | None = None, + ) -> None: self._require_auth() - params = _params(subaccount=subaccount) + params = _params(subaccount=subaccount, exchange_index=exchange_index) await self._delete(f"/portfolio/orders/{order_id}", params=params) async def list( @@ -866,6 +1007,7 @@ async def amend( client_order_id: str | None = ..., updated_client_order_id: str | None = ..., subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> AmendOrderResponse: ... async def amend( self, @@ -881,6 +1023,7 @@ async def amend( client_order_id: str | None = None, updated_client_order_id: str | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> AmendOrderResponse: self._require_auth() body = _build_amend_body( @@ -890,6 +1033,7 @@ async def amend( client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, subaccount=subaccount, + exchange_index=exchange_index, ) data = await self._post(f"/portfolio/orders/{order_id}/amend", json=body) return AmendOrderResponse.model_validate(data) @@ -906,6 +1050,7 @@ async def decrease( reduce_by: int | None = ..., reduce_to: int | None = ..., subaccount: int | None = ..., + exchange_index: int | None = ..., ) -> Order: ... async def decrease( self, @@ -915,11 +1060,12 @@ async def decrease( reduce_by: int | None = None, reduce_to: int | None = None, subaccount: int | None = None, + exchange_index: int | None = None, ) -> Order: self._require_auth() body = _build_decrease_body( request, reduce_by=reduce_by, reduce_to=reduce_to, - subaccount=subaccount, + subaccount=subaccount, exchange_index=exchange_index, ) data = await self._post(f"/portfolio/orders/{order_id}/decrease", json=body) order_data = data.get("order", data) @@ -946,3 +1092,100 @@ async def queue_position(self, order_id: str) -> Decimal: self._require_auth() data = await self._get(f"/portfolio/orders/{order_id}/queue_position") return _parse_queue_position(data) + + # V2 event-market orders (spec v3.18.0). See OrdersResource counterparts. + + async def create_v2( + self, *, request: CreateOrderV2Request, + ) -> CreateOrderV2Response: + self._require_auth() + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = await self._post("/portfolio/events/orders", json=body) + return CreateOrderV2Response.model_validate(data) + + async def cancel_v2( + self, + order_id: str, + *, + subaccount: int | None = None, + exchange_index: int | None = None, + ) -> CancelOrderV2Response: + self._require_auth() + params = _params(subaccount=subaccount, exchange_index=exchange_index) + data = await self._delete( + f"/portfolio/events/orders/{order_id}", params=params, + ) + if data is None: + raise KalshiError( + "Expected CancelOrderV2Response body, got 204 No Content." + ) + return CancelOrderV2Response.model_validate(data) + + async def amend_v2( + self, + order_id: str, + *, + request: AmendOrderV2Request, + subaccount: int | None = None, + ) -> AmendOrderV2Response: + """Amend an event-market order (V2). + + Per OpenAPI spec v3.18.0, this endpoint's ``subaccount`` is a + **query** parameter while ``exchange_index`` lives in the request + **body** (on ``AmendOrderV2Request``). The asymmetry mirrors the + spec exactly — do not move ``exchange_index`` into ``params``. + """ + self._require_auth() + params = _params(subaccount=subaccount) + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = await self._post( + f"/portfolio/events/orders/{order_id}/amend", + params=params, json=body, + ) + return AmendOrderV2Response.model_validate(data) + + async def decrease_v2( + self, + order_id: str, + *, + request: DecreaseOrderV2Request, + subaccount: int | None = None, + ) -> DecreaseOrderV2Response: + """Decrease an event-market order (V2). + + Same spec-driven asymmetry as :meth:`amend_v2`: ``subaccount`` is + a query param; ``exchange_index`` lives on the body model. Note + also that ``cancel_v2`` carries both as query params (no body) — + that endpoint declares ``ExchangeIndexQuery`` in its parameters + list, while amend/decrease declare only ``SubaccountQueryDefaultPrimary``. + """ + self._require_auth() + params = _params(subaccount=subaccount) + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = await self._post( + f"/portfolio/events/orders/{order_id}/decrease", + params=params, json=body, + ) + return DecreaseOrderV2Response.model_validate(data) + + async def batch_create_v2( + self, *, request: BatchCreateOrdersV2Request, + ) -> BatchCreateOrdersV2Response: + self._require_auth() + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = await self._post("/portfolio/events/orders/batched", json=body) + return BatchCreateOrdersV2Response.model_validate(data) + + async def batch_cancel_v2( + self, *, request: BatchCancelOrdersV2Request, + ) -> BatchCancelOrdersV2Response: + self._require_auth() + body = request.model_dump(exclude_none=True, by_alias=True, mode="json") + data = await self._delete_with_body( + "/portfolio/events/orders/batched", json=body, + ) + if data is None: + raise KalshiError( + "Expected BatchCancelOrdersV2Response body, got 204 No Content." + ) + return BatchCancelOrdersV2Response.model_validate(data) diff --git a/kalshi/resources/portfolio.py b/kalshi/resources/portfolio.py index 8166e45..279cba4 100644 --- a/kalshi/resources/portfolio.py +++ b/kalshi/resources/portfolio.py @@ -8,9 +8,11 @@ from kalshi.models.common import Page from kalshi.models.portfolio import ( Balance, + Deposit, PositionsResponse, Settlement, TotalRestingOrderValue, + Withdrawal, ) from kalshi.resources._base import ( AsyncResource, @@ -141,6 +143,58 @@ def total_resting_order_value(self) -> TotalRestingOrderValue: data = self._get("/portfolio/summary/total_resting_order_value") return TotalRestingOrderValue.model_validate(data) + def deposits( + self, + *, + limit: int | None = None, + cursor: str | None = None, + ) -> Page[Deposit]: + self._require_auth() + params = _params(limit=limit, cursor=cursor) + return self._list( + "/portfolio/deposits", Deposit, "deposits", params=params, + ) + + def deposits_all( + self, + *, + limit: int | None = None, + max_pages: int | None = None, + ) -> Iterator[Deposit]: + self._require_auth() + _validate_max_pages(max_pages) + params = _params(limit=limit) + return self._list_all( + "/portfolio/deposits", Deposit, "deposits", + params=params, max_pages=max_pages, + ) + + def withdrawals( + self, + *, + limit: int | None = None, + cursor: str | None = None, + ) -> Page[Withdrawal]: + self._require_auth() + params = _params(limit=limit, cursor=cursor) + return self._list( + "/portfolio/withdrawals", Withdrawal, "withdrawals", params=params, + ) + + def withdrawals_all( + self, + *, + limit: int | None = None, + max_pages: int | None = None, + ) -> Iterator[Withdrawal]: + self._require_auth() + _validate_max_pages(max_pages) + params = _params(limit=limit) + return self._list_all( + "/portfolio/withdrawals", Withdrawal, "withdrawals", + params=params, max_pages=max_pages, + ) + class AsyncPortfolioResource(AsyncResource): """Async portfolio API.""" @@ -218,3 +272,57 @@ async def total_resting_order_value(self) -> TotalRestingOrderValue: self._require_auth() data = await self._get("/portfolio/summary/total_resting_order_value") return TotalRestingOrderValue.model_validate(data) + + async def deposits( + self, + *, + limit: int | None = None, + cursor: str | None = None, + ) -> Page[Deposit]: + self._require_auth() + params = _params(limit=limit, cursor=cursor) + return await self._list( + "/portfolio/deposits", Deposit, "deposits", params=params, + ) + + def deposits_all( + self, + *, + limit: int | None = None, + max_pages: int | None = None, + ) -> AsyncIterator[Deposit]: + """Returns an async iterator — use ``async for``.""" + self._require_auth() + _validate_max_pages(max_pages) + params = _params(limit=limit) + return self._list_all( + "/portfolio/deposits", Deposit, "deposits", + params=params, max_pages=max_pages, + ) + + async def withdrawals( + self, + *, + limit: int | None = None, + cursor: str | None = None, + ) -> Page[Withdrawal]: + self._require_auth() + params = _params(limit=limit, cursor=cursor) + return await self._list( + "/portfolio/withdrawals", Withdrawal, "withdrawals", params=params, + ) + + def withdrawals_all( + self, + *, + limit: int | None = None, + max_pages: int | None = None, + ) -> AsyncIterator[Withdrawal]: + """Returns an async iterator — use ``async for``.""" + self._require_auth() + _validate_max_pages(max_pages) + params = _params(limit=limit) + return self._list_all( + "/portfolio/withdrawals", Withdrawal, "withdrawals", + params=params, max_pages=max_pages, + ) diff --git a/pyproject.toml b/pyproject.toml index 82c41ba..22e95a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "2.0.0" +version = "2.1.0" description = "A professional Python SDK for the Kalshi prediction markets API" readme = "README.md" license = { text = "MIT" } diff --git a/specs/asyncapi.yaml b/specs/asyncapi.yaml new file mode 100644 index 0000000..a4a7090 --- /dev/null +++ b/specs/asyncapi.yaml @@ -0,0 +1,3434 @@ +asyncapi: 3.0.0 +info: + title: Kalshi Market Data WebSocket API + version: 2.0.0 + description: > + WebSocket API for receiving real-time market data notifications and updates + on Kalshi. + + + This API provides multiple channels of information over a single connection. + After establishing + + a WebSocket connection, clients can subscribe to specific channels and + markets of interest. + + + All messages are encoded in JSON format and the communication is + asynchronous. + + + ## Error Codes Reference + + + When a command fails, the server responds with an error message containing a + numeric code: + + + | Code | Error | Description | + + |------|-------|-------------| + + | 1 | Unable to process message | General processing error | + + | 2 | Params required | Missing params object in command | + + | 3 | Channels required | Missing channels array in subscribe | + + | 4 | Subscription IDs required | Missing sids in unsubscribe | + + | 5 | Unknown command | Invalid command name | + + | 6 | Already subscribed | Duplicate subscription attempt | + + | 7 | Unknown subscription ID | Subscription ID not found | + + | 8 | Unknown channel name | Invalid channel in subscribe | + + | 9 | Authentication required | Channel requires authenticated connection | + + | 10 | Channel error | Channel-specific error | + + | 11 | Invalid parameter | Malformed parameter value | + + | 12 | Exactly one subscription ID is required | For update_subscription | + + | 13 | Unsupported action | Invalid action for update_subscription | + + | 14 | Market Ticker required | Missing market specification (market_ticker + or market_id) | + + | 15 | Action required | Missing action in update_subscription | + + | 16 | Market not found | Invalid market_ticker or market_id | + + | 17 | Internal error | Server-side processing error | + + | 18 | Command timeout | Server timed out while processing command | + + | 19 | shard_factor must be > 0 | Invalid shard_factor | + + | 20 | shard_factor is required when shard_key is set | Missing shard_factor + when shard_key is set | + + | 21 | shard_key must be >= 0 and < shard_factor | Invalid shard_key | + + | 22 | shard_factor must be <= 100 | shard_factor too large | + contact: + name: Kalshi Support + url: https://kalshi.com + email: support@kalshi.com + license: + name: Proprietary + url: https://kalshi.com/terms + tags: + - name: control-frames + description: WebSocket control frames for connection management + - name: commands + description: Client-to-server command messages + - name: responses + description: Server responses to commands + - name: market-data + description: Real-time market data updates + - name: private + description: Private/authenticated data channels +servers: + production: + host: external-api-ws.kalshi.com + pathname: /trade-api/ws/v2 + protocol: wss + description: Production Trade API WebSocket server (encrypted connection only) + security: + - $ref: '#/components/securitySchemes/apiKey' +defaultContentType: application/json +channels: + root: + address: / + title: WebSocket Connection + description: > + Main WebSocket connection endpoint. All communication happens through this + single connection. + + Authentication is required to establish the connection; include API key + headers during the WebSocket handshake. + + Some channels carry only public market data, but the connection itself + still requires authentication. + + Use the subscribe command to subscribe to specific data channels. For more + information, see the [Getting + Started](https://docs.kalshi.com/getting_started/quick_start_websockets) + guide. + bindings: + ws: + method: GET + messages: + subscribeCommand: + $ref: '#/components/messages/subscribeCommand' + unsubscribeCommand: + $ref: '#/components/messages/unsubscribeCommand' + updateSubscriptionCommand: + $ref: '#/components/messages/updateSubscriptionCommand' + updateSubscriptionDeleteCommand: + $ref: '#/components/messages/updateSubscriptionDeleteCommand' + updateSubscriptionSingleSidCommand: + $ref: '#/components/messages/updateSubscriptionSingleSidCommand' + listSubscriptionsCommand: + $ref: '#/components/messages/listSubscriptionsCommand' + subscribedResponse: + $ref: '#/components/messages/subscribedResponse' + listSubscriptionsResponse: + $ref: '#/components/messages/listSubscriptionsResponse' + unsubscribedResponse: + $ref: '#/components/messages/unsubscribedResponse' + okResponse: + $ref: '#/components/messages/okResponse' + errorResponse: + $ref: '#/components/messages/errorResponse' + control_frames: + address: / + title: Connection Keep-Alive + description: > + WebSocket control frames for connection management. + + + Kalshi sends Ping frames (`0x9`) every 10 seconds with body `heartbeat` to + maintain the connection. + + Clients should respond with Pong frames (`0xA`). Clients may also send + Ping frames to which Kalshi will respond with Pong. + messages: + incomingPing: + $ref: '#/components/messages/incomingPing' + incomingPong: + $ref: '#/components/messages/incomingPong' + outgoingPing: + $ref: '#/components/messages/outgoingPing' + outgoingPong: + $ref: '#/components/messages/outgoingPong' + orderbook_delta: + address: orderbook_delta + title: Orderbook Updates + description: > + Real-time orderbook price level changes. Provides incremental updates to + maintain a live orderbook. + + + **Requirements:** + + - Authentication required + + - Market specification required: + - Use `market_ticker` (string) for a single market + - Use `market_tickers` (array of strings) for multiple markets + - `market_id`/`market_ids` are not supported for this channel + - Sends `orderbook_snapshot` first, then incremental `orderbook_delta` + updates + + - Supports `update_subscription` with `add_markets` / `delete_markets` / + `get_snapshot` actions + + - `get_snapshot` returns an `orderbook_snapshot` for the requested + `market_tickers` without modifying the subscription + + + **Use case:** Building and maintaining a real-time orderbook + messages: + orderbookSnapshot: + $ref: '#/components/messages/orderbookSnapshot' + orderbookDelta: + $ref: '#/components/messages/orderbookDelta' + ticker: + address: ticker + title: Market Ticker + description: > + Market price, volume, and open interest updates. + + + **Requirements:** + + - No additional channel-level authentication beyond the authenticated + WebSocket connection + + - Market specification optional (omit to receive all markets) + + - Supports `market_ticker`/`market_tickers` and `market_id`/`market_ids` + + - Updates sent whenever any ticker field changes + + + **Use case:** Displaying current market prices and statistics + messages: + ticker: + $ref: '#/components/messages/ticker' + trade: + address: trade + title: Public Trades + description: > + Public trade notifications when trades occur. + + + **Requirements:** + + - No additional channel-level authentication beyond the authenticated + WebSocket connection + + - Market specification optional (omit to receive all trades) + + - Updates sent immediately after trade execution + + + **Use case:** Trade feed, volume analysis + messages: + trade: + $ref: '#/components/messages/trade' + fill: + address: fill + title: User Fills + description: > + Your order fill notifications. Requires authentication. + + + **Requirements:** + + - Authentication required + + - Market specification optional via `market_ticker`/`market_tickers` (omit + to receive all your fills) + + - Supports `update_subscription` with `add_markets` / `delete_markets` + + - Updates sent immediately when your orders are filled + + + **Use case:** Tracking your trading activity + messages: + fill: + $ref: '#/components/messages/fill' + market_positions: + address: market_positions + title: Market Positions + description: > + Real-time updates of your positions in markets. Requires authentication. + + + **Requirements:** + + - Authentication required + + - Market specification optional (omit to receive all positions) + + - Filters are by `market_ticker`/`market_tickers` only; + `market_id`/`market_ids` are not supported + + - Updates sent when your position changes due to trades, settlements, etc. + + + **Monetary Values:** + + All monetary values are returned as fixed-point dollar strings (`_dollars` + suffix). + + + **Use case:** Portfolio tracking, position monitoring, P&L calculations + messages: + marketPosition: + $ref: '#/components/messages/marketPosition' + market_lifecycle_v2: + address: market_lifecycle_v2 + title: Market & Event Lifecycle + description: > + Market state changes and event creation notifications. + + + **Requirements:** + + - No additional channel-level authentication beyond the authenticated + WebSocket connection + + - Receives all market and event lifecycle notifications (`market_ticker` + filters are not supported) + + - Event creation notifications + + + **Use case:** Tracking market lifecycle including creation, + de(activation), close date changes, determination, settlement, price level + structure changes, and metadata updates + messages: + marketLifecycleV2: + $ref: '#/components/messages/marketLifecycleV2' + eventLifecycle: + $ref: '#/components/messages/eventLifecycle' + multivariate_market_lifecycle: + address: multivariate_market_lifecycle + title: Multivariate Market & Event Lifecycle + description: > + Multivariate event (MVE) market state changes and event creation + notifications. + + + **Requirements:** + + - No additional channel-level authentication beyond the authenticated + WebSocket connection + + - Receives all multivariate market lifecycle notifications + (`market_ticker` filters are not supported) + + - Only emits lifecycle updates for multivariate events + + - Event creation notifications + + + **Use case:** Tracking multivariate market lifecycle including creation, + de(activation), close date changes, determination, settlement + messages: + multivariateMarketLifecycle: + $ref: '#/components/messages/multivariateMarketLifecycle' + eventLifecycle: + $ref: '#/components/messages/eventLifecycle' + multivariate: + address: multivariate + title: Multivariate Lookups (Deprecated) + description: > + Deprecated: this channel predates RFQs and should not be used for new + integrations. + + + Multivariate collection lookup notifications. + + + **Requirements:** + + - No additional channel-level authentication beyond the authenticated + WebSocket connection + + - No filtering parameters; subscription is global + + + **Use case:** Tracking multivariate lookup interest for legacy + integrations + messages: + multivariateLookup: + $ref: '#/components/messages/multivariateLookup' + communications: + address: communications + title: Communications + description: > + Real-time Request for Quote (RFQ) and quote notifications. Requires + authentication. + + + **Requirements:** + + - Authentication required + + - Market specification ignored + + - Optional sharding for fanout control: + - `shard_factor` (1-100) and `shard_key` (0 <= key < shard_factor) + - RFQ events (RFQCreated, RFQDeleted) always sent + + - Quote events (QuoteCreated, QuoteAccepted, QuoteExecuted) are only sent + if you created the quote OR you created the RFQ + + + **Use case:** Tracking RFQs you create and quotes on your RFQs, or quotes + you create on others' RFQs. Use QuoteExecuted to correlate fill messages + with quotes via client_order_id. + messages: + rfqCreated: + $ref: '#/components/messages/rfqCreated' + rfqDeleted: + $ref: '#/components/messages/rfqDeleted' + quoteCreated: + $ref: '#/components/messages/quoteCreated' + quoteAccepted: + $ref: '#/components/messages/quoteAccepted' + quoteExecuted: + $ref: '#/components/messages/quoteExecuted' + order_group_updates: + address: order_group_updates + title: Order Group Updates + description: > + Real-time order group lifecycle and limit updates. Requires + authentication. + + + **Requirements:** + + - Authentication required + + - Market specification ignored + + - Updates sent when order groups are created, triggered, reset, deleted, + or have limits updated + + + **Use case:** Tracking order group lifecycle and limits + messages: + orderGroupUpdates: + $ref: '#/components/messages/orderGroupUpdates' + user_orders: + address: user_orders + title: User Orders + description: > + Real-time order created and updated notifications. Requires + authentication. + + + **Requirements:** + + - Authentication required + + - Market specification optional via `market_tickers` (omit to receive all + orders) + + - Supports `update_subscription` with `add_markets` / `delete_markets` + actions + + - Updates sent when your orders are created, filled, canceled, or + otherwise updated + + + **Use case:** Tracking your resting orders, fills, and cancellations in + real time + messages: + userOrder: + $ref: '#/components/messages/userOrder' +operations: + sendPing: + action: receive + title: Send Ping + summary: Client sends Ping control frame to elicit Pong from Kalshi + channel: + $ref: '#/channels/control_frames' + messages: + - $ref: '#/channels/control_frames/messages/incomingPing' + tags: + - name: control-frames + sendPong: + action: receive + title: Send Pong + summary: Client sends Pong control frame in response to Kalshi Ping + channel: + $ref: '#/channels/control_frames' + messages: + - $ref: '#/channels/control_frames/messages/incomingPong' + tags: + - name: control-frames + sendSubscribe: + action: receive + title: Subscribe to Channels + summary: Subscribe to one or more market data channels + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/subscribeCommand' + tags: + - name: commands + sendUnsubscribe: + action: receive + title: Unsubscribe from Channels + summary: Cancel one or more active subscriptions + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/unsubscribeCommand' + tags: + - name: commands + sendListSubscriptions: + action: receive + title: List Subscriptions + summary: List all active subscriptions + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/listSubscriptionsCommand' + tags: + - name: commands + sendUpdateSubscription: + action: receive + title: Update Subscription - Add Markets + summary: Add markets to an existing subscription + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/updateSubscriptionCommand' + tags: + - name: commands + sendUpdateSubscriptionDelete: + action: receive + title: Update Subscription - Delete Markets + summary: Remove markets from an existing subscription + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/updateSubscriptionDeleteCommand' + tags: + - name: commands + sendUpdateSubscriptionSingleSid: + action: receive + title: Update Subscription - Single SID + summary: Update subscription using single sid parameter + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/updateSubscriptionSingleSidCommand' + tags: + - name: commands + receivePing: + action: send + title: Receive Ping + summary: Kalshi sends Ping control frame with body 'heartbeat' + channel: + $ref: '#/channels/control_frames' + messages: + - $ref: '#/channels/control_frames/messages/outgoingPing' + tags: + - name: control-frames + receivePong: + action: send + title: Receive Pong + summary: Kalshi sends Pong control frame in response to client Ping + channel: + $ref: '#/channels/control_frames' + messages: + - $ref: '#/channels/control_frames/messages/outgoingPong' + tags: + - name: control-frames + receiveSubscribed: + action: send + title: Subscription Confirmed + summary: Receive confirmation that subscription was successful + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/subscribedResponse' + tags: + - name: responses + receiveUnsubscribed: + action: send + title: Unsubscription Confirmed + summary: Receive confirmation that unsubscription was successful + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/unsubscribedResponse' + tags: + - name: responses + receiveOk: + action: send + title: Update Confirmed + summary: Receive confirmation that subscription update was successful + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/okResponse' + tags: + - name: responses + receiveListSubscriptions: + action: send + title: List Subscriptions Response + summary: Receive list of all active subscriptions + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/listSubscriptionsResponse' + tags: + - name: responses + receiveError: + action: send + title: Error Response + summary: Receive error message when a command fails + description: > + The server sends error responses when a command cannot be processed + successfully. + + Each error includes a numeric code identifying the error type and a + human-readable message. + + + Error codes are listed in the schema definition below. + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/errorResponse' + tags: + - name: responses + receiveOrderbookSnapshot: + action: send + title: Orderbook Snapshot + summary: Receive complete orderbook state + channel: + $ref: '#/channels/orderbook_delta' + messages: + - $ref: '#/channels/orderbook_delta/messages/orderbookSnapshot' + tags: + - name: market-data + receiveOrderbookDelta: + action: send + title: Orderbook Update + summary: Receive incremental orderbook changes + channel: + $ref: '#/channels/orderbook_delta' + messages: + - $ref: '#/channels/orderbook_delta/messages/orderbookDelta' + tags: + - name: market-data + receiveTicker: + action: send + title: Ticker Update + summary: Receive market ticker updates + channel: + $ref: '#/channels/ticker' + messages: + - $ref: '#/channels/ticker/messages/ticker' + tags: + - name: market-data + receiveTrade: + action: send + title: Trade Update + summary: Receive public trade notifications + channel: + $ref: '#/channels/trade' + messages: + - $ref: '#/channels/trade/messages/trade' + tags: + - name: market-data + receiveFill: + action: send + title: Fill Notification + summary: Receive notifications for your fills + channel: + $ref: '#/channels/fill' + messages: + - $ref: '#/channels/fill/messages/fill' + tags: + - name: private + receiveMarketPosition: + action: send + title: Market Position Update + summary: Receive updates for your market positions + channel: + $ref: '#/channels/market_positions' + messages: + - $ref: '#/channels/market_positions/messages/marketPosition' + tags: + - name: private + receiveMarketLifecycleV2: + action: send + title: Market Lifecycle Event + summary: Receive market lifecycle updates (open, close, determination, etc.) + channel: + $ref: '#/channels/market_lifecycle_v2' + messages: + - $ref: '#/channels/market_lifecycle_v2/messages/marketLifecycleV2' + tags: + - name: market-data + receiveEventLifecycle: + action: send + title: Event Lifecycle + summary: Receive event creation notifications + channel: + $ref: '#/channels/market_lifecycle_v2' + messages: + - $ref: '#/channels/market_lifecycle_v2/messages/eventLifecycle' + tags: + - name: market-data + receiveMultivariateMarketLifecycle: + action: send + title: Multivariate Market Lifecycle Event + summary: >- + Receive multivariate market lifecycle updates (open, close, determination, + etc.) + channel: + $ref: '#/channels/multivariate_market_lifecycle' + messages: + - $ref: >- + #/channels/multivariate_market_lifecycle/messages/multivariateMarketLifecycle + tags: + - name: market-data + receiveMultivariateEventLifecycle: + action: send + title: Multivariate Event Lifecycle + summary: Receive multivariate event creation notifications + channel: + $ref: '#/channels/multivariate_market_lifecycle' + messages: + - $ref: '#/channels/multivariate_market_lifecycle/messages/eventLifecycle' + tags: + - name: market-data + receiveMultivariateLookup: + action: send + title: Multivariate Lookup + summary: Receive multivariate collection lookup notifications + channel: + $ref: '#/channels/multivariate' + messages: + - $ref: '#/channels/multivariate/messages/multivariateLookup' + tags: + - name: market-data + receiveRFQCreated: + action: send + title: RFQ Created + summary: Receive RFQ created notifications + channel: + $ref: '#/channels/communications' + messages: + - $ref: '#/channels/communications/messages/rfqCreated' + tags: + - name: private + receiveRFQDeleted: + action: send + title: RFQ Deleted + summary: Receive RFQ deleted notifications + channel: + $ref: '#/channels/communications' + messages: + - $ref: '#/channels/communications/messages/rfqDeleted' + tags: + - name: private + receiveQuoteCreated: + action: send + title: Quote Created + summary: Receive quote created notifications + channel: + $ref: '#/channels/communications' + messages: + - $ref: '#/channels/communications/messages/quoteCreated' + tags: + - name: private + receiveQuoteAccepted: + action: send + title: Quote Accepted + summary: Receive quote accepted notifications + channel: + $ref: '#/channels/communications' + messages: + - $ref: '#/channels/communications/messages/quoteAccepted' + tags: + - name: private + receiveQuoteExecuted: + action: send + title: Quote Executed + summary: >- + Receive quote executed notifications with order details for fill + correlation + channel: + $ref: '#/channels/communications' + messages: + - $ref: '#/channels/communications/messages/quoteExecuted' + tags: + - name: private + receiveOrderGroupUpdates: + action: send + title: Order Group Updates + summary: Receive order group lifecycle and limit updates + channel: + $ref: '#/channels/order_group_updates' + messages: + - $ref: '#/channels/order_group_updates/messages/orderGroupUpdates' + tags: + - name: private + receiveUserOrder: + action: send + title: User Order Update + summary: Receive notifications for your order creates and updates + channel: + $ref: '#/channels/user_orders' + messages: + - $ref: '#/channels/user_orders/messages/userOrder' + tags: + - name: private +components: + messages: + incomingPing: + name: ping + title: Ping + summary: Client sends Ping frame (0x9) to elicit Pong from Kalshi + contentType: application/octet-stream + payload: + type: string + const: '' + examples: + - name: emptyPing + summary: Client sends ping + payload: '' + incomingPong: + name: pong + title: Pong + summary: Client replies to Ping with Pong Frame (0xA) + contentType: application/octet-stream + payload: + type: string + const: '' + examples: + - name: emptyPong + summary: Client sends pong + payload: '' + subscribeCommand: + name: subscribe + title: Subscribe Command + summary: Subscribe to one or more channels + contentType: application/json + payload: + $ref: '#/components/schemas/subscribeCommandPayload' + examples: + - name: subscribeToOrderbook + summary: Subscribe to orderbook_delta for specific market + payload: + id: 1 + cmd: subscribe + params: + channels: + - orderbook_delta + market_ticker: CPI-22DEC-TN0.1 + - name: subscribeMultipleChannels + summary: Subscribe to multiple channels for multiple markets + payload: + id: 2 + cmd: subscribe + params: + channels: + - orderbook_delta + - ticker + market_tickers: + - FED-23DEC-T3.00 + - CORIVER-2024-T1030 + - name: subscribeFillAll + summary: Subscribe to fill channel for all markets + payload: + id: 3 + cmd: subscribe + params: + channels: + - fill + - name: subscribeUserOrders + summary: Subscribe to user_orders channel for specific markets + payload: + id: 8 + cmd: subscribe + params: + channels: + - user_orders + market_tickers: + - FED-23DEC-T3.00 + - CORIVER-2024-T1030 + - name: subscribeAllChannelsOneMarket + summary: Subscribe to market-data channels for one market + payload: + id: 4 + cmd: subscribe + params: + channels: + - orderbook_delta + - ticker + - trade + - market_lifecycle_v2 + market_ticker: TSLA-23DEC-T200 + - name: subscribeMarketLifecycle + summary: Subscribe to market lifecycle events for all markets + payload: + id: 5 + cmd: subscribe + params: + channels: + - market_lifecycle_v2 + - name: subscribeMultivariateMarketLifecycle + summary: >- + Subscribe to multivariate market lifecycle events for all + multivariate markets + payload: + id: 15 + cmd: subscribe + params: + channels: + - multivariate_market_lifecycle + - name: subscribeCommunications + summary: Subscribe to communications (RFQs and quotes) + payload: + id: 6 + cmd: subscribe + params: + channels: + - communications + - name: subscribeTickerWithSnapshot + summary: Subscribe to ticker with initial snapshot + payload: + id: 7 + cmd: subscribe + params: + channels: + - ticker + market_tickers: + - FED-23DEC-T3.00 + send_initial_snapshot: true + unsubscribeCommand: + name: unsubscribe + title: Unsubscribe Command + summary: Cancel one or more subscriptions + contentType: application/json + payload: + $ref: '#/components/schemas/unsubscribeCommandPayload' + examples: + - name: unsubscribeMultiple + summary: Unsubscribe from multiple subscriptions + payload: + id: 124 + cmd: unsubscribe + params: + sids: + - 1 + - 2 + listSubscriptionsCommand: + name: list_subscriptions + title: List Subscriptions Command + summary: List all active subscriptions + contentType: application/json + payload: + $ref: '#/components/schemas/listSubscriptionsCommandPayload' + examples: + - name: listSubscriptions + summary: List all active subscriptions + payload: + id: 3 + cmd: list_subscriptions + updateSubscriptionCommand: + name: update_subscription_add + title: Update Subscription - Add Markets + summary: Add markets to an existing subscription + contentType: application/json + payload: + $ref: '#/components/schemas/updateSubscriptionCommandPayload' + examples: + - name: addMarkets + summary: Add markets to subscription + payload: + id: 124 + cmd: update_subscription + params: + sids: + - 456 + market_tickers: + - NEW-MARKET-1 + - NEW-MARKET-2 + action: add_markets + updateSubscriptionDeleteCommand: + name: update_subscription_delete + title: Update Subscription - Delete Markets + summary: Remove markets from an existing subscription + contentType: application/json + payload: + $ref: '#/components/schemas/updateSubscriptionCommandPayload' + examples: + - name: deleteMarkets + summary: Remove markets from subscription + payload: + id: 125 + cmd: update_subscription + params: + sids: + - 456 + market_tickers: + - MARKET-TO-REMOVE-1 + - MARKET-TO-REMOVE-2 + action: delete_markets + updateSubscriptionGetSnapshotCommand: + name: update_subscription_get_snapshot + title: Update Subscription - Get Snapshot + summary: Request an orderbook snapshot without modifying the subscription + contentType: application/json + payload: + $ref: '#/components/schemas/updateSubscriptionCommandPayload' + examples: + - name: getSnapshot + summary: Request orderbook snapshot for specific markets + payload: + id: 127 + cmd: update_subscription + params: + sids: + - 456 + market_tickers: + - MARKET-1 + - MARKET-2 + action: get_snapshot + updateSubscriptionSingleSidCommand: + name: update_subscription_single_sid + title: Update Subscription - Single SID Format + summary: Update subscription using sid parameter instead of sids array + contentType: application/json + payload: + $ref: '#/components/schemas/updateSubscriptionCommandPayload' + examples: + - name: addMarketsWithSingleSid + summary: Add markets using sid instead of sids array + payload: + id: 126 + cmd: update_subscription + params: + sid: 456 + market_tickers: + - NEW-MARKET-3 + - NEW-MARKET-4 + action: add_markets + subscribedResponse: + name: subscribed + title: Subscribed Response + summary: Confirmation that subscription was successful + contentType: application/json + payload: + $ref: '#/components/schemas/subscribedResponsePayload' + examples: + - name: subscriptionConfirmed + summary: Subscription confirmed + payload: + id: 1 + type: subscribed + msg: + channel: orderbook_delta + sid: 1 + unsubscribedResponse: + name: unsubscribed + title: Unsubscribed Response + summary: Confirmation that unsubscription was successful + contentType: application/json + payload: + $ref: '#/components/schemas/unsubscribedResponsePayload' + examples: + - name: unsubscriptionConfirmed + summary: Unsubscription confirmed + payload: + id: 102 + sid: 2 + seq: 7 + type: unsubscribed + okResponse: + name: ok + title: OK Response + summary: Successful update operation response + contentType: application/json + payload: + $ref: '#/components/schemas/okResponsePayload' + examples: + - name: updateSuccess + summary: Update subscription success + payload: + id: 123 + sid: 456 + seq: 222 + type: ok + msg: + market_tickers: + - MARKET-1 + - MARKET-2 + - MARKET-3 + listSubscriptionsResponse: + name: list_subscriptions + title: List Subscriptions Response + summary: Response containing all active subscriptions + contentType: application/json + payload: + $ref: '#/components/schemas/listSubscriptionsResponsePayload' + examples: + - name: listSubscriptionsResponse + summary: List of active subscriptions + payload: + id: 3 + type: ok + msg: + - channel: orderbook_delta + sid: 1 + - channel: ticker + sid: 2 + - channel: fill + sid: 3 + errorResponse: + name: error + title: Error Response + summary: Error response for failed operations + description: > + Error responses are sent when a command fails. Each error includes a + numeric code and a human-readable message. + + + ## Error Codes Reference + + + | Code | Error | Description | + + |------|-------|-------------| + + | 1 | Unable to process message | General processing error | + + | 2 | Params required | Missing params object in command | + + | 3 | Channels required | Missing channels array in subscribe | + + | 4 | Subscription IDs required | Missing sids in unsubscribe | + + | 5 | Unknown command | Invalid command name | + + | 6 | Already subscribed | Duplicate subscription attempt | + + | 7 | Unknown subscription ID | Subscription ID not found | + + | 8 | Unknown channel name | Invalid channel in subscribe | + + | 9 | Authentication required | Channel requires authenticated + connection | + + | 10 | Channel error | Channel-specific error | + + | 11 | Invalid parameter | Malformed parameter value | + + | 12 | Exactly one subscription ID is required | For update_subscription + | + + | 13 | Unsupported action | Invalid action for update_subscription | + + | 14 | Market Ticker required | Missing market specification + (market_ticker or market_id) | + + | 15 | Action required | Missing action in update_subscription | + + | 16 | Market not found | Invalid market_ticker or market_id | + + | 17 | Internal error | Server-side processing error | + + | 18 | Command timeout | Server timed out while processing command | + + | 19 | shard_factor must be > 0 | Invalid shard_factor | + + | 20 | shard_factor is required when shard_key is set | Missing + shard_factor when shard_key is set | + + | 21 | shard_key must be >= 0 and < shard_factor | Invalid shard_key | + + | 22 | shard_factor must be <= 100 | shard_factor too large | + contentType: application/json + payload: + $ref: '#/components/schemas/errorResponsePayload' + examples: + - name: alreadySubscribed + summary: Already subscribed error + payload: + id: 123 + type: error + msg: + code: 6 + msg: Already subscribed + - name: unknownChannel + summary: Unknown channel error + payload: + id: 124 + type: error + msg: + code: 8 + msg: Unknown channel name + - name: paramsRequired + summary: Missing params object + payload: + id: 125 + type: error + msg: + code: 2 + msg: Params required + - name: authenticationRequired + summary: Channel requires authenticated connection + payload: + id: 126 + type: error + msg: + code: 9 + msg: Authentication required + - name: marketNotFound + summary: Invalid market ticker + payload: + id: 127 + type: error + msg: + code: 16 + msg: Market not found + market_ticker: INVALID-MARKET + outgoingPing: + name: ping + title: Ping + summary: Kalshi sends Ping (0x9) with body 'heartbeat' to elicit Pong from client + contentType: application/octet-stream + payload: + type: string + const: heartbeat + examples: + - name: heartbeatPing + summary: Kalshi sends heartbeat ping + payload: heartbeat + outgoingPong: + name: pong + title: Pong + summary: Kalshi responds to client Ping with Pong frame (0xA) + contentType: application/octet-stream + payload: + type: string + const: '' + examples: + - name: emptyPong + summary: Kalshi sends pong + payload: '' + orderbookSnapshot: + name: orderbook_snapshot + title: Orderbook Snapshot + summary: Complete view of the order book's aggregated price levels + contentType: application/json + payload: + $ref: '#/components/schemas/orderbookSnapshotPayload' + examples: + - name: orderbookWithBothSides + summary: Orderbook snapshot with yes and no sides + payload: + type: orderbook_snapshot + sid: 2 + seq: 2 + msg: + market_ticker: FED-23DEC-T3.00 + market_id: 9b0f6b43-5b68-4f9f-9f02-9a2d1b8ac1a1 + yes_dollars_fp: + - - '0.0800' + - '300.00' + - - '0.2200' + - '333.00' + no_dollars_fp: + - - '0.5400' + - '20.00' + - - '0.5600' + - '146.00' + orderbookDelta: + name: orderbook_delta + title: Orderbook Delta + summary: Update to be applied to the current order book view + contentType: application/json + payload: + $ref: '#/components/schemas/orderbookDeltaPayload' + examples: + - name: priceLevelUpdate + summary: Orderbook price level update + payload: + type: orderbook_delta + sid: 2 + seq: 3 + msg: + market_ticker: FED-23DEC-T3.00 + market_id: 9b0f6b43-5b68-4f9f-9f02-9a2d1b8ac1a1 + price_dollars: '0.960' + delta_fp: '-54.00' + side: 'yes' + ts: '2022-11-22T20:44:01Z' + ts_ms: 1669149841000 + ticker: + name: ticker + title: Ticker Update + summary: Market price ticker information + contentType: application/json + payload: + $ref: '#/components/schemas/tickerPayload' + examples: + - name: tickerUpdate + summary: Ticker update + payload: + type: ticker + sid: 11 + msg: + market_ticker: FED-23DEC-T3.00 + market_id: 9b0f6b43-5b68-4f9f-9f02-9a2d1b8ac1a1 + price_dollars: '0.480' + yes_bid_dollars: '0.450' + yes_ask_dollars: '0.530' + volume_fp: '33896.00' + open_interest_fp: '20422.00' + dollar_volume: 16948 + dollar_open_interest: 10211 + yes_bid_size_fp: '300.00' + yes_ask_size_fp: '150.00' + last_trade_size_fp: '25.00' + ts: 1669149841 + ts_ms: 1669149841000 + time: '2022-11-22T20:44:01Z' + trade: + name: trade + title: Trade Update + summary: Public trade information + contentType: application/json + payload: + $ref: '#/components/schemas/tradePayload' + examples: + - name: tradeNotification + summary: Trade notification + payload: + type: trade + sid: 11 + msg: + trade_id: d91bc706-ee49-470d-82d8-11418bda6fed + market_ticker: HIGHNY-22DEC23-B53.5 + yes_price_dollars: '0.360' + no_price_dollars: '0.640' + count_fp: '136.00' + taker_side: 'no' + ts: 1669149841 + ts_ms: 1669149841000 + fill: + name: fill + title: Fill Update + summary: Private fill information for authenticated user + contentType: application/json + payload: + $ref: '#/components/schemas/fillPayload' + examples: + - name: userFill + summary: User fill notification + payload: + type: fill + sid: 13 + msg: + trade_id: d91bc706-ee49-470d-82d8-11418bda6fed + order_id: ee587a1c-8b87-4dcf-b721-9f6f790619fa + market_ticker: HIGHNY-22DEC23-B53.5 + is_taker: true + side: 'yes' + yes_price_dollars: '0.750' + count_fp: '278.00' + action: buy + ts: 1671899397 + ts_ms: 1671899397000 + post_position_fp: '500.00' + purchased_side: 'yes' + subaccount: 3 + marketLifecycleV2: + name: market_lifecycle_v2 + title: Market Lifecycle V2 + summary: >- + Market lifecycle events (created, activated, deactivated, + close_date_updated, determined, settled, price_level_structure_updated, + metadata_updated) + contentType: application/json + payload: + $ref: '#/components/schemas/marketLifecycleV2Payload' + examples: + - name: marketCreated + summary: Market created event + payload: + type: market_lifecycle_v2 + sid: 13 + msg: + market_ticker: INXD-23SEP14-B4487 + event_type: created + open_ts: 1694635200 + close_ts: 1694721600 + price_level_structure: linear_cent + additional_metadata: + name: S&P 500 daily return on Sep 14 + title: S&P 500 closes up by 0.02% or more + yes_sub_title: S&P 500 closes up 0.02%+ + no_sub_title: S&P 500 closes up <0.02% + rules_primary: The S&P 500 index level at 4:00 PM ET... + rules_secondary: '' + can_close_early: true + event_ticker: INXD-23SEP14 + expected_expiration_ts: 1694721600 + strike_type: greater + floor_strike: 4487 + - name: priceLevelStructureUpdated + summary: Price level structure updated event + payload: + type: market_lifecycle_v2 + sid: 13 + msg: + market_ticker: INXD-23SEP14-B4487 + event_type: price_level_structure_updated + price_level_structure: deci_cent + - name: metadataUpdated + summary: Market metadata updated event + payload: + type: market_lifecycle_v2 + sid: 13 + msg: + market_ticker: KXBTC-25APR30-T0915-B95000 + event_type: metadata_updated + floor_strike: 95000 + - name: metadataUpdatedSubtitle + summary: Market metadata updated event (subtitle) + payload: + type: market_lifecycle_v2 + sid: 13 + msg: + market_ticker: KXBTC15M-26APR160100-00 + event_type: metadata_updated + yes_sub_title: above $95,000 + multivariateMarketLifecycle: + name: multivariate_market_lifecycle + title: Multivariate Market Lifecycle + summary: >- + Multivariate market lifecycle events (created, activated, deactivated, + close_date_updated, determined, settled) + contentType: application/json + payload: + $ref: '#/components/schemas/multivariateMarketLifecyclePayload' + examples: + - name: mveMarketCreated + summary: Multivariate market created event + payload: + type: multivariate_market_lifecycle + sid: 14 + msg: + market_ticker: KXMVE-TEST-EVENT-M1 + event_type: created + open_ts: 1773936000 + close_ts: 1774022400 + additional_metadata: + name: MVE One + title: Market 1 + yes_sub_title: YES 1 + no_sub_title: NO 1 + rules_primary: Rule 1 + rules_secondary: Rule 2 + can_close_early: true + event_ticker: KXMVE-TEST-EVENT + expected_expiration_ts: 1774029600 + eventLifecycle: + name: event_lifecycle + title: Event Lifecycle + summary: Event creation notification + contentType: application/json + payload: + $ref: '#/components/schemas/eventLifecyclePayload' + examples: + - name: eventCreated + summary: Event created + payload: + type: event_lifecycle + sid: 5 + msg: + event_ticker: KXQUICKSETTLE-26JAN25H2150 + title: What will 1+1 equal on Jan 25 at 21:50? + subtitle: Jan 25 at 21:50 + collateral_return_type: MECNET + series_ticker: KXQUICKSETTLE + multivariateLookup: + name: multivariate_lookup + title: Multivariate Lookup (Deprecated) + summary: Deprecated multivariate collection lookup notification + description: This message predates RFQs and should not be used for new integrations. + contentType: application/json + payload: + $ref: '#/components/schemas/multivariateLookupPayload' + examples: + - name: lookupRecorded + summary: Multivariate lookup recorded + payload: + type: multivariate_lookup + sid: 13 + msg: + collection_ticker: KXOSCARWINNERS-25 + event_ticker: KXOSCARWINNERS-25C0CE5 + market_ticker: KXOSCARWINNERS-25C0CE5-36353 + selected_markets: + - event_ticker: KXOSCARACTO-25 + market_ticker: KXOSCARACTO-25-AB + side: 'yes' + - event_ticker: KXOSCARACTR-25 + market_ticker: KXOSCARACTR-25-DM + side: 'yes' + marketPosition: + name: market_position + title: Market Position Update + summary: Real-time position updates for authenticated user + contentType: application/json + payload: + $ref: '#/components/schemas/marketPositionPayload' + examples: + - name: positionUpdate + summary: User position update + payload: + type: market_position + sid: 14 + msg: + user_id: user123 + market_ticker: FED-23DEC-T3.00 + position_fp: '100.00' + position_cost_dollars: '50.0000' + realized_pnl_dollars: '10.0000' + fees_paid_dollars: '1.0000' + position_fee_cost_dollars: '0.5000' + volume_fp: '15.00' + orderGroupUpdates: + name: order_group_updates + title: Order Group Updates + summary: Order group lifecycle and limit updates for authenticated user + contentType: application/json + payload: + $ref: '#/components/schemas/orderGroupUpdatesPayload' + examples: + - name: orderGroupLimitUpdated + summary: Order group limit updated + payload: + type: order_group_updates + sid: 21 + seq: 7 + msg: + event_type: limit_updated + order_group_id: og_123 + contracts_limit_fp: '150.00' + userOrder: + name: user_order + title: User Order Update + summary: Real-time order updates for authenticated user + contentType: application/json + payload: + $ref: '#/components/schemas/userOrderPayload' + examples: + - name: userOrderCreated + summary: User order created notification + payload: + type: user_order + sid: 22 + msg: + order_id: ee587a1c-8b87-4dcf-b721-9f6f790619fa + user_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + ticker: FED-23DEC-T3.00 + status: resting + side: 'yes' + is_yes: true + yes_price_dollars: '0.3500' + fill_count_fp: '0.00' + remaining_count_fp: '10.00' + initial_count_fp: '10.00' + taker_fill_cost_dollars: '0.0000' + maker_fill_cost_dollars: '0.0000' + client_order_id: my-order-1 + order_group_id: og_123 + self_trade_prevention_type: taker_at_cross + created_time: '2024-12-01T10:00:00Z' + created_ts_ms: 1733047200000 + expiration_time: '2024-12-01T11:00:00Z' + expiration_ts_ms: 1733050800000 + subaccount_number: 0 + rfqCreated: + name: rfq_created + title: RFQ Created + summary: Notification when an RFQ is created + contentType: application/json + payload: + $ref: '#/components/schemas/rfqCreatedPayload' + examples: + - name: rfqCreatedNotification + summary: RFQ created notification + payload: + type: rfq_created + sid: 15 + msg: + id: rfq_123 + creator_id: '' + market_ticker: FED-23DEC-T3.00 + event_ticker: FED-23DEC + contracts_fp: '100.00' + target_cost_dollars: '0.35' + created_ts: '2024-12-01T10:00:00Z' + rfqDeleted: + name: rfq_deleted + title: RFQ Deleted + summary: Notification when an RFQ is deleted + contentType: application/json + payload: + $ref: '#/components/schemas/rfqDeletedPayload' + examples: + - name: rfqDeletedNotification + summary: RFQ deleted notification + payload: + type: rfq_deleted + sid: 15 + msg: + id: rfq_123 + creator_id: comm_abc123 + market_ticker: FED-23DEC-T3.00 + event_ticker: FED-23DEC + contracts_fp: '100.00' + target_cost_dollars: '0.35' + deleted_ts: '2024-12-01T10:05:00Z' + quoteCreated: + name: quote_created + title: Quote Created + summary: Notification when a quote is created on an RFQ + contentType: application/json + payload: + $ref: '#/components/schemas/quoteCreatedPayload' + examples: + - name: quoteCreatedNotification + summary: Quote created notification + payload: + type: quote_created + sid: 15 + msg: + quote_id: quote_456 + rfq_id: rfq_123 + quote_creator_id: comm_def456 + market_ticker: FED-23DEC-T3.00 + event_ticker: FED-23DEC + yes_bid_dollars: '0.35' + no_bid_dollars: '0.65' + yes_contracts_offered_fp: '100.00' + no_contracts_offered_fp: '200.00' + rfq_target_cost_dollars: '0.35' + created_ts: '2024-12-01T10:02:00Z' + quoteAccepted: + name: quote_accepted + title: Quote Accepted + summary: Notification when a quote is accepted + contentType: application/json + payload: + $ref: '#/components/schemas/quoteAcceptedPayload' + examples: + - name: quoteAcceptedNotification + summary: Quote accepted notification + payload: + type: quote_accepted + sid: 15 + msg: + quote_id: quote_456 + rfq_id: rfq_123 + quote_creator_id: comm_def456 + market_ticker: FED-23DEC-T3.00 + event_ticker: FED-23DEC + yes_bid_dollars: '0.35' + no_bid_dollars: '0.65' + accepted_side: 'yes' + contracts_accepted_fp: '50.00' + yes_contracts_offered_fp: '100.00' + no_contracts_offered_fp: '200.00' + rfq_target_cost_dollars: '0.35' + quoteExecuted: + name: quote_executed + title: Quote Executed + summary: Notification when a quote is executed and orders are placed + description: > + Sent to both the maker (quote creator) and taker (RFQ creator) when a + quote is executed. + + Each user receives their own order details (order_id and + client_order_id). + + Use this to correlate subsequent fill messages with the original quote. + contentType: application/json + payload: + $ref: '#/components/schemas/quoteExecutedPayload' + examples: + - name: quoteExecutedNotification + summary: Quote executed notification + payload: + type: quote_executed + sid: 15 + msg: + quote_id: quote_456 + rfq_id: rfq_123 + quote_creator_id: a1b2c3d4e5f6... + rfq_creator_id: f6e5d4c3b2a1... + order_id: order_789 + client_order_id: my_client_order_123 + market_ticker: FED-23DEC-T3.00 + executed_ts: '2024-12-01T10:05:00Z' + schemas: + commandId: + type: integer + description: > + Unique ID of the command request. Generated by the client and should be + unique within a WS session. + + The simplest way to use it would be to start from 1 and then increment + the value for every new command sent to the server. + + If the id is set to 0, the server treats it the same way as if there was + no id. + minimum: 0 + subscriptionId: + type: integer + description: >- + Server-generated subscription identifier (sid) used to identify the + channel + minimum: 1 + sequenceNumber: + type: integer + description: >- + Sequential number that should be checked if you want to guarantee you + received all the messages. Used for snapshot/delta consistency + minimum: 1 + marketTicker: + type: string + description: Unique market identifier + pattern: ^[A-Z0-9-]+$ + examples: + - FED-23DEC-T3.00 + - HIGHNY-22DEC23-B53.5 + marketId: + type: string + description: Unique market UUID + format: uuid + marketSide: + type: string + description: Market side + enum: + - 'yes' + - 'no' + bookSide: + type: string + description: >- + Side of the book for an order or trade. 'bid' is equivalent to + outcome_side 'yes'; 'ask' is equivalent to outcome_side 'no'. + enum: + - bid + - ask + orderAction: + type: string + description: Order action type + enum: + - buy + - sell + subscribeCommandPayload: + type: object + required: + - id + - cmd + - params + properties: + id: + $ref: '#/components/schemas/commandId' + cmd: + type: string + const: subscribe + params: + type: object + required: + - channels + properties: + channels: + type: array + description: List of channels to subscribe to + items: + type: string + enum: + - orderbook_delta + - ticker + - trade + - fill + - market_positions + - market_lifecycle_v2 + - multivariate_market_lifecycle + - multivariate + - communications + - order_group_updates + - user_orders + minItems: 1 + market_ticker: + description: >- + Subscribe to a single market. Type: string. Example: + "KXBTCD-25AUG0517-T114999.99" (mutually exclusive with + market_tickers) + type: string + market_tickers: + type: array + description: >- + Subscribe to multiple markets. Type: array of strings. Example: + ["KXBTCD-25AUG0517-T114999.99", "KXETHD-25AUG0517-T3749.99"] + (mutually exclusive with market_ticker) + items: + $ref: '#/components/schemas/marketTicker' + minItems: 1 + market_id: + type: string + format: uuid + description: >- + Subscribe to a single market by UUID (ticker only; mutually + exclusive with market_ids and market_ticker(s)) + market_ids: + type: array + description: >- + Subscribe to multiple markets by UUID (ticker only; mutually + exclusive with market_id and market_ticker(s)) + items: + $ref: '#/components/schemas/marketId' + minItems: 1 + send_initial_snapshot: + type: boolean + description: >- + If true, receive an initial snapshot for requested market + tickers on the ticker channel + default: false + skip_ticker_ack: + type: boolean + description: >- + If true, OK responses omit the market_tickers/market_ids lists + for this subscription + default: false + use_yes_price: + type: boolean + description: > + Orderbook channel only. When true, no-side `orderbook_delta` and + `orderbook_snapshot` updates + + are reported in yes-leg pricing instead of no-leg pricing — so a + single `price_dollars` scale + + applies to both sides. Default false (no-side reported in no-leg + pricing, the existing + + long-standing behavior). See [Order + direction](/getting_started/order_direction). + + + **Migration plan.** The default will be flipped to `true` in a + future release, and the flag + + will then be removed entirely in a subsequent release — at which + point unified yes-leg + + pricing becomes the only supported behavior and `use_yes_price: + false` will no longer toggle + + the legacy no-leg pricing. Integrations relying on the legacy + behavior should migrate before + + the default flip; concrete dates will be announced before each + step. + default: false + shard_factor: + type: integer + description: Number of shards for communications channel fanout (optional) + minimum: 1 + shard_key: + type: integer + description: >- + Shard key for communications channel fanout (requires + shard_factor) + minimum: 0 + unsubscribeCommandPayload: + type: object + required: + - id + - cmd + - params + properties: + id: + $ref: '#/components/schemas/commandId' + cmd: + type: string + const: unsubscribe + params: + type: object + required: + - sids + properties: + sids: + type: array + description: List of subscription IDs to cancel + items: + $ref: '#/components/schemas/subscriptionId' + minItems: 1 + updateSubscriptionCommandPayload: + type: object + required: + - id + - cmd + - params + properties: + id: + $ref: '#/components/schemas/commandId' + cmd: + type: string + const: update_subscription + params: + type: object + required: + - action + properties: + sid: + $ref: '#/components/schemas/subscriptionId' + description: Single subscription ID to update (alternative to sids array) + sids: + type: array + description: >- + Array containing exactly one subscription ID (alternative to + sid). Either sid or sids must be provided, not both. + items: + $ref: '#/components/schemas/subscriptionId' + minItems: 1 + maxItems: 1 + market_ticker: + description: 'Add/remove a single market. Type: string' + type: string + market_tickers: + type: array + description: 'Add/remove multiple markets. Type: array of strings' + items: + $ref: '#/components/schemas/marketTicker' + market_id: + type: string + format: uuid + description: Add/remove a single market by UUID (ticker only) + market_ids: + type: array + description: Add/remove multiple markets by UUID (ticker only) + items: + $ref: '#/components/schemas/marketId' + send_initial_snapshot: + type: boolean + description: >- + If true, receive an initial snapshot for newly added market + tickers on the ticker channel + default: false + action: + type: string + enum: + - add_markets + - delete_markets + - get_snapshot + subscribedResponsePayload: + type: object + required: + - type + - msg + properties: + id: + $ref: '#/components/schemas/commandId' + type: + type: string + const: subscribed + msg: + type: object + required: + - channel + - sid + properties: + channel: + type: string + sid: + $ref: '#/components/schemas/subscriptionId' + unsubscribedResponsePayload: + type: object + required: + - sid + - seq + - type + properties: + id: + $ref: '#/components/schemas/commandId' + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + type: + type: string + const: unsubscribed + okResponsePayload: + type: object + required: + - type + properties: + id: + $ref: '#/components/schemas/commandId' + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + type: + type: string + const: ok + msg: + type: object + properties: + market_tickers: + type: array + description: Full list of market tickers after update + items: + $ref: '#/components/schemas/marketTicker' + market_ids: + type: array + description: Full list of market IDs after update + items: + $ref: '#/components/schemas/marketId' + errorResponsePayload: + type: object + required: + - type + - msg + properties: + id: + $ref: '#/components/schemas/commandId' + type: + type: string + const: error + msg: + type: object + required: + - code + - msg + properties: + code: + type: integer + description: > + Error code identifying the type of error: + + - 1: Unable to process message - General processing error + + - 2: Params required - Missing params object in command + + - 3: Channels required - Missing channels array in subscribe + + - 4: Subscription IDs required - Missing sids in unsubscribe + + - 5: Unknown command - Invalid command name + + - 6: Already subscribed - Duplicate subscription attempt + + - 7: Unknown subscription ID - Subscription ID not found + + - 8: Unknown channel name - Invalid channel in subscribe + + - 9: Authentication required - Channel requires authenticated + connection + + - 10: Channel error - Channel-specific error + + - 11: Invalid parameter - Malformed parameter value + + - 12: Exactly one subscription ID is required - For + update_subscription + + - 13: Unsupported action - Invalid action for + update_subscription + + - 14: Market Ticker required - Missing market specification + (market_ticker or market_id) + + - 15: Action required - Missing action in update_subscription + + - 16: Market not found - Invalid market_ticker or market_id + + - 17: Internal error - Server-side processing error + + - 18: Command timeout - Server timed out while processing + command + + - 19: shard_factor must be > 0 - Invalid shard_factor + + - 20: shard_factor is required when shard_key is set - Missing + shard_factor when shard_key is set + + - 21: shard_key must be >= 0 and < shard_factor - Invalid + shard_key + + - 22: shard_factor must be <= 100 - shard_factor too large + minimum: 1 + maximum: 22 + msg: + type: string + description: Human-readable error message + market_id: + type: string + description: Market UUID if error is market-specific (optional) + market_ticker: + type: string + description: Market ticker if error is market-specific (optional) + listSubscriptionsCommandPayload: + type: object + required: + - id + - cmd + properties: + id: + $ref: '#/components/schemas/commandId' + cmd: + type: string + const: list_subscriptions + listSubscriptionsResponsePayload: + type: object + required: + - id + - type + - msg + properties: + id: + $ref: '#/components/schemas/commandId' + type: + type: string + const: ok + msg: + type: array + description: List of active subscriptions + items: + type: object + required: + - channel + - sid + properties: + channel: + type: string + description: Name of the subscribed channel + sid: + $ref: '#/components/schemas/subscriptionId' + orderbookSnapshotPayload: + type: object + required: + - type + - sid + - seq + - msg + properties: + type: + type: string + const: orderbook_snapshot + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: + - market_ticker + - market_id + properties: + market_ticker: + $ref: '#/components/schemas/marketTicker' + market_id: + $ref: '#/components/schemas/marketId' + yes_dollars_fp: + type: array + description: > + Optional - This key will not exist if there are no Yes offers in + the orderbook. + + Price levels represented as [price_in_dollars, + contract_count_fp]. + + Format: [price_in_dollars, contract_count_fp] + items: + type: array + items: + type: string + minItems: 2 + maxItems: 2 + no_dollars_fp: + type: array + description: > + Optional - Same format as "yes_dollars_fp" but for the NO side + of the orderbook. + + This key will not exist if there are no No offers in the + orderbook. + + Format: [price_in_dollars, contract_count_fp] + items: + type: array + items: + type: string + minItems: 2 + maxItems: 2 + orderbookDeltaPayload: + type: object + required: + - type + - sid + - seq + - msg + properties: + type: + type: string + const: orderbook_delta + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: + - market_ticker + - market_id + - price_dollars + - delta_fp + - side + properties: + market_ticker: + $ref: '#/components/schemas/marketTicker' + market_id: + $ref: '#/components/schemas/marketId' + price_dollars: + type: string + description: Price level in dollars + delta_fp: + type: string + description: Fixed-point contract delta (2 decimals) + side: + $ref: '#/components/schemas/marketSide' + client_order_id: + type: string + description: > + Optional - Present only when you caused this orderbook change. + + Contains the client_order_id of your order that triggered this + delta. + subaccount: + type: integer + description: > + Optional - Present only when you caused this orderbook change + and are using subaccounts. + + Contains the subaccount number of your order that triggered this + delta. + ts: + type: string + deprecated: true + description: >- + Deprecated - Optional timestamp for when the orderbook change + was recorded (RFC3339). Use ts_ms instead. + format: date-time + ts_ms: + type: integer + description: >- + Optional - Unix timestamp for when the orderbook change was + recorded (in milliseconds) + format: int64 + tickerPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: ticker + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - market_ticker + - market_id + - price_dollars + - yes_bid_dollars + - yes_ask_dollars + - yes_bid_size_fp + - yes_ask_size_fp + - last_trade_size_fp + - volume_fp + - open_interest_fp + - dollar_volume + - dollar_open_interest + - ts + - ts_ms + - time + properties: + market_ticker: + $ref: '#/components/schemas/marketTicker' + market_id: + $ref: '#/components/schemas/marketId' + price_dollars: + type: string + description: Last traded price in dollars + yes_bid_dollars: + type: string + description: Best bid price for yes side in dollars + yes_ask_dollars: + type: string + description: Best ask price for yes side in dollars + volume_fp: + type: string + description: Fixed-point total contracts traded (2 decimals) + open_interest_fp: + type: string + description: Fixed-point open interest (2 decimals) + dollar_volume: + type: integer + description: Number of dollars traded in the market so far + minimum: 0 + dollar_open_interest: + type: integer + description: Number of dollars positioned in the market currently + minimum: 0 + yes_bid_size_fp: + type: string + description: Fixed-point contracts at best bid (2 decimals) + yes_ask_size_fp: + type: string + description: Fixed-point contracts at best ask (2 decimals) + last_trade_size_fp: + type: string + description: Fixed-point contracts in last trade (2 decimals) + ts: + type: integer + deprecated: true + description: >- + Deprecated - Unix timestamp for when the update happened (in + seconds). Use ts_ms instead. + format: int64 + ts_ms: + type: integer + description: Unix timestamp for when the update happened (in milliseconds) + format: int64 + time: + type: string + deprecated: true + description: >- + Deprecated - Timestamp for when the update happened (RFC3339). + Use ts_ms instead. + format: date-time + tradePayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: trade + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - trade_id + - market_ticker + - yes_price_dollars + - no_price_dollars + - count_fp + - taker_side + - taker_outcome_side + - taker_book_side + - ts + - ts_ms + properties: + trade_id: + type: string + description: Unique identifier for the trade + format: uuid + market_ticker: + $ref: '#/components/schemas/marketTicker' + yes_price_dollars: + type: string + description: Yes side price in dollars + no_price_dollars: + type: string + description: No side price in dollars + count_fp: + type: string + description: Fixed-point contracts traded (2 decimals) + taker_side: + $ref: '#/components/schemas/marketSide' + deprecated: true + description: > + Deprecated. Use `taker_outcome_side` (or `taker_book_side`) + instead. See [Order + direction](/getting_started/order_direction). This field will + not be removed before May 14, 2026. + taker_outcome_side: + $ref: '#/components/schemas/marketSide' + description: > + The outcome side the taker is positioned for. buy-yes and + sell-no produce 'yes'; buy-no and sell-yes produce 'no'. + + + `taker_outcome_side` describes directional exposure only; it + does not change the trade's price. A trade at price `p` with + `taker_outcome_side=no` is matched against the maker at the same + price `p` with the opposite direction — both parties trade at + the same price. + + + `taker_outcome_side` and `taker_book_side` will become the + canonical way to determine trade direction. The legacy + `taker_side` field will be deprecated in a future release — + please migrate to these new fields. + taker_book_side: + $ref: '#/components/schemas/bookSide' + description: > + Same directional bit as taker_outcome_side in book vocabulary. + 'bid' is equivalent to taker_outcome_side 'yes'; 'ask' is + equivalent to taker_outcome_side 'no'. + + + `taker_outcome_side` and `taker_book_side` will become the + canonical way to determine trade direction. The legacy + `taker_side` field will be deprecated in a future release — + please migrate to these new fields. + ts: + type: integer + deprecated: true + description: Deprecated - Unix timestamp in seconds. Use ts_ms instead. + format: int64 + ts_ms: + type: integer + description: Unix timestamp in milliseconds + format: int64 + fillPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: fill + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - trade_id + - order_id + - market_ticker + - is_taker + - side + - yes_price_dollars + - count_fp + - fee_cost + - action + - outcome_side + - book_side + - ts + - ts_ms + - post_position_fp + - purchased_side + properties: + trade_id: + type: string + description: >- + Unique identifier for fills. This is what you use to + differentiate fills + format: uuid + order_id: + type: string + description: >- + Unique identifier for orders. This is what you use to + differentiate fills for different orders + format: uuid + market_ticker: + $ref: '#/components/schemas/marketTicker' + description: >- + Unique identifier for markets. This is what you use to + differentiate fills for different markets + is_taker: + type: boolean + description: If you were a taker on this fill + side: + $ref: '#/components/schemas/marketSide' + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See + [Order direction](/getting_started/order_direction). This field + will not be removed before May 14, 2026. + yes_price_dollars: + type: string + description: Price for the yes side of the fill in dollars + count_fp: + type: string + description: Fixed-point contracts filled (2 decimals) + fee_cost: + type: string + description: Exchange fee paid for this fill in fixed-point dollars + action: + $ref: '#/components/schemas/orderAction' + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See + [Order direction](/getting_started/order_direction). This field + will not be removed before May 14, 2026. + ts: + type: integer + deprecated: true + description: >- + Deprecated - Unix timestamp for when the update happened (in + seconds). Use ts_ms instead. + format: int64 + ts_ms: + type: integer + description: Unix timestamp for when the update happened (in milliseconds) + format: int64 + client_order_id: + type: string + description: Optional client-provided order ID + post_position_fp: + type: string + description: Fixed-point position after the fill (2 decimals) + purchased_side: + $ref: '#/components/schemas/marketSide' + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See + [Order direction](/getting_started/order_direction). This field + will not be removed before May 14, 2026. + outcome_side: + $ref: '#/components/schemas/marketSide' + description: > + The outcome side this fill positioned the user for. buy-yes and + sell-no produce 'yes'; buy-no and sell-yes produce 'no'. + + + `outcome_side` describes directional exposure only; it does not + change the fill's price. A fill at price `p` with + `outcome_side=no` is matched against an order at the same price + `p` with `outcome_side=yes` — both parties trade at the same + price, just on opposite directions. + + + `outcome_side` and `book_side` will become the canonical way to + determine fill direction. The legacy `action` and `side` fields + will be deprecated in a future release — please migrate to these + new fields. + book_side: + $ref: '#/components/schemas/bookSide' + description: > + Same directional bit as outcome_side in book vocabulary. 'bid' + is equivalent to outcome_side 'yes'; 'ask' is equivalent to + outcome_side 'no'. + + + `outcome_side` and `book_side` will become the canonical way to + determine fill direction. The legacy `action` and `side` fields + will be deprecated in a future release — please migrate to these + new fields. + subaccount: + type: integer + description: Optional subaccount number for the fill + marketLifecycleV2Payload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: market_lifecycle_v2 + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - event_type + - market_ticker + properties: + event_type: + type: string + description: > + Field to annotate which of the event type this event is for: + + - `created` - Market created + + - `activated` - Market activated + + - `deactivated` - Market deactivated + + - `close_date_updated` - Market close date updated + + - `determined` - Market determined + + - `settled` - Market settled + + - `price_level_structure_updated` - Market price level structure + changed + + - `metadata_updated` - Market metadata updated (e.g. floor + strike, yes_sub_title) + enum: + - created + - deactivated + - activated + - close_date_updated + - determined + - settled + - price_level_structure_updated + - metadata_updated + market_ticker: + $ref: '#/components/schemas/marketTicker' + description: >- + Unique identifier for markets. This is what you use to + differentiate updates for different markets + open_ts: + type: integer + description: >- + Optional - This key will ONLY exist when the market is created. + Unix timestamp for when the market opened (in seconds) + format: int64 + close_ts: + type: integer + description: >- + Optional - This key will ONLY exist when the market is created + OR when the close date is updated. Unix timestamp for when the + market is scheduled to close (in seconds). Will be updated in + case of early determination markets + format: int64 + result: + type: string + description: >- + Optional - This key will ONLY exist when the market is + determined. Result of the market + determination_ts: + type: integer + description: >- + Optional - This key will ONLY exist when the market is + determined. Unix timestamp for when the market is determined (in + seconds) + format: int64 + settlement_value: + type: string + description: >- + Optional - This key will ONLY exist when the market is + determined. Settlement value of the market in fixed-point + dollars (e.g. "0.5000") + settled_ts: + type: integer + description: >- + Optional - This key will ONLY exist when the market is settled. + Unix timestamp for when the market is settled (in seconds) + format: int64 + is_deactivated: + type: boolean + description: >- + Optional - This key will ONLY exist when the market is + paused/unpaused. Boolean flag to indicate if trading is paused + on an open market. This should only be interpreted for an open + market + price_level_structure: + type: string + description: >- + Optional - This key will exist when the market is created or + when the price level structure is updated. The price level + structure of the market + enum: + - linear_cent + - deci_cent + - tapered_deci_cent + floor_strike: + type: number + description: >- + Optional - This key will ONLY exist for metadata_updated events. + The updated floor strike value for the market + yes_sub_title: + type: string + description: >- + Optional - This key will ONLY exist for metadata_updated events. + The updated yes subtitle for the market + additional_metadata: + type: object + description: Optional - This key will be emitted when the market is created + properties: + name: + type: string + title: + type: string + yes_sub_title: + type: string + no_sub_title: + type: string + rules_primary: + type: string + rules_secondary: + type: string + can_close_early: + type: boolean + event_ticker: + type: string + expected_expiration_ts: + type: integer + format: int64 + strike_type: + type: string + floor_strike: + type: number + cap_strike: + type: number + custom_strike: + type: object + multivariateMarketLifecyclePayload: + allOf: + - $ref: '#/components/schemas/marketLifecycleV2Payload' + - type: object + properties: + type: + type: string + const: multivariate_market_lifecycle + eventLifecyclePayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: event_lifecycle + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - event_ticker + - title + - subtitle + - collateral_return_type + - series_ticker + properties: + event_ticker: + type: string + description: Unique identifier for the event being created + title: + type: string + description: Title of event + subtitle: + type: string + description: Subtitle of event + collateral_return_type: + type: string + description: >- + Collateral return type, MECNET or DIRECNET of the event. Empty + if there is no collateral return scheme for the event + enum: + - MECNET + - DIRECNET + - '' + series_ticker: + type: string + description: Series ticker for the event + strike_date: + type: integer + description: >- + Optional - Unix timestamp to indicate the strike date of the + event if there is one + format: int64 + strike_period: + type: string + description: >- + Optional - String to indicate the strike period of the event if + there is one + multivariateLookupPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: multivariate_lookup + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - collection_ticker + - event_ticker + - market_ticker + - selected_markets + properties: + collection_ticker: + type: string + event_ticker: + type: string + market_ticker: + type: string + selected_markets: + type: array + items: + type: object + required: + - event_ticker + - market_ticker + - side + properties: + event_ticker: + type: string + market_ticker: + type: string + side: + $ref: '#/components/schemas/marketSide' + marketPositionPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: market_position + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - user_id + - market_ticker + - position_fp + - position_cost_dollars + - realized_pnl_dollars + - fees_paid_dollars + - position_fee_cost_dollars + - volume_fp + properties: + user_id: + type: string + description: User ID for the position + market_ticker: + $ref: '#/components/schemas/marketTicker' + description: Market ticker for the position + position_fp: + type: string + description: Fixed-point net position (2 decimals) + position_cost_dollars: + type: string + description: >- + Current cost basis of the position as a fixed-point dollar + string + realized_pnl_dollars: + type: string + description: Realized profit/loss as a fixed-point dollar string + fees_paid_dollars: + type: string + description: Total fees paid as a fixed-point dollar string + position_fee_cost_dollars: + type: string + description: Total position fee cost as a fixed-point dollar string + volume_fp: + type: string + description: Fixed-point total volume traded (2 decimals) + subaccount: + type: integer + description: Optional subaccount number for the position + orderGroupUpdatesPayload: + type: object + required: + - type + - sid + - seq + - msg + properties: + type: + type: string + const: order_group_updates + sid: + $ref: '#/components/schemas/subscriptionId' + seq: + $ref: '#/components/schemas/sequenceNumber' + msg: + type: object + required: + - event_type + - order_group_id + - ts_ms + properties: + event_type: + type: string + description: Order group event type + enum: + - created + - triggered + - reset + - deleted + - limit_updated + order_group_id: + type: string + description: Order group identifier + contracts_limit_fp: + type: string + description: >- + Updated contracts limit in fixed-point (2 decimals). Present for + "created" and "limit_updated" events only. + ts_ms: + type: integer + format: int64 + description: >- + Matching engine timestamp at which the event was processed, as + Unix epoch milliseconds. + userOrderPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: user_order + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - order_id + - user_id + - ticker + - status + - side + - is_yes + - outcome_side + - book_side + - yes_price_dollars + - fill_count_fp + - remaining_count_fp + - initial_count_fp + - taker_fill_cost_dollars + - maker_fill_cost_dollars + - taker_fees_dollars + - maker_fees_dollars + - client_order_id + - created_time + - created_ts_ms + properties: + order_id: + type: string + description: Unique order identifier + format: uuid + user_id: + type: string + description: User identifier + format: uuid + ticker: + $ref: '#/components/schemas/marketTicker' + description: Market ticker for the order + status: + type: string + description: Current order status + enum: + - resting + - canceled + - executed + side: + $ref: '#/components/schemas/marketSide' + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See + [Order direction](/getting_started/order_direction). This field + will not be removed before May 14, 2026. + is_yes: + type: boolean + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See + [Order direction](/getting_started/order_direction). This field + will not be removed before May 14, 2026. + outcome_side: + $ref: '#/components/schemas/marketSide' + description: > + The outcome side this order is positioned for. buy-yes and + sell-no produce 'yes'; buy-no and sell-yes produce 'no'. + + + `outcome_side` describes directional exposure only; it does not + change the order's price. An order at price `p` with + `outcome_side=no` is matched by an order at the same price `p` + with `outcome_side=yes` — both parties trade at the same price, + just on opposite directions. + + + `outcome_side` and `book_side` will become the canonical way to + determine order direction. The legacy `action`, `side`, and + `is_yes` fields will be deprecated in a future release — please + migrate to these new fields. + book_side: + $ref: '#/components/schemas/bookSide' + description: > + Same directional bit as outcome_side in book vocabulary. 'bid' + is equivalent to outcome_side 'yes'; 'ask' is equivalent to + outcome_side 'no'. + + + `outcome_side` and `book_side` will become the canonical way to + determine order direction. The legacy `action`, `side`, and + `is_yes` fields will be deprecated in a future release — please + migrate to these new fields. + yes_price_dollars: + type: string + description: Yes price in fixed-point dollars (4 decimals) + fill_count_fp: + type: string + description: Number of contracts filled in fixed-point (2 decimals) + remaining_count_fp: + type: string + description: Number of contracts remaining in fixed-point (2 decimals) + initial_count_fp: + type: string + description: Initial number of contracts in fixed-point (2 decimals) + taker_fill_cost_dollars: + type: string + description: Taker fill cost in fixed-point dollars (4 decimals) + maker_fill_cost_dollars: + type: string + description: Maker fill cost in fixed-point dollars (4 decimals) + taker_fees_dollars: + type: string + description: Taker fees in fixed-point dollars (4 decimals). + maker_fees_dollars: + type: string + description: Maker fees in fixed-point dollars (4 decimals). + client_order_id: + type: string + description: Client-provided order identifier + order_group_id: + type: string + description: Order group identifier, if applicable + self_trade_prevention_type: + type: string + description: Self-trade prevention type + enum: + - taker_at_cross + - maker + created_time: + type: string + deprecated: true + description: >- + Deprecated - Order creation time in RFC3339 format. Use + created_ts_ms instead. + format: date-time + created_ts_ms: + type: integer + description: Order creation time as a Unix timestamp in milliseconds + format: int64 + last_update_time: + type: string + deprecated: true + description: >- + Deprecated - Last update time in RFC3339 format. Use + last_updated_ts_ms instead. + format: date-time + last_updated_ts_ms: + type: integer + description: Last update time as a Unix timestamp in milliseconds + format: int64 + expiration_time: + type: string + deprecated: true + description: >- + Deprecated - Order expiration time in RFC3339 format. Use + expiration_ts_ms instead. + format: date-time + expiration_ts_ms: + type: integer + description: Order expiration time as a Unix timestamp in milliseconds + format: int64 + subaccount_number: + type: integer + description: Subaccount number (0 for primary, 1-32 for subaccounts) + rfqCreatedPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: rfq_created + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - id + - creator_id + - market_ticker + - created_ts + properties: + id: + type: string + description: Unique identifier for the RFQ + creator_id: + type: string + description: >- + Public communications ID of the RFQ creator (anonymized). + Currently empty for rfq_created events. + market_ticker: + type: string + description: Market ticker for the RFQ + event_ticker: + type: string + description: Event ticker (optional) + contracts_fp: + type: string + description: Fixed-point contracts requested (2 decimals) (optional) + target_cost_dollars: + type: string + description: Target cost in dollars (optional) + created_ts: + type: string + description: Timestamp when the RFQ was created + format: date-time + mve_collection_ticker: + type: string + description: Multivariate event collection ticker (optional) + mve_selected_legs: + type: array + description: Selected legs for multivariate events (optional) + items: + type: object + properties: + event_ticker: + type: string + market_ticker: + type: string + side: + type: string + yes_settlement_value_dollars: + type: string + description: >- + Yes settlement value in dollars for the selected leg + (optional) + rfqDeletedPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: rfq_deleted + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - id + - creator_id + - market_ticker + - deleted_ts + properties: + id: + type: string + description: Unique identifier for the RFQ + creator_id: + type: string + description: Public communications ID of the RFQ creator (anonymized) + market_ticker: + type: string + description: Market ticker for the RFQ + event_ticker: + type: string + description: Event ticker (optional) + contracts_fp: + type: string + description: Fixed-point contracts requested (2 decimals) (optional) + target_cost_dollars: + type: string + description: Target cost in dollars (optional) + deleted_ts: + type: string + description: Timestamp when the RFQ was deleted + format: date-time + quoteCreatedPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: quote_created + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - quote_id + - rfq_id + - quote_creator_id + - market_ticker + - yes_bid_dollars + - no_bid_dollars + - created_ts + properties: + quote_id: + type: string + description: Unique identifier for the quote + rfq_id: + type: string + description: Identifier of the RFQ this quote is for + quote_creator_id: + type: string + description: Public communications ID of the quote creator (anonymized) + market_ticker: + type: string + description: Market ticker for the quote + event_ticker: + type: string + description: Event ticker (optional) + yes_bid_dollars: + type: string + description: Yes side bid price in dollars + no_bid_dollars: + type: string + description: No side bid price in dollars + yes_contracts_offered_fp: + type: string + description: Fixed-point yes contracts offered (2 decimals) (optional) + no_contracts_offered_fp: + type: string + description: Fixed-point no contracts offered (2 decimals) (optional) + rfq_target_cost_dollars: + type: string + description: Target cost from the RFQ in dollars (optional) + created_ts: + type: string + description: Timestamp when the quote was created + format: date-time + quoteAcceptedPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: quote_accepted + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - quote_id + - rfq_id + - quote_creator_id + - market_ticker + - yes_bid_dollars + - no_bid_dollars + properties: + quote_id: + type: string + description: Unique identifier for the quote + rfq_id: + type: string + description: Identifier of the RFQ this quote is for + quote_creator_id: + type: string + description: Public communications ID of the quote creator (anonymized) + market_ticker: + type: string + description: Market ticker for the quote + event_ticker: + type: string + description: Event ticker (optional) + yes_bid_dollars: + type: string + description: Yes side bid price in dollars + no_bid_dollars: + type: string + description: No side bid price in dollars + accepted_side: + type: string + description: Which side was accepted (yes/no) (optional) + enum: + - 'yes' + - 'no' + contracts_accepted_fp: + type: string + description: Fixed-point contracts accepted (2 decimals) (optional) + yes_contracts_offered_fp: + type: string + description: Fixed-point yes contracts offered (2 decimals) (optional) + no_contracts_offered_fp: + type: string + description: Fixed-point no contracts offered (2 decimals) (optional) + rfq_target_cost_dollars: + type: string + description: Target cost from the RFQ in dollars (optional) + quoteExecutedPayload: + type: object + required: + - type + - sid + - msg + properties: + type: + type: string + const: quote_executed + sid: + $ref: '#/components/schemas/subscriptionId' + msg: + type: object + required: + - quote_id + - rfq_id + - quote_creator_id + - rfq_creator_id + - order_id + - client_order_id + - market_ticker + - executed_ts + properties: + quote_id: + type: string + description: Unique identifier for the quote that was executed + rfq_id: + type: string + description: Identifier of the RFQ this quote was for + quote_creator_id: + type: string + description: Anonymized identifier for the quote creator + rfq_creator_id: + type: string + description: Anonymized identifier for the RFQ creator + order_id: + type: string + description: >- + Your order ID resulting from the quote execution. Use this to + match with fill messages + client_order_id: + type: string + description: >- + Your client order ID for the executed order. Use this to + correlate with fill messages + market_ticker: + type: string + description: Market ticker for the executed quote + executed_ts: + type: string + description: Timestamp when the quote was executed and orders were placed + format: date-time + securitySchemes: + apiKey: + type: apiKey + in: user + description: | + API key authentication required for WebSocket connections. + The API key should be provided during the WebSocket handshake. +x-error-codes: + title: Error Codes + description: Complete reference of WebSocket API error codes + codes: + - code: 1 + name: Unable to process message + description: General processing error + - code: 2 + name: Params required + description: Missing params object in command + - code: 3 + name: Channels required + description: Missing channels array in subscribe + - code: 4 + name: Subscription IDs required + description: Missing sids in unsubscribe + - code: 5 + name: Unknown command + description: Invalid command name + - code: 6 + name: Already subscribed + description: Duplicate subscription attempt + - code: 7 + name: Unknown subscription ID + description: Subscription ID not found + - code: 8 + name: Unknown channel name + description: Invalid channel in subscribe + - code: 9 + name: Authentication required + description: Channel requires authenticated connection + - code: 10 + name: Channel error + description: Channel-specific error + - code: 11 + name: Invalid parameter + description: Malformed parameter value + - code: 12 + name: Exactly one subscription ID is required + description: For update_subscription command + - code: 13 + name: Unsupported action + description: Invalid action for update_subscription + - code: 14 + name: Market Ticker required + description: Missing market specification (market_ticker or market_id) + - code: 15 + name: Action required + description: Missing action in update_subscription + - code: 16 + name: Market not found + description: Invalid market_ticker or market_id + - code: 17 + name: Internal error + description: Server-side processing error + - code: 18 + name: Command timeout + description: Server timed out while processing command + - code: 19 + name: shard_factor must be > 0 + description: Invalid shard_factor + - code: 20 + name: shard_factor is required when shard_key is set + description: Missing shard_factor when shard_key is set + - code: 21 + name: shard_key must be >= 0 and < shard_factor + description: Invalid shard_key + - code: 22 + name: shard_factor must be <= 100 + description: shard_factor too large diff --git a/specs/openapi.yaml b/specs/openapi.yaml index abb8e70..12cbf60 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -1,238 +1,20 @@ openapi: 3.0.0 info: title: Kalshi Trade API Manual Endpoints - version: 3.13.0 + version: 3.18.0 description: >- Manually defined OpenAPI spec for endpoints being migrated to spec-first approach servers: + - url: https://external-api.kalshi.com/trade-api/v2 + description: Production Trade API server - url: https://api.elections.kalshi.com/trade-api/v2 - description: Production server + description: Production shared API server, also supported + - url: https://external-api.demo.kalshi.co/trade-api/v2 + description: Demo Trade API server + - url: https://demo-api.kalshi.co/trade-api/v2 + description: Demo shared API server, also supported paths: - /historical/cutoff: - get: - operationId: GetHistoricalCutoff - summary: Get Historical Cutoff Timestamps - description: > - Returns the cutoff timestamps that define the boundary between **live** - and **historical** data. - - - ## Cutoff fields - - - `market_settled_ts` : Markets that **settled** before this timestamp, - and their candlesticks, must be accessed via `GET /historical/markets` - and `GET /historical/markets/{ticker}/candlesticks`. - - - `trades_created_ts` : Trades that were **filled** before this - timestamp must be accessed via `GET /historical/fills`. - - - `orders_updated_ts` : Orders that were **canceled or fully executed** - before this timestamp must be accessed via `GET /historical/orders`. - Resting (active) orders are always available in `GET /portfolio/orders`. - tags: - - historical - responses: - '200': - description: Historical cutoff timestamps retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetHistoricalCutoffResponse' - '500': - description: Internal server error - /historical/markets/{ticker}/candlesticks: - get: - operationId: GetMarketCandlesticksHistorical - summary: Get Historical Market Candlesticks - description: ' Endpoint for fetching historical candlestick data for markets that have been archived from the live data set. Time period length of each candlestick in minutes. Valid values: 1 (1 minute), 60 (1 hour), 1440 (1 day).' - tags: - - historical - parameters: - - name: ticker - in: path - required: true - description: Market ticker - unique identifier for the specific market - schema: - type: string - - name: start_ts - in: query - required: true - description: >- - Start timestamp (Unix timestamp). Candlesticks will include those - ending on or after this time. - schema: - type: integer - format: int64 - - name: end_ts - in: query - required: true - description: >- - End timestamp (Unix timestamp). Candlesticks will include those - ending on or before this time. - schema: - type: integer - format: int64 - - name: period_interval - in: query - required: true - description: >- - Time period length of each candlestick in minutes. Valid values are - 1 (1 minute), 60 (1 hour), or 1440 (1 day). - schema: - type: integer - enum: - - 1 - - 60 - - 1440 - x-oapi-codegen-extra-tags: - validate: required,oneof=1 60 1440 - responses: - '200': - description: Candlesticks retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetMarketCandlesticksHistoricalResponse' - '400': - description: Bad request - '404': - description: Not found - '500': - description: Internal server error - /historical/fills: - get: - operationId: GetFillsHistorical - summary: Get Historical Fills - description: ' Endpoint for getting all historical fills for the member. A fill is when a trade you have is matched.' - tags: - - historical - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/MaxTsQuery' - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/CursorQuery' - responses: - '200': - description: Fills retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetFillsResponse' - '400': - description: Bad request - '401': - description: Unauthorized - '404': - $ref: '#/components/responses/NotFoundError' - '500': - description: Internal server error - /historical/orders: - get: - operationId: GetHistoricalOrders - summary: Get Historical Orders - description: ' Endpoint for getting orders that have been archived to the historical database.' - tags: - - historical - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/MaxTsQuery' - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/CursorQuery' - responses: - '200': - description: Historical orders retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetOrdersResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /historical/trades: - get: - operationId: GetTradesHistorical - summary: Get Historical Trades - description: ' Endpoint for getting all historical trades for all markets. Trades that were filled before the historical cutoff are available via this endpoint. See [Historical Data](https://kalshi.com/docs/getting_started/historical_data) for details.' - tags: - - historical - parameters: - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/MinTsQuery' - - $ref: '#/components/parameters/MaxTsQuery' - - $ref: '#/components/parameters/MarketLimitQuery' - - $ref: '#/components/parameters/CursorQuery' - responses: - '200': - description: Historical trades retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetTradesResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' - /historical/markets: - get: - operationId: GetHistoricalMarkets - summary: Get Historical Markets - description: > - Endpoint for getting markets that have been archived to the historical - database. Filters are mutually exclusive. - tags: - - historical - parameters: - - $ref: '#/components/parameters/MarketLimitQuery' - - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/TickersQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/SeriesTickerQuery' - - $ref: '#/components/parameters/MveHistoricalFilterQuery' - responses: - '200': - description: Historical markets retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetMarketsResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '500': - $ref: '#/components/responses/InternalServerError' - /historical/markets/{ticker}: - get: - operationId: GetHistoricalMarket - summary: Get Historical Market - description: ' Endpoint for getting data about a specific market by its ticker from the historical database.' - tags: - - historical - parameters: - - $ref: '#/components/parameters/TickerPath' - responses: - '200': - description: Historical market retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetMarketResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' /exchange/status: get: operationId: GetExchangeStatus @@ -344,319 +126,1440 @@ paths: $ref: '#/components/schemas/GetUserDataTimestampResponse' '500': description: Internal server error - /portfolio/orders: + /series/{series_ticker}/markets/{ticker}/candlesticks: get: - operationId: GetOrders - summary: Get Orders + operationId: GetMarketCandlesticks + summary: Get Market Candlesticks description: > - Restricts the response to orders that have a certain status: resting, - canceled, or executed. + Time period length of each candlestick in minutes. Valid values: 1 (1 + minute), 60 (1 hour), 1440 (1 day). - Orders that have been canceled or fully executed before the historical - cutoff are only available via `GET /historical/orders`. Resting orders - will always be available through this endpoint. See [Historical - Data](https://kalshi.com/docs/getting_started/historical_data) for + Candlesticks for markets that settled before the historical cutoff are + only available via `GET /historical/markets/{ticker}/candlesticks`. See + [Historical + Data](https://docs.kalshi.com/getting_started/historical_data) for details. tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - market parameters: - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/MultipleEventTickerQuery' - - $ref: '#/components/parameters/MinTsQuery' - - $ref: '#/components/parameters/MaxTsQuery' - - $ref: '#/components/parameters/StatusQuery' - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/SubaccountQuery' - responses: - '200': - description: Orders retrieved successfully + - name: series_ticker + in: path + required: true + description: Series ticker - the series that contains the target market + schema: + type: string + - name: ticker + in: path + required: true + description: Market ticker - unique identifier for the specific market + schema: + type: string + - name: start_ts + in: query + required: true + description: >- + Start timestamp (Unix timestamp). Candlesticks will include those + ending on or after this time. + schema: + type: integer + format: int64 + - name: end_ts + in: query + required: true + description: >- + End timestamp (Unix timestamp). Candlesticks will include those + ending on or before this time. + schema: + type: integer + format: int64 + - name: period_interval + in: query + required: true + description: >- + Time period length of each candlestick in minutes. Valid values are + 1 (1 minute), 60 (1 hour), or 1440 (1 day). + schema: + type: integer + enum: + - 1 + - 60 + - 1440 + x-oapi-codegen-extra-tags: + validate: required,oneof=1 60 1440 + - name: include_latest_before_start + in: query + required: false + description: > + If true, prepends the latest candlestick available before the + start_ts. This synthetic candlestick is created by: + + 1. Finding the most recent real candlestick before start_ts + + 2. Projecting it forward to the first period boundary (calculated as + the next period interval after start_ts) + + 3. Setting all OHLC prices to null, and `previous_price` to the + close price from the real candlestick + schema: + type: boolean + default: false + responses: + '200': + description: Candlesticks retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetOrdersResponse' + $ref: '#/components/schemas/GetMarketCandlesticksResponse' '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' + description: Bad request + '404': + description: Not found '500': - $ref: '#/components/responses/InternalServerError' - post: - operationId: CreateOrder - summary: Create Order - description: ' Endpoint for submitting orders in a market. Each user is limited to 200 000 open orders at a time.' + description: Internal server error + /markets/trades: + get: + operationId: GetTrades + summary: Get Trades + description: > + Endpoint for getting all trades for all markets. A trade represents a + completed transaction between two users on a specific market. Each trade + includes the market ticker, price, quantity, and timestamp information. + This endpoint returns a paginated response. Use the 'limit' parameter to + control page size (1-1000, defaults to 100). The response includes a + 'cursor' field - pass this value in the 'cursor' parameter of your next + request to get the next page. An empty cursor indicates no more pages + are available. tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateOrderRequest' + - market + parameters: + - $ref: '#/components/parameters/MarketLimitQuery' + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/MinTsQuery' + - $ref: '#/components/parameters/MaxTsQuery' responses: - '201': - description: Order created successfully + '200': + description: Trades retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/CreateOrderResponse' + $ref: '#/components/schemas/GetTradesResponse' '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '409': - $ref: '#/components/responses/ConflictError' - '429': - $ref: '#/components/responses/RateLimitError' + description: Bad request '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/orders/{order_id}: + description: Internal server error + /markets/{ticker}/orderbook: get: - operationId: GetOrder - summary: Get Order - description: ' Endpoint for getting a single order.' + operationId: GetMarketOrderbook + summary: Get Market Orderbook + description: ' Endpoint for getting the current order book for a specific market. The order book shows all active bid orders for both yes and no sides of a binary market. It returns yes bids and no bids only (no asks are returned). This is because in binary markets, a bid for yes at price X is equivalent to an ask for no at price (100-X). For example, a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract sizes. Each side shows price levels with their corresponding quantities and order counts, organized from best to worst prices.' tags: - - orders + - market security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/OrderIdPath' + - $ref: '#/components/parameters/TickerPath' + - name: depth + in: query + description: >- + Depth of the orderbook to retrieve (0 or negative means all levels, + 1-100 for specific depth) + required: false + schema: + type: integer + minimum: 0 + maximum: 100 + default: 0 + x-oapi-codegen-extra-tags: + validate: omitempty,min=0,max=100 responses: '200': - description: Order retrieved successfully + description: Orderbook retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetOrderResponse' + $ref: '#/components/schemas/GetMarketOrderbookResponse' '401': $ref: '#/components/responses/UnauthorizedError' '404': $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - delete: - operationId: CancelOrder - summary: Cancel Order - description: ' Endpoint for canceling orders. The value for the orderId should match the id field of the order you want to decrease. Commonly, DELETE-type endpoints return 204 status with no body content on success. But we can''t completely delete the order, as it may be partially filled already. Instead, the DeleteOrder endpoint reduce the order completely, essentially zeroing the remaining resting contracts on it. The zeroed order is returned on the response payload as a form of validation for the client.' + /markets/orderbooks: + get: + operationId: GetMarketOrderbooks + summary: Get Multiple Market Orderbooks + description: >- + Endpoint for getting the current order books for multiple markets in a + single request. The order book shows all active bid orders for both yes + and no sides of a binary market. It returns yes bids and no bids only + (no asks are returned). This is because in binary markets, a bid for yes + at price X is equivalent to an ask for no at price (100-X). For example, + a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract + sizes. Each side shows price levels with their corresponding quantities + and order counts, organized from best to worst prices. Returns one + orderbook per requested market ticker. tags: - - orders + - market security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/OrderIdPath' - - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + - name: tickers + in: query + required: true + description: List of market tickers to fetch orderbooks for + schema: + type: array + items: + type: string + maxLength: 200 + minItems: 1 + maxItems: 100 + style: form + explode: true + x-oapi-codegen-extra-tags: + validate: required,min=1,max=100,dive,max=200 responses: '200': - description: Order cancelled successfully + description: Orderbooks retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/CancelOrderResponse' + $ref: '#/components/schemas/GetMarketOrderbooksResponse' + '400': + $ref: '#/components/responses/BadRequestError' '401': $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/orders/batched: - post: - operationId: BatchCreateOrders - summary: Batch Create Orders - description: ' Endpoint for submitting a batch of orders. Each order in the batch is counted against the total rate limit for order operations. Consequently, the size of the batch is capped by the current per-second rate-limit configuration applicable to the user. At the moment of writing, the limit is 20 orders per batch.' + /series/{series_ticker}: + get: + operationId: GetSeries + summary: Get Series + description: ' Endpoint for getting data about a specific series by its ticker. A series represents a template for recurring events that follow the same format and rules (e.g., "Monthly Jobs Report", "Weekly Initial Jobless Claims", "Daily Weather in NYC"). Series define the structure, settlement sources, and metadata that will be applied to each recurring event instance within that series.' tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCreateOrdersRequest' + - market + parameters: + - name: series_ticker + in: path + required: true + schema: + type: string + description: The ticker of the series to retrieve + - name: include_volume + in: query + required: false + schema: + type: boolean + default: false + x-go-type-skip-optional-pointer: true + description: >- + If true, includes the total volume traded across all events in this + series. responses: - '201': - description: Batch order creation completed + '200': + description: Series retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/BatchCreateOrdersResponse' + $ref: '#/components/schemas/GetSeriesResponse' '400': $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' - delete: - operationId: BatchCancelOrders - summary: Batch Cancel Orders - description: ' Endpoint for cancelling up to 20 orders at once.' + /series: + get: + operationId: GetSeriesList + summary: Get Series List + description: ' Endpoint for getting data about multiple series with specified filters. A series represents a template for recurring events that follow the same format and rules (e.g., "Monthly Jobs Report", "Weekly Initial Jobless Claims", "Daily Weather in NYC"). This endpoint allows you to browse and discover available series templates by category.' tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCancelOrdersRequest' + - market + parameters: + - name: category + in: query + required: false + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: tags + in: query + required: false + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: include_product_metadata + in: query + required: false + schema: + type: boolean + default: false + x-go-type-skip-optional-pointer: true + - name: include_volume + in: query + required: false + schema: + type: boolean + default: false + x-go-type-skip-optional-pointer: true + description: >- + If true, includes the total volume traded across all events in each + series. + - name: min_updated_ts + in: query + required: false + description: >- + Filter series with metadata updated after this Unix timestamp (in + seconds). Use this to efficiently poll for changes. + schema: + type: integer + format: int64 responses: '200': - description: Batch order cancellation completed + description: Series list retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/BatchCancelOrdersResponse' + $ref: '#/components/schemas/GetSeriesListResponse' '400': $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/orders/{order_id}/amend: - post: - operationId: AmendOrder - summary: Amend Order - description: ' Endpoint for amending the max number of fillable contracts and/or price in an existing order. Max fillable contracts is `remaining_count` + `fill_count`.' + /markets: + get: + operationId: GetMarkets + summary: Get Markets + description: > + Filter by market status. Possible values: `unopened`, `open`, `closed`, + `settled`. Leave empty to return markets with any status. + - Only one `status` filter may be supplied at a time. + - Timestamp filters will be mutually exclusive from other timestamp filters and certain status filters. + + | Compatible Timestamp Filters | Additional Status Filters| Extra Notes | + |------------------------------|--------------------------|-------------| + | min_created_ts, max_created_ts | `unopened`, `open`, *empty* | | + | min_close_ts, max_close_ts | `closed`, *empty* | | + | min_settled_ts, max_settled_ts | `settled`, *empty* | | + | min_updated_ts | *empty* | Incompatible with all filters besides `mve_filter=exclude` | + + Markets that settled before the historical cutoff are only available via `GET /historical/markets`. See [Historical Data](https://docs.kalshi.com/getting_started/historical_data) for details. tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - market parameters: - - $ref: '#/components/parameters/OrderIdPath' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AmendOrderRequest' + - $ref: '#/components/parameters/MarketLimitQuery' + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/SingleEventTickerQuery' + - $ref: '#/components/parameters/SeriesTickerQuery' + - $ref: '#/components/parameters/MinCreatedTsQuery' + - $ref: '#/components/parameters/MaxCreatedTsQuery' + - $ref: '#/components/parameters/MinUpdatedTsQuery' + - $ref: '#/components/parameters/MaxCloseTsQuery' + - $ref: '#/components/parameters/MinCloseTsQuery' + - $ref: '#/components/parameters/MinSettledTsQuery' + - $ref: '#/components/parameters/MaxSettledTsQuery' + - $ref: '#/components/parameters/MarketStatusQuery' + - $ref: '#/components/parameters/TickersQuery' + - $ref: '#/components/parameters/MveFilterQuery' responses: '200': - description: Order amended successfully + description: Markets retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/AmendOrderResponse' + $ref: '#/components/schemas/GetMarketsResponse' '400': - $ref: '#/components/responses/BadRequestError' + description: Bad request '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' + description: Unauthorized '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/orders/{order_id}/decrease: - post: - operationId: DecreaseOrder - summary: Decrease Order - description: ' Endpoint for decreasing the number of contracts in an existing order. This is the only kind of edit available on order quantity. Cancelling an order is equivalent to decreasing an order amount to zero.' + description: Internal server error + /markets/{ticker}: + get: + operationId: GetMarket + summary: Get Market + description: ' Endpoint for getting data about a specific market by its ticker. A market represents a specific binary outcome within an event that users can trade on (e.g., "Will candidate X win?"). Markets have yes/no positions, current prices, volume, and settlement rules.' tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - market parameters: - - $ref: '#/components/parameters/OrderIdPath' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DecreaseOrderRequest' + - $ref: '#/components/parameters/TickerPath' responses: '200': - description: Order decreased successfully + description: Market retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/DecreaseOrderResponse' - '400': - $ref: '#/components/responses/BadRequestError' + $ref: '#/components/schemas/GetMarketResponse' '401': - $ref: '#/components/responses/UnauthorizedError' + description: Unauthorized '404': - $ref: '#/components/responses/NotFoundError' + description: Not found '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/orders/queue_positions: + description: Internal server error + /markets/candlesticks: get: - operationId: GetOrderQueuePositions - summary: Get Queue Positions for Orders - description: ' Endpoint for getting queue positions for all resting orders. Queue position represents the number of contracts that need to be matched before an order receives a partial or full match, determined using price-time priority.' + operationId: BatchGetMarketCandlesticks + summary: Batch Get Market Candlesticks + description: > + Endpoint for retrieving candlestick data for multiple markets. + + + - Accepts up to 100 market tickers per request + + - Returns up to 10,000 candlesticks total across all markets + + - Returns candlesticks grouped by market_id + + - Optionally includes a synthetic initial candlestick for price + continuity (see `include_latest_before_start` parameter) tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - market parameters: - name: market_tickers in: query - description: Comma-separated list of market tickers to filter by + required: true + description: Comma-separated list of market tickers (maximum 100) schema: type: string - - name: event_ticker + - name: start_ts in: query - description: Event ticker to filter by + required: true + description: Start timestamp in Unix seconds schema: - type: string - - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + type: integer + format: int64 + - name: end_ts + in: query + required: true + description: End timestamp in Unix seconds + schema: + type: integer + format: int64 + - name: period_interval + in: query + required: true + description: Candlestick period interval in minutes + schema: + type: integer + format: int32 + minimum: 1 + - name: include_latest_before_start + in: query + required: false + description: > + If true, prepends the latest candlestick available before the + start_ts. This synthetic candlestick is created by: + + 1. Finding the most recent real candlestick before start_ts + + 2. Projecting it forward to the first period boundary (calculated as + the next period interval after start_ts) + + 3. Setting all OHLC prices to null, and `previous_price` to the + close price from the real candlestick + schema: + type: boolean + default: false responses: '200': - description: Queue positions retrieved successfully + description: Market candlesticks retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetOrderQueuePositionsResponse' + $ref: '#/components/schemas/BatchGetMarketCandlesticksResponse' '400': - $ref: '#/components/responses/BadRequestError' + description: Bad request '401': - $ref: '#/components/responses/UnauthorizedError' + description: Unauthorized '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/orders/{order_id}/queue_position: + description: Internal server error + /series/{series_ticker}/events/{ticker}/candlesticks: get: - operationId: GetOrderQueuePosition - summary: Get Order Queue Position - description: ' Endpoint for getting an order''s queue position in the order book. This represents the amount of orders that need to be matched before this order receives a partial or full match. Queue position is determined using a price-time priority.' + operationId: GetMarketCandlesticksByEvent + summary: Get Event Candlesticks + description: ' End-point for returning aggregated data across all markets corresponding to an event.' tags: - - orders - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - events parameters: - - $ref: '#/components/parameters/OrderIdPath' - responses: - '200': - description: Queue position retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetOrderQueuePositionResponse' + - name: ticker + in: path + required: true + description: The event ticker + schema: + type: string + - name: series_ticker + in: path + required: true + description: The series ticker + schema: + type: string + - name: start_ts + in: query + required: true + description: Start timestamp for the range + schema: + type: integer + format: int64 + x-oapi-codegen-extra-tags: + validate: required + - name: end_ts + in: query + required: true + description: End timestamp for the range + schema: + type: integer + format: int64 + x-oapi-codegen-extra-tags: + validate: required + - name: period_interval + in: query + required: true + description: >- + Specifies the length of each candlestick period, in minutes. Must be + one minute, one hour, or one day. + schema: + type: integer + format: int32 + enum: + - 1 + - 60 + - 1440 + x-oapi-codegen-extra-tags: + validate: required,oneof=1 60 1440 + responses: + '200': + description: Event candlesticks retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetEventCandlesticksResponse' + '400': + description: Bad request + '401': + description: Unauthorized + '500': + description: Internal server error + /events: + get: + operationId: GetEvents + summary: Get Events + description: > + Get all events. This endpoint excludes multivariate events. + + To retrieve multivariate events, use the GET /events/multivariate + endpoint. + + All events are accessible through this endpoint, even if their + associated markets are older than the historical cutoff. + tags: + - events + parameters: + - name: limit + in: query + required: false + description: >- + Parameter to specify the number of results per page. Defaults to + 200. Maximum value is 200. + schema: + type: integer + minimum: 1 + maximum: 200 + default: 200 + - name: cursor + in: query + required: false + description: >- + Parameter to specify the pagination cursor. Use the cursor value + returned from the previous response to get the next page of results. + Leave empty for the first page. + schema: + type: string + - name: with_nested_markets + in: query + required: false + description: >- + Parameter to specify if nested markets should be included in the + response. When true, each event will include a 'markets' field + containing a list of Market objects associated with that event. + Historical markets settled before the historical cutoff will not be + included. + schema: + type: boolean + default: false + x-go-type-skip-optional-pointer: true + - name: with_milestones + in: query + required: false + description: If true, includes related milestones as a field alongside events. + schema: + type: boolean + default: false + x-go-type-skip-optional-pointer: true + - name: status + in: query + required: false + description: >- + Filter by event status. Possible values are 'unopened', 'open', + 'closed', 'settled'. Leave empty to return events with any status. + schema: + type: string + enum: + - unopened + - open + - closed + - settled + - $ref: '#/components/parameters/SeriesTickerQuery' + - name: min_close_ts + in: query + required: false + description: >- + Filter events with at least one market with close timestamp greater + than this Unix timestamp (in seconds). + schema: + type: integer + format: int64 + - name: min_updated_ts + in: query + required: false + description: >- + Filter events with metadata updated after this Unix timestamp (in + seconds). Use this to efficiently poll for changes. + schema: + type: integer + format: int64 + responses: + '200': + description: Events retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetEventsResponse' + '400': + description: Bad request + '401': + description: Unauthorized + '500': + description: Internal server error + /events/multivariate: + get: + operationId: GetMultivariateEvents + summary: Get Multivariate Events + description: >- + Retrieve multivariate (combo) events. These are dynamically created + events from multivariate event collections. Supports filtering by series + and collection ticker. + tags: + - events + parameters: + - name: limit + in: query + required: false + description: Number of results per page. Defaults to 100. Maximum value is 200. + schema: + type: integer + minimum: 1 + maximum: 200 + default: 100 + - name: cursor + in: query + required: false + description: >- + Pagination cursor. Use the cursor value returned from the previous + response to get the next page of results. + schema: + type: string + - $ref: '#/components/parameters/SeriesTickerQuery' + - name: collection_ticker + in: query + required: false + description: >- + Filter events by collection ticker. Returns only multivariate events + belonging to the specified collection. Cannot be used together with + series_ticker. + schema: + type: string + - name: with_nested_markets + in: query + required: false + description: >- + Parameter to specify if nested markets should be included in the + response. When true, each event will include a 'markets' field + containing a list of Market objects associated with that event. + schema: + type: boolean + default: false + responses: + '200': + description: Multivariate events retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetMultivariateEventsResponse' + '400': + description: Bad request - invalid parameters + '401': + description: Unauthorized + '500': + description: Internal server error + /events/{event_ticker}: + get: + operationId: GetEvent + summary: Get Event + description: > + Endpoint for getting data about an event by its ticker. An event + represents a real-world occurrence that can be traded on, such as an + election, sports game, or economic indicator release. + + Events contain one or more markets where users can place trades on + different outcomes. + + All events are accessible through this endpoint, even if their + associated markets are older than the historical cutoff. + tags: + - events + parameters: + - name: event_ticker + in: path + required: true + description: Event ticker + schema: + type: string + - name: with_nested_markets + in: query + required: false + description: >- + If true, markets are included within the event object. If false + (default), markets are returned as a separate top-level field in the + response. Historical markets settled before the historical cutoff + will not be included. + schema: + type: boolean + default: false + x-go-type-skip-optional-pointer: true + responses: + '200': + description: Event retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetEventResponse' + '400': + description: Bad request + '401': + description: Unauthorized + '404': + description: Event not found + '500': + description: Internal server error + /events/{event_ticker}/metadata: + get: + operationId: GetEventMetadata + summary: Get Event Metadata + description: ' Endpoint for getting metadata about an event by its ticker. Returns only the metadata information for an event.' + tags: + - events + parameters: + - name: event_ticker + in: path + required: true + description: Event ticker + schema: + type: string + responses: + '200': + description: Event metadata retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetEventMetadataResponse' + '400': + description: Bad request + '401': + description: Unauthorized + '404': + description: Event not found + '500': + description: Internal server error + /series/{series_ticker}/events/{ticker}/forecast_percentile_history: + get: + operationId: GetEventForecastPercentilesHistory + summary: Get Event Forecast Percentile History + description: >- + Endpoint for getting the historical raw and formatted forecast numbers + for an event at specific percentiles. + tags: + - events + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - name: ticker + in: path + required: true + description: The event ticker + schema: + type: string + - name: series_ticker + in: path + required: true + description: The series ticker + schema: + type: string + - name: percentiles + in: query + required: true + description: Array of percentile values to retrieve (0-9999, max 10 values) + schema: + type: array + items: + type: integer + format: int32 + minimum: 0 + maximum: 9999 + maxItems: 10 + style: form + explode: true + - name: start_ts + in: query + required: true + description: Start timestamp for the range + schema: + type: integer + format: int64 + - name: end_ts + in: query + required: true + description: End timestamp for the range + schema: + type: integer + format: int64 + - name: period_interval + in: query + required: true + description: >- + Specifies the length of each forecast period, in minutes. 0 for + 5-second intervals, or 1, 60, or 1440 for minute-based intervals. + schema: + type: integer + format: int32 + enum: + - 0 + - 1 + - 60 + - 1440 + responses: + '200': + description: Event forecast percentile history retrieved successfully + content: + application/json: + schema: + $ref: >- + #/components/schemas/GetEventForecastPercentilesHistoryResponse + '400': + description: Bad request + '401': + description: Unauthorized + '500': + description: Internal server error + /portfolio/orders: + get: + operationId: GetOrders + summary: Get Orders + description: > + Restricts the response to orders that have a certain status: resting, + canceled, or executed. + + Orders that have been canceled or fully executed before the historical + cutoff are only available via `GET /historical/orders`. Resting orders + will always be available through this endpoint. See [Historical + Data](https://docs.kalshi.com/getting_started/historical_data) for + details. + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/MultipleEventTickerQuery' + - $ref: '#/components/parameters/MinTsQuery' + - $ref: '#/components/parameters/MaxTsQuery' + - $ref: '#/components/parameters/StatusQuery' + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/SubaccountQuery' + responses: + '200': + description: Orders retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetOrdersResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + post: + operationId: CreateOrder + summary: Create Order + description: ' Endpoint for submitting orders in a market. Each user is limited to 200 000 open orders at a time.' + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOrderRequest' + responses: + '201': + description: Order created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOrderResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '409': + $ref: '#/components/responses/ConflictError' + '429': + $ref: '#/components/responses/RateLimitError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/orders/{order_id}: + get: + operationId: GetOrder + summary: Get Order + description: ' Endpoint for getting a single order.' + x-mint: + content: > + + + **Rate limit:** 2 tokens per request. Other endpoints use the default + cost of 10 tokens per request unless noted on their own page. See + [Rate Limits and Tiers](/getting_started/rate_limits). + + + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + responses: + '200': + description: Order retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetOrderResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + delete: + operationId: CancelOrder + summary: Cancel Order + description: ' Endpoint for canceling orders. The value for the orderId should match the id field of the order you want to decrease. Commonly, DELETE-type endpoints return 204 status with no body content on success. But we can''t completely delete the order, as it may be partially filled already. Instead, the DeleteOrder endpoint reduce the order completely, essentially zeroing the remaining resting contracts on it. The zeroed order is returned on the response payload as a form of validation for the client.' + x-mint: + content: > + + + **Rate limit:** 2 tokens per request. Other endpoints use the default + cost of 10 tokens per request unless noted on their own page. See + [Rate Limits and Tiers](/getting_started/rate_limits). + + + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + - $ref: '#/components/parameters/ExchangeIndexQuery' + responses: + '200': + description: Order cancelled successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CancelOrderResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/orders/batched: + post: + operationId: BatchCreateOrders + summary: Batch Create Orders + description: >- + Endpoint for submitting a batch of orders. The maximum batch size scales + with your tier's write budget — see [Rate Limits and + Tiers](/getting_started/rate_limits). + x-mint: + content: > + + + **Rate limit:** 10 tokens per order in the batch — billed per item, so + total cost for a batch of N orders is N × 10. Other endpoints cost 10 + tokens per request (not per item) unless noted on their own page. See + [Rate Limits and Tiers](/getting_started/rate_limits). + + + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCreateOrdersRequest' + responses: + '201': + description: Batch order creation completed + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCreateOrdersResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/InternalServerError' + delete: + operationId: BatchCancelOrders + summary: Batch Cancel Orders + description: >- + Endpoint for cancelling a batch of orders. The maximum batch size scales + with your tier's write budget — see [Rate Limits and + Tiers](/getting_started/rate_limits). + x-mint: + content: > + + + **Rate limit:** 2 tokens per order in the batch — billed per item, so + total cost for a batch of N cancels is N × 2. Other endpoints cost 10 + tokens per request unless noted on their own page. See [Rate Limits + and Tiers](/getting_started/rate_limits). + + + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCancelOrdersRequest' + responses: + '200': + description: Batch order cancellation completed + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCancelOrdersResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/orders/{order_id}/amend: + post: + operationId: AmendOrder + summary: Amend Order + description: ' Endpoint for amending the max number of fillable contracts and/or price in an existing order. Max fillable contracts is `remaining_count` + `fill_count`.' + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AmendOrderRequest' + responses: + '200': + description: Order amended successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AmendOrderResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/orders/{order_id}/decrease: + post: + operationId: DecreaseOrder + summary: Decrease Order + description: ' Endpoint for decreasing the number of contracts in an existing order. This is the only kind of edit available on order quantity. Cancelling an order is equivalent to decreasing an order amount to zero.' + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DecreaseOrderRequest' + responses: + '200': + description: Order decreased successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DecreaseOrderResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/orders/queue_positions: + get: + operationId: GetOrderQueuePositions + summary: Get Queue Positions for Orders + description: ' Endpoint for getting queue positions for all resting orders. Queue position represents the number of contracts that need to be matched before an order receives a partial or full match, determined using price-time priority.' + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - name: market_tickers + in: query + description: Comma-separated list of market tickers to filter by + schema: + type: string + - name: event_ticker + in: query + description: Event ticker to filter by + schema: + type: string + - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + responses: + '200': + description: Queue positions retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetOrderQueuePositionsResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/orders/{order_id}/queue_position: + get: + operationId: GetOrderQueuePosition + summary: Get Order Queue Position + description: ' Endpoint for getting an order''s queue position in the order book. This represents the amount of orders that need to be matched before this order receives a partial or full match. Queue position is determined using a price-time priority.' + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + responses: + '200': + description: Queue position retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetOrderQueuePositionResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/events/orders: + post: + operationId: CreateOrderV2 + summary: Create Order (V2) + description: >- + Endpoint for submitting event-market orders using the V2 + request/response shape (single-book `bid`/`ask` side and fixed-point + dollar prices). The legacy `/portfolio/orders` endpoint will be + deprecated no earlier than May 6, 2026 — clients should migrate to this + path. + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOrderV2Request' + responses: + '201': + description: Order created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOrderV2Response' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '409': + $ref: '#/components/responses/ConflictError' + '429': + $ref: '#/components/responses/RateLimitError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/events/orders/batched: + post: + operationId: BatchCreateOrdersV2 + summary: Batch Create Orders (V2) + description: >- + Endpoint for submitting a batch of event-market orders using the V2 + request/response shape. The maximum batch size scales with your tier's + write budget — see [Rate Limits and + Tiers](/getting_started/rate_limits). + x-mint: + content: > + + + **Rate limit:** 10 tokens per order in the batch — billed per item, so + total cost for a batch of N orders is N × 10. Other endpoints cost 10 + tokens per request (not per item) unless noted on their own page. See + [Rate Limits and Tiers](/getting_started/rate_limits). + + + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCreateOrdersV2Request' + responses: + '201': + description: Batch order creation completed + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCreateOrdersV2Response' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/InternalServerError' + delete: + operationId: BatchCancelOrdersV2 + summary: Batch Cancel Orders (V2) + description: >- + Endpoint for cancelling a batch of event-market orders using the V2 + response shape. The maximum batch size scales with your tier's write + budget — see [Rate Limits and Tiers](/getting_started/rate_limits). + x-mint: + content: > + + + **Rate limit:** 2 tokens per order in the batch — billed per item, so + total cost for a batch of N cancels is N × 2. Other endpoints cost 10 + tokens per request unless noted on their own page. See [Rate Limits + and Tiers](/getting_started/rate_limits). + + + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCancelOrdersV2Request' + responses: + '200': + description: Batch order cancellation completed + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCancelOrdersV2Response' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/events/orders/{order_id}: + delete: + operationId: CancelOrderV2 + summary: Cancel Order (V2) + description: >- + Endpoint for cancelling event-market orders using the V2 response shape. + Returns `{order_id, client_order_id, reduced_by}` rather than a full + order object. + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + - $ref: '#/components/parameters/ExchangeIndexQuery' + responses: + '200': + description: Order cancelled successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CancelOrderV2Response' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/events/orders/{order_id}/amend: + post: + operationId: AmendOrderV2 + summary: Amend Order (V2) + description: >- + Endpoint for amending the price and/or max fillable count of an existing + event-market order using the V2 request/response shape. The request + `count` is the updated total/max fillable count, equal to already filled + count plus desired resting remaining count. This behavior matches the v1 + amend endpoints; only the request/response shape differs. + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AmendOrderV2Request' + responses: + '200': + description: Order amended successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AmendOrderV2Response' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/events/orders/{order_id}/decrease: + post: + operationId: DecreaseOrderV2 + summary: Decrease Order (V2) + description: >- + Endpoint for decreasing the remaining count of an existing event-market + order using the V2 request/response shape. Exactly one of `reduce_by` or + `reduce_to` must be provided. + tags: + - orders + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/OrderIdPath' + - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DecreaseOrderV2Request' + responses: + '200': + description: Order decreased successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DecreaseOrderV2Response' + '400': + $ref: '#/components/responses/BadRequestError' '401': $ref: '#/components/responses/UnauthorizedError' '404': @@ -759,6 +1662,7 @@ paths: parameters: - $ref: '#/components/parameters/OrderGroupIdPath' - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + - $ref: '#/components/parameters/ExchangeIndexQuery' responses: '200': description: Order group deleted successfully @@ -786,6 +1690,7 @@ paths: parameters: - $ref: '#/components/parameters/OrderGroupIdPath' - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + - $ref: '#/components/parameters/ExchangeIndexQuery' requestBody: required: false content: @@ -819,6 +1724,7 @@ paths: parameters: - $ref: '#/components/parameters/OrderGroupIdPath' - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' + - $ref: '#/components/parameters/ExchangeIndexQuery' requestBody: required: false content: @@ -851,6 +1757,7 @@ paths: kalshiAccessTimestamp: [] parameters: - $ref: '#/components/parameters/OrderGroupIdPath' + - $ref: '#/components/parameters/ExchangeIndexQuery' requestBody: required: true content: @@ -901,8 +1808,10 @@ paths: operationId: CreateSubaccount summary: Create Subaccount description: >- - Creates a new subaccount for the authenticated user. Subaccounts are - numbered sequentially starting from 1. Maximum 32 subaccounts per user. + Creates a new subaccount for the authenticated user. This endpoint is + currently only available to institutions and market makers. Subaccounts + are numbered sequentially starting from 1. Maximum 32 subaccounts per + user. tags: - portfolio security: @@ -1000,246 +1909,18 @@ paths: schema: $ref: '#/components/schemas/GetSubaccountTransfersResponse' '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/subaccounts/netting: - put: - operationId: UpdateSubaccountNetting - summary: Update Subaccount Netting - description: >- - Updates the netting enabled setting for a specific subaccount. Use 0 for - the primary account, or 1-32 for numbered subaccounts. - tags: - - portfolio - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateSubaccountNettingRequest' - responses: - '200': - description: Netting setting updated successfully - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - get: - operationId: GetSubaccountNetting - summary: Get Subaccount Netting - description: Gets the netting enabled settings for all subaccounts. - tags: - - portfolio - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - responses: - '200': - description: Netting settings retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetSubaccountNettingResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/positions: - get: - operationId: GetPositions - summary: Get Positions - description: >- - Restricts the positions to those with any of following fields with - non-zero values, as a comma separated list. The following values are - accepted: position, total_traded - tags: - - portfolio - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/PositionsCursorQuery' - - $ref: '#/components/parameters/PositionsLimitQuery' - - $ref: '#/components/parameters/CountFilterQuery' - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' - responses: - '200': - description: Positions retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetPositionsResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/settlements: - get: - operationId: GetSettlements - summary: Get Settlements - description: ' Endpoint for getting the member''s settlements historical track.' - tags: - - portfolio - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/MinTsQuery' - - $ref: '#/components/parameters/MaxTsQuery' - - $ref: '#/components/parameters/SubaccountQuery' - responses: - '200': - description: Settlements retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetSettlementsResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/summary/total_resting_order_value: - get: - operationId: GetPortfolioRestingOrderTotalValue - summary: Get Total Resting Order Value - description: ' Endpoint for getting the total value, in cents, of resting orders. This endpoint is only intended for use by FCM members (rare). Note: If you''re uncertain about this endpoint, it likely does not apply to you.' - tags: - - portfolio - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - responses: - '200': - description: Total resting order value retrieved successfully - content: - application/json: - schema: - $ref: >- - #/components/schemas/GetPortfolioRestingOrderTotalValueResponse - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /portfolio/fills: - get: - operationId: GetFills - summary: Get Fills - description: > - Endpoint for getting all fills for the member. A fill is when a trade - you have is matched. - - Fills that occurred before the historical cutoff are only available via - `GET /historical/fills`. See [Historical - Data](https://kalshi.com/docs/getting_started/historical_data) for - details. - tags: - - portfolio - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/OrderIdQuery' - - $ref: '#/components/parameters/MinTsQuery' - - $ref: '#/components/parameters/MaxTsQuery' - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/SubaccountQuery' - responses: - '200': - description: Fills retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetFillsResponse' - '400': - description: Bad request - '401': - description: Unauthorized - '500': - description: Internal server error - /api_keys: - get: - operationId: GetApiKeys - summary: Get API Keys - description: ' Endpoint for retrieving all API keys associated with the authenticated user. API keys allow programmatic access to the platform without requiring username/password authentication. Each key has a unique identifier and name.' - tags: - - api-keys - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - responses: - '200': - description: List of API keys retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetApiKeysResponse' - '401': - description: Unauthorized - '500': - description: Internal server error - post: - operationId: CreateApiKey - summary: Create API Key - description: ' Endpoint for creating a new API key with a user-provided public key. This endpoint allows users with Premier or Market Maker API usage levels to create API keys by providing their own RSA public key. The platform will use this public key to verify signatures on API requests.' - tags: - - api-keys - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateApiKeyRequest' - responses: - '201': - description: API key created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/CreateApiKeyResponse' - '400': - description: Bad request - invalid input - '401': - description: Unauthorized - '403': - description: Forbidden - insufficient API usage level + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /api_keys/generate: - post: - operationId: GenerateApiKey - summary: Generate API Key - description: ' Endpoint for generating a new API key with an automatically created key pair. This endpoint generates both a public and private RSA key pair. The public key is stored on the platform, while the private key is returned to the user and must be stored securely. The private key cannot be retrieved again.' + $ref: '#/components/responses/InternalServerError' + /portfolio/subaccounts/netting: + put: + operationId: UpdateSubaccountNetting + summary: Update Subaccount Netting + description: >- + Updates the netting enabled setting for a specific subaccount. Use 0 for + the primary account, or 1-32 for numbered subaccounts. tags: - - api-keys + - portfolio security: - kalshiAccessKey: [] kalshiAccessSignature: [] @@ -1249,1190 +1930,951 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GenerateApiKeyRequest' + $ref: '#/components/schemas/UpdateSubaccountNettingRequest' responses: - '201': - description: API key generated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GenerateApiKeyResponse' + '200': + description: Netting setting updated successfully '400': - description: Bad request - invalid input + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /api_keys/{api_key}: - delete: - operationId: DeleteApiKey - summary: Delete API Key - description: ' Endpoint for deleting an existing API key. This endpoint permanently deletes an API key. Once deleted, the key can no longer be used for authentication. This action cannot be undone.' + $ref: '#/components/responses/InternalServerError' + get: + operationId: GetSubaccountNetting + summary: Get Subaccount Netting + description: Gets the netting enabled settings for all subaccounts. tags: - - api-keys + - portfolio security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] - parameters: - - name: api_key - in: path - required: true - description: API key ID to delete - schema: - type: string - responses: - '204': - description: API key successfully deleted - '400': - description: Bad request - invalid API key ID - '401': - description: Unauthorized - '404': - description: API key not found - '500': - description: Internal server error - /search/tags_by_categories: - get: - operationId: GetTagsForSeriesCategories - summary: Get Tags for Series Categories - description: > - Retrieve tags organized by series categories. - - - This endpoint returns a mapping of series categories to their associated - tags, which can be used for filtering and search functionality. - tags: - - search - responses: - '200': - description: Tags retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetTagsForSeriesCategoriesResponse' - '401': - description: Unauthorized - '500': - description: Internal server error - /search/filters_by_sport: - get: - operationId: GetFiltersForSports - summary: Get Filters for Sports - description: > - Retrieve available filters organized by sport. - - - This endpoint returns filtering options available for each sport, - including scopes and competitions. It also provides an ordered list of - sports for display purposes. - tags: - - search responses: '200': - description: Filters retrieved successfully + description: Netting settings retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetFiltersBySportsResponse' + $ref: '#/components/schemas/GetSubaccountNettingResponse' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /account/limits: + $ref: '#/components/responses/InternalServerError' + /portfolio/positions: get: - operationId: GetAccountApiLimits - summary: Get Account API Limits - description: ' Endpoint to retrieve the API tier limits associated with the authenticated user.' + operationId: GetPositions + summary: Get Positions + description: >- + Restricts the positions to those with any of following fields with + non-zero values, as a comma separated list. The following values are + accepted: position, total_traded tags: - - account + - portfolio security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/PositionsCursorQuery' + - $ref: '#/components/parameters/PositionsLimitQuery' + - $ref: '#/components/parameters/CountFilterQuery' + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/SingleEventTickerQuery' + - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' responses: '200': - description: Account API tier limits retrieved successfully + description: Positions retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetAccountApiLimitsResponse' + $ref: '#/components/schemas/GetPositionsResponse' + '400': + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /series/{series_ticker}/markets/{ticker}/candlesticks: + $ref: '#/components/responses/InternalServerError' + /portfolio/settlements: get: - operationId: GetMarketCandlesticks - summary: Get Market Candlesticks - description: > - Time period length of each candlestick in minutes. Valid values: 1 (1 - minute), 60 (1 hour), 1440 (1 day). - - Candlesticks for markets that settled before the historical cutoff are - only available via `GET /historical/markets/{ticker}/candlesticks`. See - [Historical - Data](https://kalshi.com/docs/getting_started/historical_data) for - details. + operationId: GetSettlements + summary: Get Settlements + description: ' Endpoint for getting the member''s settlements historical track.' tags: - - market + - portfolio + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: series_ticker - in: path - required: true - description: Series ticker - the series that contains the target market - schema: - type: string - - name: ticker - in: path - required: true - description: Market ticker - unique identifier for the specific market - schema: - type: string - - name: start_ts - in: query - required: true - description: >- - Start timestamp (Unix timestamp). Candlesticks will include those - ending on or after this time. - schema: - type: integer - format: int64 - - name: end_ts - in: query - required: true - description: >- - End timestamp (Unix timestamp). Candlesticks will include those - ending on or before this time. - schema: - type: integer - format: int64 - - name: period_interval - in: query - required: true - description: >- - Time period length of each candlestick in minutes. Valid values are - 1 (1 minute), 60 (1 hour), or 1440 (1 day). - schema: - type: integer - enum: - - 1 - - 60 - - 1440 - x-oapi-codegen-extra-tags: - validate: required,oneof=1 60 1440 - - name: include_latest_before_start - in: query - required: false - description: > - If true, prepends the latest candlestick available before the - start_ts. This synthetic candlestick is created by: - - 1. Finding the most recent real candlestick before start_ts - - 2. Projecting it forward to the first period boundary (calculated as - the next period interval after start_ts) - - 3. Setting all OHLC prices to null, and `previous_price` to the - close price from the real candlestick - schema: - type: boolean - default: false + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/SingleEventTickerQuery' + - $ref: '#/components/parameters/MinTsQuery' + - $ref: '#/components/parameters/MaxTsQuery' + - $ref: '#/components/parameters/SubaccountQuery' responses: '200': - description: Candlesticks retrieved successfully + description: Settlements retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMarketCandlesticksResponse' + $ref: '#/components/schemas/GetSettlementsResponse' '400': - description: Bad request - '404': - description: Not found + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /markets/trades: + $ref: '#/components/responses/InternalServerError' + /portfolio/deposits: get: - operationId: GetTrades - summary: Get Trades - description: > - Endpoint for getting all trades for all markets. A trade represents a - completed transaction between two users on a specific market. Each trade - includes the market ticker, price, quantity, and timestamp information. - This endpoint returns a paginated response. Use the 'limit' parameter to - control page size (1-1000, defaults to 100). The response includes a - 'cursor' field - pass this value in the 'cursor' parameter of your next - request to get the next page. An empty cursor indicates no more pages - are available. + operationId: GetDeposits + summary: Get Deposits + description: Endpoint for getting the member's deposit history. tags: - - market + - portfolio + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/MarketLimitQuery' + - $ref: '#/components/parameters/WithdrawalLimitQuery' - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/TickerQuery' - - $ref: '#/components/parameters/MinTsQuery' - - $ref: '#/components/parameters/MaxTsQuery' responses: '200': - description: Trades retrieved successfully + description: Deposits retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetTradesResponse' + $ref: '#/components/schemas/GetDepositsResponse' '400': - description: Bad request + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /series/{series_ticker}/events/{ticker}/candlesticks: + $ref: '#/components/responses/InternalServerError' + /portfolio/withdrawals: get: - operationId: GetMarketCandlesticksByEvent - summary: Get Event Candlesticks - description: ' End-point for returning aggregated data across all markets corresponding to an event.' + operationId: GetWithdrawals + summary: Get Withdrawals + description: Endpoint for getting the member's withdrawal history. tags: - - events + - portfolio + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: ticker - in: path - required: true - description: The event ticker - schema: - type: string - - name: series_ticker - in: path - required: true - description: The series ticker - schema: - type: string - - name: start_ts - in: query - required: true - description: Start timestamp for the range - schema: - type: integer - format: int64 - x-oapi-codegen-extra-tags: - validate: required - - name: end_ts - in: query - required: true - description: End timestamp for the range - schema: - type: integer - format: int64 - x-oapi-codegen-extra-tags: - validate: required - - name: period_interval - in: query - required: true - description: >- - Specifies the length of each candlestick period, in minutes. Must be - one minute, one hour, or one day. - schema: - type: integer - format: int32 - enum: - - 1 - - 60 - - 1440 - x-oapi-codegen-extra-tags: - validate: required,oneof=1 60 1440 + - $ref: '#/components/parameters/WithdrawalLimitQuery' + - $ref: '#/components/parameters/CursorQuery' responses: '200': - description: Event candlesticks retrieved successfully + description: Withdrawals retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetEventCandlesticksResponse' + $ref: '#/components/schemas/GetWithdrawalsResponse' '400': - description: Bad request + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /events: + $ref: '#/components/responses/InternalServerError' + /portfolio/summary/total_resting_order_value: get: - operationId: GetEvents - summary: Get Events + operationId: GetPortfolioRestingOrderTotalValue + summary: Get Total Resting Order Value + description: ' Endpoint for getting the total value, in cents, of resting orders. This endpoint is only intended for use by FCM members (rare). Note: If you''re uncertain about this endpoint, it likely does not apply to you.' + tags: + - portfolio + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + responses: + '200': + description: Total resting order value retrieved successfully + content: + application/json: + schema: + $ref: >- + #/components/schemas/GetPortfolioRestingOrderTotalValueResponse + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + /portfolio/fills: + get: + operationId: GetFills + summary: Get Fills description: > - Get all events. This endpoint excludes multivariate events. - - To retrieve multivariate events, use the GET /events/multivariate - endpoint. + Endpoint for getting all fills for the member. A fill is when a trade + you have is matched. - All events are accessible through this endpoint, even if their - associated markets are older than the historical cutoff. + Fills that occurred before the historical cutoff are only available via + `GET /historical/fills`. See [Historical + Data](https://docs.kalshi.com/getting_started/historical_data) for + details. tags: - - events + - portfolio + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: limit - in: query - required: false - description: >- - Parameter to specify the number of results per page. Defaults to - 200. Maximum value is 200. - schema: - type: integer - minimum: 1 - maximum: 200 - default: 200 - - name: cursor - in: query - required: false - description: >- - Parameter to specify the pagination cursor. Use the cursor value - returned from the previous response to get the next page of results. - Leave empty for the first page. - schema: - type: string - - name: with_nested_markets - in: query - required: false - description: >- - Parameter to specify if nested markets should be included in the - response. When true, each event will include a 'markets' field - containing a list of Market objects associated with that event. - Historical markets settled before the historical cutoff will not be - included. - schema: - type: boolean - default: false - x-go-type-skip-optional-pointer: true - - name: with_milestones - in: query - required: false - description: If true, includes related milestones as a field alongside events. - schema: - type: boolean - default: false - x-go-type-skip-optional-pointer: true - - name: status - in: query - required: false - description: >- - Filter by event status. Possible values are 'unopened', 'open', - 'closed', 'settled'. Leave empty to return events with any status. - schema: - type: string - enum: - - unopened - - open - - closed - - settled - - $ref: '#/components/parameters/SeriesTickerQuery' - - name: min_close_ts - in: query - required: false - description: >- - Filter events with at least one market with close timestamp greater - than this Unix timestamp (in seconds). - schema: - type: integer - format: int64 - - name: min_updated_ts - in: query - required: false - description: >- - Filter events with metadata updated after this Unix timestamp (in - seconds). Use this to efficiently poll for changes. - schema: - type: integer - format: int64 + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/OrderIdQuery' + - $ref: '#/components/parameters/MinTsQuery' + - $ref: '#/components/parameters/MaxTsQuery' + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/SubaccountQuery' responses: '200': - description: Events retrieved successfully + description: Fills retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetEventsResponse' + $ref: '#/components/schemas/GetFillsResponse' '400': description: Bad request '401': description: Unauthorized '500': description: Internal server error - /events/multivariate: + /communications/id: get: - operationId: GetMultivariateEvents - summary: Get Multivariate Events - description: >- - Retrieve multivariate (combo) events. These are dynamically created - events from multivariate event collections. Supports filtering by series - and collection ticker. + operationId: GetCommunicationsID + summary: Get Communications ID + description: ' Endpoint for getting the communications ID of the logged-in user.' tags: - - events + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + responses: + '200': + description: Communications ID retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetCommunicationsIDResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + /communications/rfqs: + get: + operationId: GetRFQs + summary: Get RFQs + description: ' Endpoint for getting RFQs' + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/SingleEventTickerQuery' + - $ref: '#/components/parameters/MarketTickerQuery' + - $ref: '#/components/parameters/SubaccountQuery' - name: limit in: query - required: false - description: Number of results per page. Defaults to 100. Maximum value is 200. + description: >- + Parameter to specify the number of results per page. Defaults to + 100. schema: type: integer + format: int32 minimum: 1 - maximum: 200 + maximum: 100 default: 100 - - name: cursor + - name: status in: query - required: false - description: >- - Pagination cursor. Use the cursor value returned from the previous - response to get the next page of results. + description: Filter RFQs by status schema: type: string - - $ref: '#/components/parameters/SeriesTickerQuery' - - name: collection_ticker + - name: creator_user_id in: query - required: false - description: >- - Filter events by collection ticker. Returns only multivariate events - belonging to the specified collection. Cannot be used together with - series_ticker. + description: Filter RFQs by creator user ID + deprecated: true schema: type: string - - name: with_nested_markets + - name: user_filter in: query required: false - description: >- - Parameter to specify if nested markets should be included in the - response. When true, each event will include a 'markets' field - containing a list of Market objects associated with that event. schema: - type: boolean - default: false + $ref: '#/components/schemas/UserFilter' + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: omitempty,oneof=self responses: '200': - description: Multivariate events retrieved successfully + description: RFQs retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMultivariateEventsResponse' - '400': - description: Bad request - invalid parameters + $ref: '#/components/schemas/GetRFQsResponse' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /events/{event_ticker}: - get: - operationId: GetEvent - summary: Get Event - description: > - Endpoint for getting data about an event by its ticker. An event - represents a real-world occurrence that can be traded on, such as an - election, sports game, or economic indicator release. - - Events contain one or more markets where users can place trades on - different outcomes. - - All events are accessible through this endpoint, even if their - associated markets are older than the historical cutoff. + $ref: '#/components/responses/InternalServerError' + post: + operationId: CreateRFQ + summary: Create RFQ + description: ' Endpoint for creating a new RFQ. You can have a maximum of 100 open RFQs at a time.' tags: - - events - parameters: - - name: event_ticker - in: path - required: true - description: Event ticker - schema: - type: string - - name: with_nested_markets - in: query - required: false - description: >- - If true, markets are included within the event object. If false - (default), markets are returned as a separate top-level field in the - response. Historical markets settled before the historical cutoff - will not be included. - schema: - type: boolean - default: false - x-go-type-skip-optional-pointer: true + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateRFQRequest' responses: - '200': - description: Event retrieved successfully + '201': + description: RFQ created successfully content: application/json: schema: - $ref: '#/components/schemas/GetEventResponse' + $ref: '#/components/schemas/CreateRFQResponse' '400': - description: Bad request + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized - '404': - description: Event not found + $ref: '#/components/responses/UnauthorizedError' + '409': + $ref: '#/components/responses/ConflictError' '500': - description: Internal server error - /events/{event_ticker}/metadata: + $ref: '#/components/responses/InternalServerError' + /communications/rfqs/{rfq_id}: get: - operationId: GetEventMetadata - summary: Get Event Metadata - description: ' Endpoint for getting metadata about an event by its ticker. Returns only the metadata information for an event.' + operationId: GetRFQ + summary: Get RFQ + description: ' Endpoint for getting a single RFQ by id' tags: - - events + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: event_ticker - in: path - required: true - description: Event ticker - schema: - type: string + - $ref: '#/components/parameters/RfqIdPath' responses: '200': - description: Event metadata retrieved successfully + description: RFQ retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetEventMetadataResponse' - '400': - description: Bad request + $ref: '#/components/schemas/GetRFQResponse' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '404': - description: Event not found + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error - /series/{series_ticker}/events/{ticker}/forecast_percentile_history: + $ref: '#/components/responses/InternalServerError' + delete: + operationId: DeleteRFQ + summary: Delete RFQ + description: ' Endpoint for deleting an RFQ by ID' + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - $ref: '#/components/parameters/RfqIdPath' + responses: + '204': + description: RFQ deleted successfully + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /communications/quotes: get: - operationId: GetEventForecastPercentilesHistory - summary: Get Event Forecast Percentile History - description: >- - Endpoint for getting the historical raw and formatted forecast numbers - for an event at specific percentiles. + operationId: GetQuotes + summary: Get Quotes + description: ' Endpoint for getting quotes' tags: - - events + - communications security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - name: ticker - in: path - required: true - description: The event ticker + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/SingleEventTickerQuery' + - $ref: '#/components/parameters/MarketTickerQuery' + - name: limit + in: query + description: >- + Parameter to specify the number of results per page. Defaults to + 500. + schema: + type: integer + format: int32 + minimum: 1 + maximum: 500 + default: 500 + - name: status + in: query + description: Filter quotes by status schema: type: string - - name: series_ticker - in: path - required: true - description: The series ticker + x-go-type-skip-optional-pointer: true + - name: quote_creator_user_id + in: query + description: Filter quotes by quote creator user ID + deprecated: true schema: type: string - - name: percentiles + x-go-type-skip-optional-pointer: true + - name: user_filter in: query - required: true - description: Array of percentile values to retrieve (0-10000, max 10 values) + required: false + description: Filter for quotes created by the authenticated user. schema: - type: array - items: - type: integer - format: int32 - minimum: 0 - maximum: 10000 - maxItems: 10 - style: form - explode: true - - name: start_ts + $ref: '#/components/schemas/UserFilter' + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: omitempty,oneof=self + - name: rfq_user_filter in: query - required: true - description: Start timestamp for the range + required: false + description: >- + Filter for quotes responding to RFQs created by the authenticated + user. schema: - type: integer - format: int64 - - name: end_ts + $ref: '#/components/schemas/UserFilter' + x-go-type-skip-optional-pointer: true + x-oapi-codegen-extra-tags: + validate: omitempty,oneof=self + - name: rfq_creator_user_id in: query - required: true - description: End timestamp for the range + description: Filter quotes by RFQ creator user ID + deprecated: true schema: - type: integer - format: int64 - - name: period_interval + type: string + x-go-type-skip-optional-pointer: true + - name: rfq_creator_subtrader_id in: query - required: true - description: >- - Specifies the length of each forecast period, in minutes. 0 for - 5-second intervals, or 1, 60, or 1440 for minute-based intervals. + description: Filter quotes by RFQ creator subtrader ID (FCM members only) schema: - type: integer - format: int32 - enum: - - 0 - - 1 - - 60 - - 1440 + type: string + x-go-type-skip-optional-pointer: true + - name: rfq_id + in: query + description: Filter quotes by RFQ ID + schema: + type: string + x-go-type-skip-optional-pointer: true + responses: + '200': + description: Quotes retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetQuotesResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + post: + operationId: CreateQuote + summary: Create Quote + description: ' Endpoint for creating a quote in response to an RFQ' + x-mint: + content: > + + + **Rate limit:** 2 tokens per request. Other endpoints use the default + cost of 10 tokens per request unless noted on their own page. See + [Rate Limits and Tiers](/getting_started/rate_limits). + + + tags: + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateQuoteRequest' responses: - '200': - description: Event forecast percentile history retrieved successfully + '201': + description: Quote created successfully content: application/json: schema: - $ref: >- - #/components/schemas/GetEventForecastPercentilesHistoryResponse + $ref: '#/components/schemas/CreateQuoteResponse' '400': - description: Bad request + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /live_data/milestone/{milestone_id}: + $ref: '#/components/responses/InternalServerError' + /communications/quotes/{quote_id}: get: - operationId: GetLiveDataByMilestone - summary: Get Live Data - description: Get live data for a specific milestone. + operationId: GetQuote + summary: Get Quote + description: ' Endpoint for getting a particular quote' tags: - - live-data + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: milestone_id - in: path - required: true - description: Milestone ID - schema: - type: string - - name: include_player_stats - in: query - required: false - description: >- - When true, includes player-level statistics in the live data - response. Supported for Pro Football, Pro Basketball, and College - Men's Basketball milestones that have player ID mappings configured. - Has no effect for other sports or milestones without player - mappings. - schema: - type: boolean - default: false + - $ref: '#/components/parameters/QuoteIdPath' responses: '200': - description: Live data retrieved successfully + description: Quote retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetLiveDataResponse' + $ref: '#/components/schemas/GetQuoteResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' '404': - description: Live data not found + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error - /live_data/{type}/milestone/{milestone_id}: - get: - operationId: GetLiveData - summary: Get Live Data (with type) - description: >- - Get live data for a specific milestone. This is the legacy endpoint that - requires a type path parameter. Prefer using - `/live_data/milestone/{milestone_id}` instead. + $ref: '#/components/responses/InternalServerError' + delete: + operationId: DeleteQuote + summary: Delete Quote + description: ' Endpoint for deleting a quote, which means it can no longer be accepted.' + x-mint: + content: > + + + **Rate limit:** 2 tokens per request. Other endpoints use the default + cost of 10 tokens per request unless noted on their own page. See + [Rate Limits and Tiers](/getting_started/rate_limits). + + tags: - - live-data + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: type - in: path - required: true - description: Type of live data - schema: - type: string - - name: milestone_id - in: path - required: true - description: Milestone ID - schema: - type: string - - name: include_player_stats - in: query - required: false - description: >- - When true, includes player-level statistics in the live data - response. Supported for Pro Football, Pro Basketball, and College - Men's Basketball milestones that have player ID mappings configured. - Has no effect for other sports or milestones without player - mappings. - schema: - type: boolean - default: false + - $ref: '#/components/parameters/QuoteIdPath' responses: - '200': - description: Live data retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetLiveDataResponse' + '204': + description: Quote deleted successfully + '401': + $ref: '#/components/responses/UnauthorizedError' '404': - description: Live data not found + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error - /live_data/batch: - get: - operationId: GetLiveDatas - summary: Get Multiple Live Data - description: Get live data for multiple milestones + $ref: '#/components/responses/InternalServerError' + /communications/quotes/{quote_id}/accept: + put: + operationId: AcceptQuote + summary: Accept Quote + description: ' Endpoint for accepting a quote. This will require the quoter to confirm' tags: - - live-data + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: milestone_ids - in: query - required: true - description: Array of milestone IDs - schema: - type: array - items: - type: string - maxItems: 100 - style: form - explode: true - - name: include_player_stats - in: query - required: false - description: >- - When true, includes player-level statistics in the live data - response. Supported for Pro Football, Pro Basketball, and College - Men's Basketball milestones that have player ID mappings configured. - Has no effect for other sports or milestones without player - mappings. - schema: - type: boolean - default: false + - $ref: '#/components/parameters/QuoteIdPath' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AcceptQuoteRequest' responses: - '200': - description: Live data retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetLiveDatasResponse' + '204': + description: Quote accepted successfully + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' '500': - description: Internal server error - /live_data/milestone/{milestone_id}/game_stats: - get: - operationId: GetGameStats - summary: Get Game Stats - description: >- - Get play-by-play game statistics for a specific milestone. Supported - sports: Pro Football, College Football, Pro Basketball, College Men's - Basketball, College Women's Basketball, WNBA, Soccer, Pro Hockey, and - Pro Baseball. Returns null for unsupported milestone types or milestones - without a Sportradar ID. + $ref: '#/components/responses/InternalServerError' + /communications/quotes/{quote_id}/confirm: + put: + operationId: ConfirmQuote + summary: Confirm Quote + description: ' Endpoint for confirming a quote. This will start a timer for order execution' tags: - - live-data + - communications + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - name: milestone_id - in: path - required: true - description: Milestone ID - schema: - type: string + - $ref: '#/components/parameters/QuoteIdPath' + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyResponse' + responses: + '204': + description: Quote confirmed successfully + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /api_keys: + get: + operationId: GetApiKeys + summary: Get API Keys + description: ' Endpoint for retrieving all API keys associated with the authenticated user. API keys allow programmatic access to the platform without requiring username/password authentication. Each key has a unique identifier and name.' + tags: + - api-keys + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] responses: '200': - description: Game stats retrieved successfully + description: List of API keys retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetGameStatsResponse' - '404': - description: Game stats not found + $ref: '#/components/schemas/GetApiKeysResponse' + '401': + description: Unauthorized '500': description: Internal server error - /incentive_programs: - get: - operationId: GetIncentivePrograms - summary: Get Incentives - description: ' List incentives with optional filters. Incentives are rewards programs for trading activity on specific markets.' + post: + operationId: CreateApiKey + summary: Create API Key + description: ' Endpoint for creating a new API key with a user-provided public key. This endpoint allows users with Premier or Market Maker API usage levels to create API keys by providing their own RSA public key. The platform will use this public key to verify signatures on API requests.' tags: - - incentive-programs - parameters: - - name: status - in: query - required: false - description: >- - Status filter. Can be "all", "active", "upcoming", "closed", or - "paid_out". Default is "all". - schema: - type: string - enum: - - all - - active - - upcoming - - closed - - paid_out - - name: type - in: query - required: false - description: >- - Type filter. Can be "all", "liquidity", or "volume". Default is - "all". - schema: - type: string - enum: - - all - - liquidity - - volume - - name: limit - in: query - required: false - description: Number of results per page. Defaults to 100. Maximum value is 10000. - schema: - type: integer - minimum: 1 - maximum: 10000 - - name: cursor - in: query - required: false - description: Cursor for pagination - schema: - type: string + - api-keys + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateApiKeyRequest' responses: - '200': - description: Incentive programs retrieved successfully + '201': + description: API key created successfully content: application/json: schema: - $ref: '#/components/schemas/GetIncentiveProgramsResponse' + $ref: '#/components/schemas/CreateApiKeyResponse' '400': - description: Invalid request parameters - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + description: Bad request - invalid input + '401': + description: Unauthorized + '403': + description: Forbidden - insufficient API usage level '500': description: Internal server error + /api_keys/generate: + post: + operationId: GenerateApiKey + summary: Generate API Key + description: ' Endpoint for generating a new API key with an automatically created key pair. This endpoint generates both a public and private RSA key pair. The public key is stored on the platform, while the private key is returned to the user and must be stored securely. The private key cannot be retrieved again.' + tags: + - api-keys + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateApiKeyRequest' + responses: + '201': + description: API key generated successfully content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' - /fcm/orders: - get: - operationId: GetFCMOrders - summary: Get FCM Orders - description: > - Endpoint for FCM members to get orders filtered by subtrader ID. - - This endpoint requires FCM member access level and allows filtering - orders by subtrader ID. + $ref: '#/components/schemas/GenerateApiKeyResponse' + '400': + description: Bad request - invalid input + '401': + description: Unauthorized + '500': + description: Internal server error + /api_keys/{api_key}: + delete: + operationId: DeleteApiKey + summary: Delete API Key + description: ' Endpoint for deleting an existing API key. This endpoint permanently deletes an API key. Once deleted, the key can no longer be used for authentication. This action cannot be undone.' tags: - - fcm + - api-keys security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] parameters: - - name: subtrader_id - in: query + - name: api_key + in: path required: true - description: >- - Restricts the response to orders for a specific subtrader (FCM - members only) - schema: - type: string - - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/TickerQuery' - - name: min_ts - in: query - description: >- - Restricts the response to orders after a timestamp, formatted as a - Unix Timestamp - schema: - type: integer - format: int64 - - name: max_ts - in: query - description: >- - Restricts the response to orders before a timestamp, formatted as a - Unix Timestamp - schema: - type: integer - format: int64 - - name: status - in: query - description: Restricts the response to orders that have a certain status + description: API key ID to delete schema: type: string - enum: - - resting - - canceled - - executed - - name: limit - in: query - description: Parameter to specify the number of results per page. Defaults to 100 - schema: - type: integer - minimum: 1 - maximum: 1000 responses: - '200': - description: Orders retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetOrdersResponse' + '204': + description: API key successfully deleted '400': - description: Bad request + description: Bad request - invalid API key ID '401': description: Unauthorized '404': - description: Not found + description: API key not found '500': description: Internal server error - /fcm/positions: + /account/limits: get: - operationId: GetFCMPositions - summary: Get FCM Positions - description: > - Endpoint for FCM members to get market positions filtered by subtrader - ID. - - This endpoint requires FCM member access level and allows filtering - positions by subtrader ID. + operationId: GetAccountApiLimits + summary: Get Account API Limits + description: ' Endpoint to retrieve the API tier limits associated with the authenticated user.' tags: - - fcm + - account security: - kalshiAccessKey: [] kalshiAccessSignature: [] kalshiAccessTimestamp: [] - parameters: - - name: subtrader_id - in: query - required: true - description: >- - Restricts the response to positions for a specific subtrader (FCM - members only) - schema: - type: string - - name: ticker - in: query - description: Ticker of desired positions - schema: - type: string - x-go-type-skip-optional-pointer: true - - name: event_ticker - in: query - description: Event ticker of desired positions - schema: - type: string - x-go-type-skip-optional-pointer: true - - name: count_filter - in: query - description: >- - Restricts the positions to those with any of following fields with - non-zero values, as a comma separated list - schema: - type: string - - name: settlement_status - in: query - description: Settlement status of the markets to return. Defaults to unsettled - schema: - type: string - enum: - - all - - unsettled - - settled - - name: limit - in: query - description: Parameter to specify the number of results per page. Defaults to 100 - schema: - type: integer - minimum: 1 - maximum: 1000 - - name: cursor - in: query - description: >- - The Cursor represents a pointer to the next page of records in the - pagination - schema: - type: string responses: '200': - description: Positions retrieved successfully + description: Account API tier limits retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetAccountApiLimitsResponse' + '401': + description: Unauthorized + '500': + description: Internal server error + /account/endpoint_costs: + get: + operationId: GetAccountEndpointCosts + summary: List Non-Default Endpoint Costs + description: >- + Lists API v2 endpoints whose configured token cost differs from the + default cost. Endpoints that use the default cost are omitted. + tags: + - account + responses: + '200': + description: Non-default endpoint costs retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetPositionsResponse' - '400': - description: Bad request + $ref: '#/components/schemas/GetAccountEndpointCostsResponse' + '500': + description: Internal server error + /search/tags_by_categories: + get: + operationId: GetTagsForSeriesCategories + summary: Get Tags for Series Categories + description: > + Retrieve tags organized by series categories. + + + This endpoint returns a mapping of series categories to their associated + tags, which can be used for filtering and search functionality. + tags: + - search + responses: + '200': + description: Tags retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetTagsForSeriesCategoriesResponse' '401': description: Unauthorized - '404': - description: Not found '500': description: Internal server error - /structured_targets: + /search/filters_by_sport: get: - operationId: GetStructuredTargets - summary: Get Structured Targets - description: 'Page size (min: 1, max: 2000)' + operationId: GetFiltersForSports + summary: Get Filters for Sports + description: > + Retrieve available filters organized by sport. + + + This endpoint returns filtering options available for each sport, + including scopes and competitions. It also provides an ordered list of + sports for display purposes. tags: - - structured-targets - parameters: - - name: ids - in: query - description: >- - Filter by specific structured target IDs. Pass multiple IDs by - repeating the parameter (e.g. `?ids=uuid1&ids=uuid2`). - required: false - schema: - type: array - items: - type: string - style: form - explode: true - - name: type - in: query - description: Filter by structured target type - required: false - schema: - type: string - example: basketball_player - - name: competition - in: query - description: >- - Filter by competition. Matches against the league, conference, - division, or tour in the structured target details. - required: false - schema: - type: string - example: NBA - - name: page_size - in: query - description: Number of items per page (min 1, max 2000, default 100) - required: false - schema: - type: integer - format: int32 - minimum: 1 - maximum: 2000 - default: 100 - - name: cursor - in: query - description: Pagination cursor - required: false - schema: - type: string + - search responses: '200': - description: Structured targets retrieved successfully + description: Filters retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetStructuredTargetsResponse' + $ref: '#/components/schemas/GetFiltersBySportsResponse' '401': description: Unauthorized '500': description: Internal server error - /structured_targets/{structured_target_id}: + /live_data/milestone/{milestone_id}: get: - operationId: GetStructuredTarget - summary: Get Structured Target - description: ' Endpoint for getting data about a specific structured target by its ID.' + operationId: GetLiveDataByMilestone + summary: Get Live Data + description: Get live data for a specific milestone. tags: - - structured-targets + - live-data parameters: - - name: structured_target_id + - name: milestone_id in: path required: true - description: Structured target ID + description: Milestone ID schema: type: string + - name: include_player_stats + in: query + required: false + description: >- + When true, includes player-level statistics in the live data + response. Supported for Pro Football, Pro Basketball, and College + Men's Basketball milestones that have player ID mappings configured. + Has no effect for other sports or milestones without player + mappings. + schema: + type: boolean + default: false responses: '200': - description: Structured target retrieved successfully + description: Live data retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetStructuredTargetResponse' - '401': - description: Unauthorized + $ref: '#/components/schemas/GetLiveDataResponse' '404': - description: Not found + description: Live data not found '500': description: Internal server error - /markets/{ticker}/orderbook: + /live_data/{type}/milestone/{milestone_id}: get: - operationId: GetMarketOrderbook - summary: Get Market Orderbook - description: ' Endpoint for getting the current order book for a specific market. The order book shows all active bid orders for both yes and no sides of a binary market. It returns yes bids and no bids only (no asks are returned). This is because in binary markets, a bid for yes at price X is equivalent to an ask for no at price (100-X). For example, a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract sizes. Each side shows price levels with their corresponding quantities and order counts, organized from best to worst prices.' + operationId: GetLiveData + summary: Get Live Data (with type) + description: >- + Get live data for a specific milestone. This is the legacy endpoint that + requires a type path parameter. Prefer using + `/live_data/milestone/{milestone_id}` instead. tags: - - market - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - live-data parameters: - - $ref: '#/components/parameters/TickerPath' - - name: depth + - name: type + in: path + required: true + description: Type of live data + schema: + type: string + - name: milestone_id + in: path + required: true + description: Milestone ID + schema: + type: string + - name: include_player_stats in: query - description: >- - Depth of the orderbook to retrieve (0 or negative means all levels, - 1-100 for specific depth) required: false + description: >- + When true, includes player-level statistics in the live data + response. Supported for Pro Football, Pro Basketball, and College + Men's Basketball milestones that have player ID mappings configured. + Has no effect for other sports or milestones without player + mappings. schema: - type: integer - minimum: 0 - maximum: 100 - default: 0 - x-oapi-codegen-extra-tags: - validate: omitempty,min=0,max=100 + type: boolean + default: false responses: '200': - description: Orderbook retrieved successfully + description: Live data retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMarketOrderbookResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' + $ref: '#/components/schemas/GetLiveDataResponse' '404': - $ref: '#/components/responses/NotFoundError' + description: Live data not found '500': - $ref: '#/components/responses/InternalServerError' - /markets/orderbooks: + description: Internal server error + /live_data/batch: get: - operationId: GetMarketOrderbooks - summary: Get Multiple Market Orderbooks - description: >- - Endpoint for getting the current order books for multiple markets in a - single request. The order book shows all active bid orders for both yes - and no sides of a binary market. It returns yes bids and no bids only - (no asks are returned). This is because in binary markets, a bid for yes - at price X is equivalent to an ask for no at price (100-X). For example, - a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract - sizes. Each side shows price levels with their corresponding quantities - and order counts, organized from best to worst prices. Returns one - orderbook per requested market ticker. + operationId: GetLiveDatas + summary: Get Multiple Live Data + description: Get live data for multiple milestones tags: - - market - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - live-data parameters: - - name: tickers + - name: milestone_ids in: query required: true - description: List of market tickers to fetch orderbooks for + description: Array of milestone IDs schema: type: array items: type: string - maxLength: 200 - minItems: 1 maxItems: 100 style: form explode: true - x-oapi-codegen-extra-tags: - validate: required,min=1,max=100,dive,max=200 + - name: include_player_stats + in: query + required: false + description: >- + When true, includes player-level statistics in the live data + response. Supported for Pro Football, Pro Basketball, and College + Men's Basketball milestones that have player ID mappings configured. + Has no effect for other sports or milestones without player + mappings. + schema: + type: boolean + default: false responses: '200': - description: Orderbooks retrieved successfully + description: Live data retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMarketOrderbooksResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' + $ref: '#/components/schemas/GetLiveDatasResponse' '500': - $ref: '#/components/responses/InternalServerError' - /milestones/{milestone_id}: + description: Internal server error + /live_data/milestone/{milestone_id}/game_stats: get: - operationId: GetMilestone - summary: Get Milestone - description: ' Endpoint for getting data about a specific milestone by its ID.' + operationId: GetGameStats + summary: Get Game Stats + description: >- + Get play-by-play game statistics for a specific milestone. Supported + sports: Pro Football, College Football, Pro Basketball, College Men's + Basketball, College Women's Basketball, WNBA, Soccer, Pro Hockey, and + Pro Baseball. Returns null for unsupported milestone types or milestones + without a Sportradar ID. tags: - - milestone + - live-data parameters: - name: milestone_id in: path @@ -2442,461 +2884,227 @@ paths: type: string responses: '200': - description: Milestone retrieved successfully + description: Game stats retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMilestoneResponse' - '400': - description: Bad Request - '401': - description: Unauthorized + $ref: '#/components/schemas/GetGameStatsResponse' '404': - description: Not Found + description: Game stats not found '500': - description: Internal Server Error - /milestones: + description: Internal server error + /structured_targets: get: - operationId: GetMilestones - summary: Get Milestones - description: 'Minimum start date to filter milestones. Format: RFC3339 timestamp' + operationId: GetStructuredTargets + summary: Get Structured Targets + description: 'Page size (min: 1, max: 2000)' tags: - - milestone + - structured-targets parameters: - - name: limit - in: query - description: Number of milestones to return per page - required: true - schema: - type: integer - minimum: 1 - maximum: 500 - - name: minimum_start_date - in: query - description: Minimum start date to filter milestones. Format RFC3339 timestamp - required: false - schema: - type: string - format: date-time - - name: category - in: query - description: >- - Filter by milestone category. E.g. Sports, Elections, Esports, - Crypto. - required: false - schema: - type: string - example: Sports - - name: competition + - name: ids in: query description: >- - Filter by competition. E.g. Pro Football, Pro Basketball (M), Pro - Baseball, Pro Hockey, College Football. - required: false - schema: - type: string - example: Pro Football - - name: source_id - in: query - description: Filter by source id + Filter by specific structured target IDs. Pass multiple IDs by + repeating the parameter (e.g. `?ids=uuid1&ids=uuid2`). required: false schema: - type: string + type: array + items: + type: string + style: form + explode: true - name: type in: query - description: >- - Filter by milestone type. E.g. football_game, basketball_game, - soccer_tournament_multi_leg, baseball_game, hockey_match, - political_race. - required: false - schema: - type: string - example: football_game - - name: related_event_ticker - in: query - description: Filter by related event ticker + description: Filter by structured target type required: false schema: type: string - - name: cursor + example: basketball_player + - name: competition in: query description: >- - Pagination cursor. Use the cursor value returned from the previous - response to get the next page of results + Filter by competition. Matches against the league, conference, + division, or tour in the structured target details. required: false schema: type: string - - name: min_updated_ts + example: NBA + - name: page_size in: query + description: Number of items per page (min 1, max 2000, default 100) required: false - description: >- - Filter milestones with metadata updated after this Unix timestamp - (in seconds). Use this to efficiently poll for changes. - schema: - type: integer - format: int64 - responses: - '200': - description: Milestones retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetMilestonesResponse' - '400': - description: Bad Request - '401': - description: Unauthorized - '500': - description: Internal Server Error - /communications/id: - get: - operationId: GetCommunicationsID - summary: Get Communications ID - description: ' Endpoint for getting the communications ID of the logged-in user.' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - responses: - '200': - description: Communications ID retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetCommunicationsIDResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /communications/rfqs: - get: - operationId: GetRFQs - summary: Get RFQs - description: ' Endpoint for getting RFQs' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/MarketTickerQuery' - - $ref: '#/components/parameters/SubaccountQuery' - - name: limit - in: query - description: >- - Parameter to specify the number of results per page. Defaults to - 100. schema: type: integer format: int32 minimum: 1 - maximum: 100 + maximum: 2000 default: 100 - - name: status - in: query - description: Filter RFQs by status - schema: - type: string - - name: creator_user_id + - name: cursor in: query - description: Filter RFQs by creator user ID + description: Pagination cursor + required: false schema: type: string responses: '200': - description: RFQs retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetRFQsResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - operationId: CreateRFQ - summary: Create RFQ - description: ' Endpoint for creating a new RFQ. You can have a maximum of 100 open RFQs at a time.' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateRFQRequest' - responses: - '201': - description: RFQ created successfully + description: Structured targets retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/CreateRFQResponse' - '400': - $ref: '#/components/responses/BadRequestError' + $ref: '#/components/schemas/GetStructuredTargetsResponse' '401': - $ref: '#/components/responses/UnauthorizedError' - '409': - $ref: '#/components/responses/ConflictError' + description: Unauthorized '500': - $ref: '#/components/responses/InternalServerError' - /communications/rfqs/{rfq_id}: + description: Internal server error + /structured_targets/{structured_target_id}: get: - operationId: GetRFQ - summary: Get RFQ - description: ' Endpoint for getting a single RFQ by id' + operationId: GetStructuredTarget + summary: Get Structured Target + description: ' Endpoint for getting data about a specific structured target by its ID.' tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - structured-targets parameters: - - $ref: '#/components/parameters/RfqIdPath' + - name: structured_target_id + in: path + required: true + description: Structured target ID + schema: + type: string responses: '200': - description: RFQ retrieved successfully + description: Structured target retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetRFQResponse' + $ref: '#/components/schemas/GetStructuredTargetResponse' '401': - $ref: '#/components/responses/UnauthorizedError' + description: Unauthorized '404': - $ref: '#/components/responses/NotFoundError' + description: Not found '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: DeleteRFQ - summary: Delete RFQ - description: ' Endpoint for deleting an RFQ by ID' + description: Internal server error + /milestones/{milestone_id}: + get: + operationId: GetMilestone + summary: Get Milestone + description: ' Endpoint for getting data about a specific milestone by its ID.' tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - milestone parameters: - - $ref: '#/components/parameters/RfqIdPath' + - name: milestone_id + in: path + required: true + description: Milestone ID + schema: + type: string responses: - '204': - description: RFQ deleted successfully + '200': + description: Milestone retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetMilestoneResponse' + '400': + description: Bad Request '401': - $ref: '#/components/responses/UnauthorizedError' + description: Unauthorized '404': - $ref: '#/components/responses/NotFoundError' + description: Not Found '500': - $ref: '#/components/responses/InternalServerError' - /communications/quotes: + description: Internal Server Error + /milestones: get: - operationId: GetQuotes - summary: Get Quotes - description: ' Endpoint for getting quotes' + operationId: GetMilestones + summary: Get Milestones + description: 'Minimum start date to filter milestones. Format: RFC3339 timestamp' tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] + - milestone parameters: - - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/MarketTickerQuery' - name: limit in: query - description: >- - Parameter to specify the number of results per page. Defaults to - 500. + description: Number of milestones to return per page + required: true schema: type: integer - format: int32 minimum: 1 maximum: 500 - default: 500 - - name: status + - name: minimum_start_date in: query - description: Filter quotes by status + description: Minimum start date to filter milestones. Format RFC3339 timestamp + required: false schema: type: string - x-go-type-skip-optional-pointer: true - - name: quote_creator_user_id + format: date-time + - name: category in: query - description: Filter quotes by quote creator user ID + description: >- + Filter by milestone category. E.g. Sports, Elections, Esports, + Crypto. + required: false schema: type: string - x-go-type-skip-optional-pointer: true - - name: rfq_creator_user_id + example: Sports + - name: competition in: query - description: Filter quotes by RFQ creator user ID + description: >- + Filter by competition. E.g. Pro Football, Pro Basketball (M), Pro + Baseball, Pro Hockey, College Football. + required: false schema: type: string - x-go-type-skip-optional-pointer: true - - name: rfq_creator_subtrader_id + example: Pro Football + - name: source_id in: query - description: Filter quotes by RFQ creator subtrader ID (FCM members only) + description: Filter by source id + required: false schema: type: string - x-go-type-skip-optional-pointer: true - - name: rfq_id + - name: type in: query - description: Filter quotes by RFQ ID + description: >- + Filter by milestone type. E.g. football_game, basketball_game, + soccer_tournament_multi_leg, baseball_game, hockey_match, + political_race. + required: false schema: type: string - x-go-type-skip-optional-pointer: true - responses: - '200': - description: Quotes retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/GetQuotesResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - post: - operationId: CreateQuote - summary: Create Quote - description: ' Endpoint for creating a quote in response to an RFQ' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateQuoteRequest' - responses: - '201': - description: Quote created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/CreateQuoteResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '500': - $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}: - get: - operationId: GetQuote - summary: Get Quote - description: ' Endpoint for getting a particular quote' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/QuoteIdPath' + example: football_game + - name: related_event_ticker + in: query + description: Filter by related event ticker + required: false + schema: + type: string + - name: cursor + in: query + description: >- + Pagination cursor. Use the cursor value returned from the previous + response to get the next page of results + required: false + schema: + type: string + - name: min_updated_ts + in: query + required: false + description: >- + Filter milestones with metadata updated after this Unix timestamp + (in seconds). Use this to efficiently poll for changes. + schema: + type: integer + format: int64 responses: '200': - description: Quote retrieved successfully + description: Milestones retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetQuoteResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' - delete: - operationId: DeleteQuote - summary: Delete Quote - description: ' Endpoint for deleting a quote, which means it can no longer be accepted.' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/QuoteIdPath' - responses: - '204': - description: Quote deleted successfully - '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}/accept: - put: - operationId: AcceptQuote - summary: Accept Quote - description: ' Endpoint for accepting a quote. This will require the quoter to confirm' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/QuoteIdPath' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AcceptQuoteRequest' - responses: - '204': - description: Quote accepted successfully + $ref: '#/components/schemas/GetMilestonesResponse' '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/InternalServerError' - /communications/quotes/{quote_id}/confirm: - put: - operationId: ConfirmQuote - summary: Confirm Quote - description: ' Endpoint for confirming a quote. This will start a timer for order execution' - tags: - - communications - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - parameters: - - $ref: '#/components/parameters/QuoteIdPath' - requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/EmptyResponse' - responses: - '204': - description: Quote confirmed successfully + description: Bad Request '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' + description: Unauthorized '500': - $ref: '#/components/responses/InternalServerError' + description: Internal Server Error /multivariate_event_collections/{collection_ticker}: get: operationId: GetMultivariateEventCollection @@ -3028,7 +3236,30 @@ paths: put: operationId: LookupTickersForMarketInMultivariateEventCollection summary: Lookup Tickers For Market In Multivariate Event Collection - description: ' Endpoint for looking up an individual market in a multivariate event collection. If CreateMarketInMultivariateEventCollection has never been hit with that variable combination before, this will return a 404.' + deprecated: true + description: >- + DEPRECATED: This endpoint predates RFQs and should not be used for new + integrations. Endpoint for looking up an individual market in a + multivariate event collection. If + CreateMarketInMultivariateEventCollection has never been hit with that + variable combination before, this will return a 404. + x-mint: + content: > + + + This endpoint is deprecated and predates RFQs. Do not use it for new + integrations. + + + + + + + **Rate limit:** 2 tokens per request. Other endpoints use the default + cost of 10 tokens per request unless noted on their own page. See + [Rate Limits and Tiers](/getting_started/rate_limits). + + tags: - multivariate security: @@ -3068,7 +3299,19 @@ paths: get: operationId: GetMultivariateEventCollectionLookupHistory summary: Get Multivariate Event Collection Lookup History - description: ' Endpoint for retrieving which markets in an event collection were recently looked up.' + deprecated: true + description: >- + DEPRECATED: This endpoint predates RFQs and should not be used for new + integrations. Endpoint for retrieving which markets in an event + collection were recently looked up. + x-mint: + content: > + + + This endpoint is deprecated and predates RFQs. Do not use it for new + integrations. + + tags: - multivariate parameters: @@ -3104,245 +3347,459 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' - /series/{series_ticker}: + /incentive_programs: get: - operationId: GetSeries - summary: Get Series - description: ' Endpoint for getting data about a specific series by its ticker. A series represents a template for recurring events that follow the same format and rules (e.g., "Monthly Jobs Report", "Weekly Initial Jobless Claims", "Daily Weather in NYC"). Series define the structure, settlement sources, and metadata that will be applied to each recurring event instance within that series.' + operationId: GetIncentivePrograms + summary: Get Incentives + description: ' List incentives with optional filters. Incentives are rewards programs for trading activity on specific markets.' tags: - - market + - incentive-programs parameters: - - name: series_ticker - in: path - required: true + - name: status + in: query + required: false + description: >- + Status filter. Can be "all", "active", "upcoming", "closed", or + "paid_out". Default is "all". schema: type: string - description: The ticker of the series to retrieve - - name: include_volume + enum: + - all + - active + - upcoming + - closed + - paid_out + - name: type in: query required: false + description: >- + Type filter. Can be "all", "liquidity", or "volume". Default is + "all". schema: - type: boolean - default: false - x-go-type-skip-optional-pointer: true + type: string + enum: + - all + - liquidity + - volume + - name: incentive_description + in: query + required: false + description: Filter by exact incentive description. + schema: + type: string + - name: limit + in: query + required: false + description: Number of results per page. Defaults to 100. Maximum value is 10000. + schema: + type: integer + minimum: 1 + maximum: 10000 + - name: cursor + in: query + required: false + description: Cursor for pagination + schema: + type: string + responses: + '200': + description: Incentive programs retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetIncentiveProgramsResponse' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /fcm/orders: + get: + operationId: GetFCMOrders + summary: Get FCM Orders + description: > + Endpoint for FCM members to get orders filtered by subtrader ID. + + This endpoint requires FCM member access level and allows filtering + orders by subtrader ID. + tags: + - fcm + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - name: subtrader_id + in: query + required: true description: >- - If true, includes the total volume traded across all events in this - series. + Restricts the response to orders for a specific subtrader (FCM + members only) + schema: + type: string + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/SingleEventTickerQuery' + - $ref: '#/components/parameters/TickerQuery' + - name: min_ts + in: query + description: >- + Restricts the response to orders after a timestamp, formatted as a + Unix Timestamp + schema: + type: integer + format: int64 + - name: max_ts + in: query + description: >- + Restricts the response to orders before a timestamp, formatted as a + Unix Timestamp + schema: + type: integer + format: int64 + - name: status + in: query + description: Restricts the response to orders that have a certain status + schema: + type: string + enum: + - resting + - canceled + - executed + - name: limit + in: query + description: Parameter to specify the number of results per page. Defaults to 100 + schema: + type: integer + minimum: 1 + maximum: 1000 + responses: + '200': + description: Orders retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetOrdersResponse' + '400': + description: Bad request + '401': + description: Unauthorized + '404': + description: Not found + '500': + description: Internal server error + /fcm/positions: + get: + operationId: GetFCMPositions + summary: Get FCM Positions + description: > + Endpoint for FCM members to get market positions filtered by subtrader + ID. + + This endpoint requires FCM member access level and allows filtering + positions by subtrader ID. + tags: + - fcm + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + parameters: + - name: subtrader_id + in: query + required: true + description: >- + Restricts the response to positions for a specific subtrader (FCM + members only) + schema: + type: string + - name: ticker + in: query + description: Ticker of desired positions + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: event_ticker + in: query + description: Event ticker of desired positions + schema: + type: string + x-go-type-skip-optional-pointer: true + - name: count_filter + in: query + description: >- + Restricts the positions to those with any of following fields with + non-zero values, as a comma separated list + schema: + type: string + - name: settlement_status + in: query + description: Settlement status of the markets to return. Defaults to unsettled + schema: + type: string + enum: + - all + - unsettled + - settled + - name: limit + in: query + description: Parameter to specify the number of results per page. Defaults to 100 + schema: + type: integer + minimum: 1 + maximum: 1000 + - name: cursor + in: query + description: >- + The Cursor represents a pointer to the next page of records in the + pagination + schema: + type: string + responses: + '200': + description: Positions retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetPositionsResponse' + '400': + description: Bad request + '401': + description: Unauthorized + '404': + description: Not found + '500': + description: Internal server error + /historical/cutoff: + get: + operationId: GetHistoricalCutoff + summary: Get Historical Cutoff Timestamps + description: > + Returns the cutoff timestamps that define the boundary between **live** + and **historical** data. + + + ## Cutoff fields + + - `market_settled_ts` : Markets that **settled** before this timestamp, + and their candlesticks, must be accessed via `GET /historical/markets` + and `GET /historical/markets/{ticker}/candlesticks`. + + - `trades_created_ts` : Trades that were **filled** before this + timestamp must be accessed via `GET /historical/fills`. + + - `orders_updated_ts` : Orders that were **canceled or fully executed** + before this timestamp must be accessed via `GET /historical/orders`. + Resting (active) orders are always available in `GET /portfolio/orders`. + tags: + - historical responses: '200': - description: Series retrieved successfully + description: Historical cutoff timestamps retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetSeriesResponse' - '400': - $ref: '#/components/responses/BadRequestError' + $ref: '#/components/schemas/GetHistoricalCutoffResponse' '500': - $ref: '#/components/responses/InternalServerError' - /series: + description: Internal server error + /historical/markets/{ticker}/candlesticks: get: - operationId: GetSeriesList - summary: Get Series List - description: ' Endpoint for getting data about multiple series with specified filters. A series represents a template for recurring events that follow the same format and rules (e.g., "Monthly Jobs Report", "Weekly Initial Jobless Claims", "Daily Weather in NYC"). This endpoint allows you to browse and discover available series templates by category.' + operationId: GetMarketCandlesticksHistorical + summary: Get Historical Market Candlesticks + description: ' Endpoint for fetching historical candlestick data for markets that have been archived from the live data set. Time period length of each candlestick in minutes. Valid values: 1 (1 minute), 60 (1 hour), 1440 (1 day).' tags: - - market + - historical parameters: - - name: category - in: query - required: false - schema: - type: string - x-go-type-skip-optional-pointer: true - - name: tags - in: query - required: false + - name: ticker + in: path + required: true + description: Market ticker - unique identifier for the specific market schema: type: string - x-go-type-skip-optional-pointer: true - - name: include_product_metadata + - name: start_ts in: query - required: false + required: true + description: >- + Start timestamp (Unix timestamp). Candlesticks will include those + ending on or after this time. schema: - type: boolean - default: false - x-go-type-skip-optional-pointer: true - - name: include_volume + type: integer + format: int64 + - name: end_ts in: query - required: false - schema: - type: boolean - default: false - x-go-type-skip-optional-pointer: true + required: true description: >- - If true, includes the total volume traded across all events in each - series. - - name: min_updated_ts + End timestamp (Unix timestamp). Candlesticks will include those + ending on or before this time. + schema: + type: integer + format: int64 + - name: period_interval in: query - required: false + required: true description: >- - Filter series with metadata updated after this Unix timestamp (in - seconds). Use this to efficiently poll for changes. + Time period length of each candlestick in minutes. Valid values are + 1 (1 minute), 60 (1 hour), or 1440 (1 day). schema: type: integer - format: int64 + enum: + - 1 + - 60 + - 1440 + x-oapi-codegen-extra-tags: + validate: required,oneof=1 60 1440 responses: '200': - description: Series list retrieved successfully + description: Candlesticks retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetSeriesListResponse' + $ref: '#/components/schemas/GetMarketCandlesticksHistoricalResponse' '400': - $ref: '#/components/responses/BadRequestError' + description: Bad request + '404': + description: Not found '500': - $ref: '#/components/responses/InternalServerError' - /markets: + description: Internal server error + /historical/fills: get: - operationId: GetMarkets - summary: Get Markets - description: > - Filter by market status. Possible values: `unopened`, `open`, `closed`, - `settled`. Leave empty to return markets with any status. - - Only one `status` filter may be supplied at a time. - - Timestamp filters will be mutually exclusive from other timestamp filters and certain status filters. - - | Compatible Timestamp Filters | Additional Status Filters| Extra Notes | - |------------------------------|--------------------------|-------------| - | min_created_ts, max_created_ts | `unopened`, `open`, *empty* | | - | min_close_ts, max_close_ts | `closed`, *empty* | | - | min_settled_ts, max_settled_ts | `settled`, *empty* | | - | min_updated_ts | *empty* | Incompatible with all filters besides `mve_filter=exclude` | - - Markets that settled before the historical cutoff are only available via `GET /historical/markets`. See [Historical Data](https://kalshi.com/docs/getting_started/historical_data) for details. + operationId: GetFillsHistorical + summary: Get Historical Fills + description: ' Endpoint for getting all historical fills for the member. A fill is when a trade you have is matched.' tags: - - market + - historical + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/MarketLimitQuery' + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/MaxTsQuery' + - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/CursorQuery' - - $ref: '#/components/parameters/SingleEventTickerQuery' - - $ref: '#/components/parameters/SeriesTickerQuery' - - $ref: '#/components/parameters/MinCreatedTsQuery' - - $ref: '#/components/parameters/MaxCreatedTsQuery' - - $ref: '#/components/parameters/MinUpdatedTsQuery' - - $ref: '#/components/parameters/MaxCloseTsQuery' - - $ref: '#/components/parameters/MinCloseTsQuery' - - $ref: '#/components/parameters/MinSettledTsQuery' - - $ref: '#/components/parameters/MaxSettledTsQuery' - - $ref: '#/components/parameters/MarketStatusQuery' - - $ref: '#/components/parameters/TickersQuery' - - $ref: '#/components/parameters/MveFilterQuery' responses: '200': - description: Markets retrieved successfully + description: Fills retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMarketsResponse' + $ref: '#/components/schemas/GetFillsResponse' '400': description: Bad request '401': description: Unauthorized + '404': + $ref: '#/components/responses/NotFoundError' '500': description: Internal server error - /markets/{ticker}: + /historical/orders: get: - operationId: GetMarket - summary: Get Market - description: ' Endpoint for getting data about a specific market by its ticker. A market represents a specific binary outcome within an event that users can trade on (e.g., "Will candidate X win?"). Markets have yes/no positions, current prices, volume, and settlement rules.' + operationId: GetHistoricalOrders + summary: Get Historical Orders + description: ' Endpoint for getting orders that have been archived to the historical database.' tags: - - market + - historical + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] parameters: - - $ref: '#/components/parameters/TickerPath' + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/MaxTsQuery' + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/CursorQuery' responses: '200': - description: Market retrieved successfully + description: Historical orders retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/GetMarketResponse' + $ref: '#/components/schemas/GetOrdersResponse' + '400': + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized - '404': - description: Not found + $ref: '#/components/responses/UnauthorizedError' '500': - description: Internal server error - /markets/candlesticks: + $ref: '#/components/responses/InternalServerError' + /historical/trades: get: - operationId: BatchGetMarketCandlesticks - summary: Batch Get Market Candlesticks - description: > - Endpoint for retrieving candlestick data for multiple markets. - - - - Accepts up to 100 market tickers per request - - - Returns up to 10,000 candlesticks total across all markets - - - Returns candlesticks grouped by market_id - - - Optionally includes a synthetic initial candlestick for price - continuity (see `include_latest_before_start` parameter) + operationId: GetTradesHistorical + summary: Get Historical Trades + description: ' Endpoint for getting all historical trades for all markets. Trades that were filled before the historical cutoff are available via this endpoint. See [Historical Data](https://docs.kalshi.com/getting_started/historical_data) for details.' tags: - - market + - historical parameters: - - name: market_tickers - in: query - required: true - description: Comma-separated list of market tickers (maximum 100) - schema: - type: string - - name: start_ts - in: query - required: true - description: Start timestamp in Unix seconds - schema: - type: integer - format: int64 - - name: end_ts - in: query - required: true - description: End timestamp in Unix seconds - schema: - type: integer - format: int64 - - name: period_interval - in: query - required: true - description: Candlestick period interval in minutes - schema: - type: integer - format: int32 - minimum: 1 - - name: include_latest_before_start - in: query - required: false - description: > - If true, prepends the latest candlestick available before the - start_ts. This synthetic candlestick is created by: - - 1. Finding the most recent real candlestick before start_ts - - 2. Projecting it forward to the first period boundary (calculated as - the next period interval after start_ts) - - 3. Setting all OHLC prices to null, and `previous_price` to the - close price from the real candlestick - schema: - type: boolean - default: false + - $ref: '#/components/parameters/TickerQuery' + - $ref: '#/components/parameters/MinTsQuery' + - $ref: '#/components/parameters/MaxTsQuery' + - $ref: '#/components/parameters/MarketLimitQuery' + - $ref: '#/components/parameters/CursorQuery' + responses: + '200': + description: Historical trades retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetTradesResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' + /historical/markets: + get: + operationId: GetHistoricalMarkets + summary: Get Historical Markets + description: > + Endpoint for getting markets that have been archived to the historical + database. Filters are mutually exclusive. + tags: + - historical + parameters: + - $ref: '#/components/parameters/MarketLimitQuery' + - $ref: '#/components/parameters/CursorQuery' + - $ref: '#/components/parameters/TickersQuery' + - $ref: '#/components/parameters/SingleEventTickerQuery' + - $ref: '#/components/parameters/SeriesTickerQuery' + - $ref: '#/components/parameters/MveHistoricalFilterQuery' responses: '200': - description: Market candlesticks retrieved successfully + description: Historical markets retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/BatchGetMarketCandlesticksResponse' + $ref: '#/components/schemas/GetMarketsResponse' '400': - description: Bad request - '401': - description: Unauthorized + $ref: '#/components/responses/BadRequestError' '500': - description: Internal server error + $ref: '#/components/responses/InternalServerError' + /historical/markets/{ticker}: + get: + operationId: GetHistoricalMarket + summary: Get Historical Market + description: ' Endpoint for getting data about a specific market by its ticker from the historical database.' + tags: + - historical + parameters: + - $ref: '#/components/parameters/TickerPath' + responses: + '200': + description: Historical market retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetMarketResponse' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/InternalServerError' components: securitySchemes: kalshiAccessKey: @@ -3398,7 +3855,10 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' RateLimitError: - description: Rate limit exceeded + description: >- + Rate limit exceeded. The default cost is 10 tokens per request; + endpoints that deviate show a **Rate limit** callout at the top of their + own page. See [Rate Limits and Tiers](/getting_started/rate_limits). content: application/json: schema: @@ -3416,6 +3876,18 @@ components: default: 100 x-oapi-codegen-extra-tags: validate: omitempty,min=1,max=1000 + WithdrawalLimitQuery: + name: limit + in: query + description: Number of results per page. Defaults to 100. Maximum value is 500. + schema: + type: integer + format: int64 + minimum: 1 + maximum: 500 + default: 100 + x-oapi-codegen-extra-tags: + validate: omitempty,min=1,max=500 MarketLimitQuery: name: limit in: query @@ -3556,6 +4028,12 @@ components: description: Subaccount number (0 for primary, 1-32 for subaccounts). Defaults to 0. schema: type: integer + ExchangeIndexQuery: + name: exchange_index + in: query + schema: + $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true OrderIdPath: name: order_id in: path @@ -3691,6 +4169,12 @@ components: contract count fields are legacy and will be deprecated; when both integer and fp fields are provided, they must match. example: '10.00' + ExchangeIndex: + type: integer + description: >- + Identifier for an exchange shard. Defaults to 0 if unspecified. Note: + currently only 0 supported. + example: 0 GetMarketCandlesticksHistoricalResponse: type: object required: @@ -3844,7 +4328,22 @@ components: enum: - taker_at_cross - maker - description: The self-trade prevention type for orders + description: > + The self-trade prevention type for orders. `taker_at_cross` cancels the + taker order when it would trade against another order from the same + user; execution stops and any partial fills already matched are + executed. `maker` cancels the resting maker order and continues + matching. + BookSide: + type: string + enum: + - bid + - ask + description: >- + Side of the book for an order or trade. For event markets, this refers + to the YES leg only: `bid` means buy YES, `ask` means sell YES. (Selling + YES is economically equivalent to buying NO at `1 - price`, but this + endpoint quotes everything from the YES side.) OrderStatus: type: string enum: @@ -3858,6 +4357,15 @@ components: - event_contract - margined description: The exchange instance type + UserFilter: + type: string + enum: + - self + x-enum-varnames: + - UserFilterSelf + description: >- + Omit or leave empty to return all results. Use `self` to filter by the + authenticated user. ApiKey: type: object required: @@ -4002,22 +4510,79 @@ components: description: Ordered list of sports for display items: type: string + BucketLimit: + type: object + description: | + Token-bucket budget for one rate-limit bucket. Each request deducts + tokens equal to its endpoint cost; the bucket refills at refill_rate + tokens per second up to bucket_capacity. A request is allowed if the + bucket holds enough tokens to cover its cost; otherwise the request + is rejected with HTTP 429. + required: + - refill_rate + - bucket_capacity + properties: + refill_rate: + type: integer + description: Tokens added to the bucket per second. + bucket_capacity: + type: integer + description: | + Maximum tokens the bucket can hold. When equal to refill_rate the + bucket holds one second of budget; larger values represent burst + headroom that idle clients accumulate and can spend in a single + pulse (e.g. write buckets at non-Basic tiers hold two seconds of + budget). GetAccountApiLimitsResponse: type: object required: - usage_tier - - read_limit - - write_limit + - read + - write properties: usage_tier: type: string - description: User's API usage tier - read_limit: + description: User's API usage tier. + read: + $ref: '#/components/schemas/BucketLimit' + write: + $ref: '#/components/schemas/BucketLimit' + EndpointTokenCost: + type: object + required: + - method + - path + - cost + properties: + method: + type: string + description: HTTP method for the endpoint. + path: + type: string + description: API route path for the endpoint. + cost: type: integer - description: Maximum read requests per second - write_limit: + description: >- + Configured token cost for an endpoint whose cost differs from the + default cost. + GetAccountEndpointCostsResponse: + type: object + required: + - default_cost + - endpoint_costs + properties: + default_cost: type: integer - description: Maximum write requests per second + description: >- + Default token cost applied to endpoints that are not listed in + `endpoint_costs`. This is currently 10. + endpoint_costs: + type: array + description: >- + API v2 endpoints whose configured token cost differs from + `default_cost`. Endpoints that use the default cost are omitted. + items: + $ref: '#/components/schemas/EndpointTokenCost' ExchangeStatus: type: object required: @@ -4473,10 +5038,21 @@ components: items: type: object additionalProperties: true + IndexedBalance: + type: object + required: + - exchange_index + - balance + properties: + exchange_index: + $ref: '#/components/schemas/ExchangeIndex' + balance: + $ref: '#/components/schemas/FixedPointDollars' GetBalanceResponse: type: object required: - balance + - balance_dollars - portfolio_value - updated_ts properties: @@ -4486,6 +5062,11 @@ components: description: >- Member's available balance in cents. This represents the amount available for trading. + balance_dollars: + $ref: '#/components/schemas/FixedPointDollars' + description: >- + Member's available balance as a fixed-point dollar string. This + represents the amount available for trading. portfolio_value: type: integer format: int64 @@ -4496,6 +5077,11 @@ components: type: integer format: int64 description: Unix timestamp of the last update to the balance. + balance_breakdown: + type: array + items: + $ref: '#/components/schemas/IndexedBalance' + description: Balance broken down per exchange index. CreateSubaccountResponse: type: object required: @@ -4716,6 +5302,130 @@ components: total_resting_order_value: type: integer description: Total value of resting orders in cents + GetDepositsResponse: + type: object + required: + - deposits + properties: + deposits: + type: array + items: + $ref: '#/components/schemas/Deposit' + cursor: + type: string + Deposit: + type: object + required: + - id + - status + - type + - amount_cents + - fee_cents + - created_ts + properties: + id: + type: string + description: Unique identifier for the deposit. + status: + type: string + enum: + - pending + - applied + - failed + - returned + description: >- + Current status of the deposit. 'applied' means funds are reflected + in balance. + type: + type: string + enum: + - ach + - wire + - crypto + - debit + - apm + description: Payment method used for the deposit. + amount_cents: + type: integer + format: int64 + description: Deposit amount in cents. + fee_cents: + type: integer + format: int64 + description: Fee charged for the deposit in cents. + created_ts: + type: integer + format: int64 + description: Unix timestamp of when the deposit was created. + finalized_ts: + type: integer + format: int64 + nullable: true + description: >- + Unix timestamp of when the deposit was finalized (applied, failed, + or returned). + GetWithdrawalsResponse: + type: object + required: + - withdrawals + properties: + withdrawals: + type: array + items: + $ref: '#/components/schemas/Withdrawal' + cursor: + type: string + Withdrawal: + type: object + required: + - id + - status + - type + - amount_cents + - fee_cents + - created_ts + properties: + id: + type: string + description: Unique identifier for the withdrawal. + status: + type: string + enum: + - pending + - applied + - failed + - returned + description: >- + Current status of the withdrawal. 'applied' means funds have been + deducted from balance. + type: + type: string + enum: + - ach + - wire + - crypto + - debit + - apm + description: Payment type used for the withdrawal. + amount_cents: + type: integer + format: int64 + description: Withdrawal amount in cents. + fee_cents: + type: integer + format: int64 + description: Fee charged for the withdrawal in cents. + created_ts: + type: integer + format: int64 + description: Unix timestamp of when the withdrawal was created. + finalized_ts: + type: integer + format: int64 + nullable: true + description: >- + Unix timestamp of when the withdrawal was finalized (applied, + failed, or returned). Order: type: object required: @@ -4725,6 +5435,8 @@ components: - ticker - side - action + - outcome_side + - book_side - type - status - yes_price_dollars @@ -4751,11 +5463,54 @@ components: enum: - 'yes' - 'no' + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. action: type: string enum: - buy - sell + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. + outcome_side: + type: string + enum: + - 'yes' + - 'no' + description: > + The outcome side this order is positioned for. buy-yes and sell-no + produce 'yes'; buy-no and sell-yes produce 'no'. + + + `outcome_side` describes directional exposure only; it does not + change the order's price. An order at price `p` with + `outcome_side=no` is matched by an order at the same price `p` with + `outcome_side=yes` — both parties trade at the same price, just on + opposite directions. + + + `outcome_side` and `book_side` will become the canonical way to + determine order direction. The legacy `action`, `side`, and `is_yes` + fields will be deprecated in a future release — please migrate to + these new fields. + book_side: + $ref: '#/components/schemas/BookSide' + description: > + Same directional bit as outcome_side in book vocabulary. 'bid' is + equivalent to outcome_side 'yes'; 'ask' is equivalent to + outcome_side 'no'. + + + `outcome_side` and `book_side` will become the canonical way to + determine order direction. The legacy `action`, `side`, and `is_yes` + fields will be deprecated in a future release — please migrate to + these new fields. type: type: string enum: @@ -4827,6 +5582,10 @@ components: nullable: true x-omitempty: true description: Subaccount number (0 for primary, 1-32 for subaccounts). + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true Milestone: type: object required: @@ -5071,6 +5830,8 @@ components: - yes_price_dollars - no_price_dollars - taker_side + - taker_outcome_side + - taker_book_side - created_time properties: trade_id: @@ -5098,7 +5859,47 @@ components: x-enum-varnames: - TradeTakerSideYes - TradeTakerSideNo - description: Side for the taker of this trade + deprecated: true + description: > + Deprecated. Use `taker_outcome_side` (or `taker_book_side`) instead. + See [Order direction](/getting_started/order_direction). This field + will not be removed before May 14, 2026. + taker_outcome_side: + type: string + enum: + - 'yes' + - 'no' + x-enum-varnames: + - TradeTakerOutcomeSideYes + - TradeTakerOutcomeSideNo + description: > + The outcome side the taker is positioned for. buy-yes and sell-no + produce 'yes'; buy-no and sell-yes produce 'no'. + + + `taker_outcome_side` describes directional exposure only; it does + not change the trade's price. A trade at price `p` with + `taker_outcome_side=no` is matched against the maker at the same + price `p` with the opposite direction — both parties trade at the + same price. + + + `taker_outcome_side` and `taker_book_side` will become the canonical + way to determine trade direction. The legacy `taker_side` field will + be deprecated in a future release — please migrate to these new + fields. + taker_book_side: + $ref: '#/components/schemas/BookSide' + description: > + Same directional bit as taker_outcome_side in book vocabulary. 'bid' + is equivalent to taker_outcome_side 'yes'; 'ask' is equivalent to + taker_outcome_side 'no'. + + + `taker_outcome_side` and `taker_book_side` will become the canonical + way to determine trade direction. The legacy `taker_side` field will + be deprecated in a future release — please migrate to these new + fields. created_time: type: string format: date-time @@ -5122,6 +5923,7 @@ components: - market_id - market_ticker - incentive_type + - incentive_description - start_date - end_date - period_reward @@ -5146,6 +5948,9 @@ components: - liquidity - volume description: Type of incentive program + incentive_description: + type: string + description: Plain text description of the incentive program start_date: type: string format: date-time @@ -5194,6 +5999,8 @@ components: - market_ticker - side - action + - outcome_side + - book_side - count_fp - yes_price_dollars - no_price_dollars @@ -5220,13 +6027,54 @@ components: enum: - 'yes' - 'no' - description: Specifies if this is a 'yes' or 'no' fill + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. action: type: string enum: - buy - sell - description: Specifies if this is a buy or sell order + deprecated: true + description: > + Deprecated. Use `outcome_side` (or `book_side`) instead. See [Order + direction](/getting_started/order_direction). This field will not be + removed before May 14, 2026. + outcome_side: + type: string + enum: + - 'yes' + - 'no' + description: > + The outcome side this fill positioned the user for. buy-yes and + sell-no produce 'yes'; buy-no and sell-yes produce 'no'. + + + `outcome_side` describes directional exposure only; it does not + change the fill's price. A fill at price `p` with `outcome_side=no` + is matched against an order at the same price `p` with + `outcome_side=yes` — both parties trade at the same price, just on + opposite directions. + + + `outcome_side` and `book_side` will become the canonical way to + determine fill direction. The legacy `action` and `side` fields will + be deprecated in a future release — please migrate to these new + fields. + book_side: + $ref: '#/components/schemas/BookSide' + description: > + Same directional bit as outcome_side in book vocabulary. 'bid' is + equivalent to outcome_side 'yes'; 'ask' is equivalent to + outcome_side 'no'. + + + `outcome_side` and `book_side` will become the canonical way to + determine fill direction. The legacy `action` and `side` fields will + be deprecated in a future release — please migrate to these new + fields. count_fp: $ref: '#/components/schemas/FixedPointCount' description: >- @@ -5341,6 +6189,16 @@ components: type: integer format: int64 description: The amount to transfer in centicents + source_exchange_shard: + type: integer + default: 0 + x-go-type-skip-optional-pointer: true + description: Source exchange shard index (default 0) + destination_exchange_shard: + type: integer + default: 0 + x-go-type-skip-optional-pointer: true + description: Destination exchange shard index (default 0) IntraExchangeInstanceTransferResponse: type: object required: @@ -5369,6 +6227,10 @@ components: type: boolean description: Whether auto-cancel is enabled for this order group x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true GetOrderGroupsResponse: type: object properties: @@ -5398,6 +6260,10 @@ components: type: string description: List of order IDs that belong to this order group x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true CreateOrderGroupRequest: type: object properties: @@ -5429,6 +6295,11 @@ components: matched within this group over a rolling 15-second window. Provide contracts_limit or contracts_limit_fp; if both provided they must match. + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true UpdateOrderGroupLimitRequest: type: object properties: @@ -5456,10 +6327,22 @@ components: type: object required: - order_group_id + - subaccount properties: order_group_id: type: string description: The unique identifier for the created order group + subaccount: + type: integer + minimum: 0 + description: >- + Subaccount number that owns the created order group (0 for primary, + 1-32 for subaccounts). + x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true GetCommunicationsIDResponse: type: object required: @@ -5527,6 +6410,11 @@ components: type: string description: User ID of the RFQ creator (private field) x-go-type-skip-optional-pointer: true + creator_subaccount: + type: integer + description: >- + Subaccount number of the RFQ creator (visible when the caller is the + RFQ creator) cancelled_ts: type: string format: date-time @@ -5700,6 +6588,11 @@ components: rest_remainder: type: boolean description: Whether to rest the remainder of the quote after execution + post_only: + type: boolean + description: >- + Whether the quote creator's order is post-only (visible when the + caller is the quote creator) cancellation_reason: type: string description: Reason for quote cancellation if cancelled @@ -5723,6 +6616,16 @@ components: type: string description: Order ID for the quote creator (private field) x-go-type-skip-optional-pointer: true + creator_subaccount: + type: integer + description: >- + Subaccount number of the quote creator (visible when the caller is + the quote creator) + rfq_creator_subaccount: + type: integer + description: >- + Subaccount number of the RFQ creator (visible when the caller is the + RFQ creator) yes_contracts_fp: $ref: '#/components/schemas/FixedPointCount' description: Number of YES contracts offered in the quote (fixed-point) @@ -5773,6 +6676,11 @@ components: rest_remainder: type: boolean description: Whether to rest the remainder of the quote after execution + post_only: + type: boolean + description: >- + If true, the quote creator's resting order will be cancelled rather + than crossed if it would take liquidity. Defaults to false. subaccount: type: integer description: >- @@ -5866,8 +6774,29 @@ components: expiration_ts: type: integer format: int64 + description: > + Optional Unix timestamp in seconds for when the order expires. To + place + + an expiring order, set `time_in_force` to `good_till_canceled` and + + provide this `expiration_ts`. `GTT` is an internal execution type + and is + + not a valid API value for `time_in_force`. The `immediate_or_cancel` + + time-in-force value cannot be combined with `expiration_ts`. time_in_force: type: string + description: > + Specifies how long the order remains active. Use + `good_till_canceled` + + with `expiration_ts` for an order that should rest until a specific + + expiration time; without `expiration_ts`, `good_till_canceled` is a + + true good-till-canceled order. `GTT` is not a valid API value. enum: - fill_or_kill - good_till_canceled @@ -5912,6 +6841,11 @@ components: The subaccount number to use for this order. 0 is the primary subaccount. x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true CreateOrderResponse: type: object required: @@ -5985,6 +6919,11 @@ components: Optional subaccount number to use for this cancellation (0 for primary, 1-32 for subaccounts) x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true BatchCancelOrdersResponse: type: object required: @@ -6105,6 +7044,11 @@ components: String representation of the updated quantity for the order. If updating quantity, provide count or count_fp; if both provided they must match. + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true AmendOrderResponse: type: object required: @@ -6160,6 +7104,11 @@ components: Reduce-to may be provided via reduce_to or reduce_to_fp; if both provided they must match. Exactly one of reduce_by(/reduce_by_fp) or reduce_to(/reduce_to_fp) must be provided. + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true DecreaseOrderResponse: type: object required: @@ -6167,6 +7116,427 @@ components: properties: order: $ref: '#/components/schemas/Order' + CreateOrderV2Request: + type: object + required: + - ticker + - client_order_id + - side + - count + - price + - time_in_force + - self_trade_prevention_type + properties: + ticker: + type: string + x-oapi-codegen-extra-tags: + validate: required,min=1 + client_order_id: + type: string + x-go-type-skip-optional-pointer: true + side: + $ref: '#/components/schemas/BookSide' + x-oapi-codegen-extra-tags: + validate: required,oneof=bid ask + count: + $ref: '#/components/schemas/FixedPointCount' + description: String representation of the order quantity in contracts. + price: + $ref: '#/components/schemas/FixedPointDollars' + description: Price for the order in fixed-point dollars. + x-go-type-skip-optional-pointer: true + expiration_time: + type: integer + format: int64 + description: > + Optional Unix timestamp in seconds for when the order expires. To + place + + an expiring order, set `time_in_force` to `good_till_canceled` and + + provide this `expiration_time`. `GTT` is an internal execution type + and + + is not a valid API value for `time_in_force`. The + + `immediate_or_cancel` time-in-force value cannot be combined with + + `expiration_time`. + time_in_force: + type: string + description: > + Specifies how long the order remains active. Use + `good_till_canceled` + + with `expiration_time` for an order that should rest until a + specific + + expiration time; without `expiration_time`, `good_till_canceled` is + a + + true good-till-canceled order. `GTT` is not a valid API value. + enum: + - fill_or_kill + - good_till_canceled + - immediate_or_cancel + x-oapi-codegen-extra-tags: + validate: required,oneof=fill_or_kill good_till_canceled immediate_or_cancel + x-go-type-skip-optional-pointer: true + post_only: + type: boolean + self_trade_prevention_type: + allOf: + - $ref: '#/components/schemas/SelfTradePreventionType' + x-oapi-codegen-extra-tags: + validate: required,oneof=taker_at_cross maker + x-go-type-skip-optional-pointer: true + cancel_order_on_pause: + type: boolean + description: >- + If this flag is set to true, the order will be canceled if the order + is open and trading on the exchange is paused for any reason. + reduce_only: + type: boolean + description: >- + Specifies whether the order place count should be capped by the + member's current position. + subaccount: + type: integer + minimum: 0 + default: 0 + description: >- + The subaccount number to use for this order. 0 is the primary + subaccount. + x-go-type-skip-optional-pointer: true + order_group_id: + type: string + description: The order group this order is part of + x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true + CreateOrderV2Response: + type: object + required: + - order_id + - fill_count + - remaining_count + - ts_ms + properties: + order_id: + type: string + client_order_id: + type: string + fill_count: + $ref: '#/components/schemas/FixedPointCount' + description: Number of contracts filled immediately upon placement. + remaining_count: + $ref: '#/components/schemas/FixedPointCount' + description: >- + Number of contracts remaining after placement. For IOC orders, this + reflects the final state after unfilled contracts are canceled. + average_fill_price: + $ref: '#/components/schemas/FixedPointDollars' + description: >- + Volume-weighted average fill price. Only present when fill_count > + 0. + average_fee_paid: + $ref: '#/components/schemas/FixedPointDollars' + description: >- + Volume-weighted average fee paid per contract for fills resulting + from this request. Only present when fill_count > 0. + ts_ms: + type: integer + format: int64 + description: >- + Matching engine timestamp at which the order was processed, as Unix + epoch milliseconds. + CancelOrderV2Response: + type: object + required: + - order_id + - reduced_by + - ts_ms + properties: + order_id: + type: string + client_order_id: + type: string + reduced_by: + $ref: '#/components/schemas/FixedPointCount' + description: >- + Number of contracts that were canceled (i.e. the remaining count at + time of cancellation). + ts_ms: + type: integer + format: int64 + description: >- + Matching engine timestamp at which the cancellation was processed, + as Unix epoch milliseconds. + DecreaseOrderV2Request: + type: object + properties: + reduce_by: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + description: >- + String representation of the number of contracts to reduce by. + Exactly one of `reduce_by` or `reduce_to` must be provided. + reduce_to: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + description: >- + String representation of the number of contracts to reduce to. + Exactly one of `reduce_by` or `reduce_to` must be provided. + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true + DecreaseOrderV2Response: + type: object + required: + - order_id + - remaining_count + - ts_ms + properties: + order_id: + type: string + client_order_id: + type: string + remaining_count: + $ref: '#/components/schemas/FixedPointCount' + description: Number of contracts remaining after the decrease. + ts_ms: + type: integer + format: int64 + description: >- + Matching engine timestamp at which the decrease was processed, as + Unix epoch milliseconds. + AmendOrderV2Request: + type: object + required: + - ticker + - side + - price + - count + properties: + ticker: + type: string + description: Market ticker + x-oapi-codegen-extra-tags: + validate: required,min=1 + side: + $ref: '#/components/schemas/BookSide' + description: Side of the order + x-oapi-codegen-extra-tags: + validate: required,oneof=bid ask + price: + $ref: '#/components/schemas/FixedPointDollars' + description: Updated price for the order in fixed-point dollars. + x-go-type-skip-optional-pointer: true + count: + $ref: '#/components/schemas/FixedPointCount' + description: >- + Updated total/max fillable count for the order. Set this to the + order's already filled count plus the desired resting remaining + count after the amend. + x-go-type-skip-optional-pointer: true + client_order_id: + type: string + description: The original client-specified order ID to be amended + x-go-type-skip-optional-pointer: true + updated_client_order_id: + type: string + description: The new client-specified order ID after amendment + x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true + AmendOrderV2Response: + type: object + required: + - order_id + - ts_ms + properties: + order_id: + type: string + client_order_id: + type: string + remaining_count: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + x-omitempty: false + description: >- + Number of resting contracts remaining after the amend. This is the + actual post-amend resting quantity, not the request's total/max + fillable count. Only present when the amend caused a fill or changed + the resting size. + fill_count: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + x-omitempty: false + description: >- + Number of contracts filled as a result of the amend crossing the + book. Only present when fills occurred or remaining size changed. + average_fill_price: + $ref: '#/components/schemas/FixedPointDollars' + nullable: true + x-omitempty: false + description: >- + Volume-weighted average fill price for fills resulting from the + amend. Only present when fills occurred. + average_fee_paid: + $ref: '#/components/schemas/FixedPointDollars' + nullable: true + x-omitempty: false + description: >- + Volume-weighted average fee paid per contract for fills resulting + from the amend. Only present when fills occurred. + ts_ms: + type: integer + format: int64 + description: >- + Matching engine timestamp at which the amend was processed, as Unix + epoch milliseconds. + BatchCreateOrdersV2Request: + type: object + required: + - orders + properties: + orders: + type: array + x-oapi-codegen-extra-tags: + validate: required,dive + items: + $ref: '#/components/schemas/CreateOrderV2Request' + BatchCreateOrdersV2Response: + type: object + required: + - orders + properties: + orders: + type: array + items: + type: object + properties: + order_id: + type: string + client_order_id: + type: string + nullable: true + fill_count: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + x-omitempty: false + description: Number of contracts filled immediately upon placement. + remaining_count: + $ref: '#/components/schemas/FixedPointCount' + nullable: true + x-omitempty: false + description: Number of contracts remaining after placement. + average_fill_price: + $ref: '#/components/schemas/FixedPointDollars' + nullable: true + x-omitempty: false + description: >- + Volume-weighted average fill price. Only present when + fill_count > 0. + average_fee_paid: + $ref: '#/components/schemas/FixedPointDollars' + nullable: true + x-omitempty: false + description: >- + Volume-weighted average fee paid per contract. Only present + when fill_count > 0. + ts_ms: + type: integer + format: int64 + nullable: true + x-omitempty: false + description: >- + Matching engine timestamp at which the order was processed, as + Unix epoch milliseconds. Absent when the request errored. + error: + allOf: + - $ref: '#/components/schemas/ErrorResponse' + nullable: true + BatchCancelOrdersV2Request: + type: object + required: + - orders + properties: + orders: + type: array + x-oapi-codegen-extra-tags: + validate: required,dive + description: >- + An array of orders to cancel, each optionally specifying a + subaccount. + items: + type: object + required: + - order_id + properties: + order_id: + type: string + description: Order ID to cancel. + subaccount: + type: integer + minimum: 0 + default: 0 + description: >- + Optional subaccount number to use for this cancellation (0 for + primary, 1-32 for subaccounts). + x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + default: 0 + x-go-type-skip-optional-pointer: true + BatchCancelOrdersV2Response: + type: object + required: + - orders + properties: + orders: + type: array + items: + type: object + required: + - order_id + - reduced_by + properties: + order_id: + type: string + description: >- + The order ID identifying which order this entry corresponds + to. + client_order_id: + type: string + nullable: true + reduced_by: + $ref: '#/components/schemas/FixedPointCount' + description: >- + Number of contracts that were canceled (i.e. the remaining + count at time of cancellation). Zero if the cancel errored. + ts_ms: + type: integer + format: int64 + nullable: true + x-omitempty: false + description: >- + Matching engine timestamp at which the cancellation was + processed, as Unix epoch milliseconds. Absent when the cancel + errored. + error: + allOf: + - $ref: '#/components/schemas/ErrorResponse' + nullable: true AssociatedEvent: type: object required: @@ -6632,7 +8002,7 @@ components: percentile: type: integer format: int32 - description: The percentile value (0-10000). + description: The percentile value (0-9999). raw_numerical_forecast: type: number description: The raw numerical forecast value. @@ -6719,6 +8089,25 @@ components: type: string format: date-time description: Timestamp of when this event's metadata was last updated. + fee_type_override: + type: string + nullable: true + x-omitempty: true + description: >- + Fee type override for this event. When present, takes precedence + over the series-level fee for this event's markets. + fee_multiplier_override: + type: number + format: double + nullable: true + x-omitempty: true + description: >- + Fee multiplier override for this event. Paired with + fee_type_override. + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true Series: type: object required: @@ -7096,6 +8485,10 @@ components: type: boolean fractional_trading_enabled: type: boolean + deprecated: true + description: >- + Deprecated. This flag is always `true` and carries no information. + Will be removed after a pre-announcement with the removal date. open_interest_fp: $ref: '#/components/schemas/FixedPointCount' description: >- @@ -7162,11 +8555,6 @@ components: x-omitempty: true description: The condition under which the market can close early x-go-type-skip-optional-pointer: true - tick_size: - type: integer - deprecated: true - x-go-type-skip-optional-pointer: true - description: 'DEPRECATED: Use price_level_structure and price_ranges instead.' strike_type: type: string enum: @@ -7241,6 +8629,10 @@ components: If true, the market may be removed after determination if there is no activity on it x-go-type-skip-optional-pointer: true + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true tags: - name: api-keys description: API key management endpoints diff --git a/tests/_contract_support.py b/tests/_contract_support.py index 95bebee..d336b07 100644 --- a/tests/_contract_support.py +++ b/tests/_contract_support.py @@ -128,6 +128,11 @@ class Exclusion: http_method="GET", path_template="/account/limits", ), + MethodEndpointEntry( + sdk_method="kalshi.resources.account.AccountResource.endpoint_costs", + http_method="GET", + path_template="/account/endpoint_costs", + ), # ── api keys ──────────────────────────────────────────────────────────── MethodEndpointEntry( sdk_method="kalshi.resources.api_keys.ApiKeysResource.list", @@ -367,6 +372,42 @@ class Exclusion: http_method="GET", path_template="/portfolio/orders/{order_id}/queue_position", ), + # ── orders v2 (event-market, spec v3.18.0) ────────────────────────────── + MethodEndpointEntry( + sdk_method="kalshi.resources.orders.OrdersResource.create_v2", + http_method="POST", + path_template="/portfolio/events/orders", + request_body_schema="#/components/schemas/CreateOrderV2Request", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.orders.OrdersResource.cancel_v2", + http_method="DELETE", + path_template="/portfolio/events/orders/{order_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.orders.OrdersResource.amend_v2", + http_method="POST", + path_template="/portfolio/events/orders/{order_id}/amend", + request_body_schema="#/components/schemas/AmendOrderV2Request", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.orders.OrdersResource.decrease_v2", + http_method="POST", + path_template="/portfolio/events/orders/{order_id}/decrease", + request_body_schema="#/components/schemas/DecreaseOrderV2Request", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.orders.OrdersResource.batch_create_v2", + http_method="POST", + path_template="/portfolio/events/orders/batched", + request_body_schema="#/components/schemas/BatchCreateOrdersV2Request", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.orders.OrdersResource.batch_cancel_v2", + http_method="DELETE", + path_template="/portfolio/events/orders/batched", + request_body_schema="#/components/schemas/BatchCancelOrdersV2Request", + ), # ── order groups ──────────────────────────────────────────────────────── MethodEndpointEntry( sdk_method="kalshi.resources.order_groups.OrderGroupsResource.list", @@ -538,6 +579,26 @@ class Exclusion: http_method="GET", path_template="/portfolio/summary/total_resting_order_value", ), + MethodEndpointEntry( + sdk_method="kalshi.resources.portfolio.PortfolioResource.deposits", + http_method="GET", + path_template="/portfolio/deposits", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.portfolio.PortfolioResource.deposits_all", + http_method="GET", + path_template="/portfolio/deposits", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.portfolio.PortfolioResource.withdrawals", + http_method="GET", + path_template="/portfolio/withdrawals", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.portfolio.PortfolioResource.withdrawals_all", + http_method="GET", + path_template="/portfolio/withdrawals", + ), # ── series ────────────────────────────────────────────────────────────── MethodEndpointEntry( sdk_method="kalshi.resources.series.SeriesResource.list", @@ -759,6 +820,14 @@ class Exclusion: reason="paginator-handled; not a caller-facing kwarg on list_all", kind="paginator_handled", ), + ("kalshi.resources.portfolio.PortfolioResource.deposits_all", "cursor"): Exclusion( + reason="paginator-handled; not a caller-facing kwarg on list_all", + kind="paginator_handled", + ), + ("kalshi.resources.portfolio.PortfolioResource.withdrawals_all", "cursor"): Exclusion( + reason="paginator-handled; not a caller-facing kwarg on list_all", + kind="paginator_handled", + ), # --- batch_cancel body param (not a query/path param) --- ("kalshi.resources.orders.OrdersResource.batch_cancel", "orders"): Exclusion( reason="body param (BatchCancelOrdersRequest.orders); not query/path", @@ -776,6 +845,13 @@ class Exclusion: reason="Optional request-model overload; not a spec field. See #56.", kind="body_param", ), + ("kalshi.resources.orders.OrdersResource.batch_cancel_v2", "request"): Exclusion( + reason=( + "Required request-model kwarg for the model-only V2 surface; " + "not a spec field. Mirrors batch_cancel." + ), + kind="body_param", + ), # --- AmendOrderRequest spec fields deliberately not on the model --- ("kalshi.models.orders.AmendOrderRequest", "yes_price"): Exclusion( reason=( @@ -1057,6 +1133,8 @@ class Exclusion: "kalshi.resources.communications.CommunicationsResource.list_all_quotes", "kalshi.resources.subaccounts.SubaccountsResource.list_all_transfers", "kalshi.resources.portfolio.PortfolioResource.settlements_all", + "kalshi.resources.portfolio.PortfolioResource.deposits_all", + "kalshi.resources.portfolio.PortfolioResource.withdrawals_all", "kalshi.resources.fcm.FcmResource.orders_all", "kalshi.resources.incentive_programs.IncentiveProgramsResource.list_all", "kalshi.resources.structured_targets.StructuredTargetsResource.list_all", @@ -1077,6 +1155,8 @@ class Exclusion: "kalshi.resources.communications.AsyncCommunicationsResource.list_all_quotes", "kalshi.resources.subaccounts.AsyncSubaccountsResource.list_all_transfers", "kalshi.resources.portfolio.AsyncPortfolioResource.settlements_all", + "kalshi.resources.portfolio.AsyncPortfolioResource.deposits_all", + "kalshi.resources.portfolio.AsyncPortfolioResource.withdrawals_all", "kalshi.resources.fcm.AsyncFcmResource.orders_all", "kalshi.resources.incentive_programs.AsyncIncentiveProgramsResource.list_all", "kalshi.resources.structured_targets.AsyncStructuredTargetsResource.list_all", diff --git a/tests/integration/test_account.py b/tests/integration/test_account.py index b2065a2..0116619 100644 --- a/tests/integration/test_account.py +++ b/tests/integration/test_account.py @@ -6,11 +6,11 @@ from kalshi.async_client import AsyncKalshiClient from kalshi.client import KalshiClient -from kalshi.models.account import AccountApiLimits +from kalshi.models.account import AccountApiLimits, AccountEndpointCosts from tests.integration.assertions import assert_model_fields from tests.integration.coverage_harness import register -register("AccountResource", ["limits"]) +register("AccountResource", ["endpoint_costs", "limits"]) @pytest.mark.integration @@ -25,6 +25,15 @@ def test_limits(self, sync_client: KalshiClient) -> None: assert result.write.refill_rate > 0 assert result.usage_tier + def test_endpoint_costs(self, sync_client: KalshiClient) -> None: + result = sync_client.account.endpoint_costs() + assert isinstance(result, AccountEndpointCosts) + assert result.default_cost > 0 + for entry in result.endpoint_costs: + assert entry.method + assert entry.path + assert entry.cost >= 0 + @pytest.mark.integration class TestAccountAsync: @@ -32,3 +41,8 @@ async def test_limits(self, async_client: AsyncKalshiClient) -> None: result = await async_client.account.limits() assert isinstance(result, AccountApiLimits) assert_model_fields(result) + + async def test_endpoint_costs(self, async_client: AsyncKalshiClient) -> None: + result = await async_client.account.endpoint_costs() + assert isinstance(result, AccountEndpointCosts) + assert result.default_cost > 0 diff --git a/tests/integration/test_orders.py b/tests/integration/test_orders.py index 4483736..f9e176f 100644 --- a/tests/integration/test_orders.py +++ b/tests/integration/test_orders.py @@ -23,11 +23,17 @@ "OrdersResource", [ "amend", + "amend_v2", "batch_cancel", + "batch_cancel_v2", "batch_create", + "batch_create_v2", "cancel", + "cancel_v2", "create", + "create_v2", "decrease", + "decrease_v2", "fills", "fills_all", "get", diff --git a/tests/integration/test_portfolio.py b/tests/integration/test_portfolio.py index 7b460ca..50314a1 100644 --- a/tests/integration/test_portfolio.py +++ b/tests/integration/test_portfolio.py @@ -9,9 +9,11 @@ from kalshi.models.common import Page from kalshi.models.portfolio import ( Balance, + Deposit, PositionsResponse, Settlement, TotalRestingOrderValue, + Withdrawal, ) from tests.integration.assertions import assert_model_fields from tests.integration.coverage_harness import register @@ -20,10 +22,14 @@ "PortfolioResource", [ "balance", + "deposits", + "deposits_all", "positions", "settlements", "settlements_all", "total_resting_order_value", + "withdrawals", + "withdrawals_all", ], ) @@ -35,6 +41,8 @@ def test_balance(self, sync_client: KalshiClient) -> None: assert isinstance(result, Balance) assert_model_fields(result) assert isinstance(result.balance, int) + # spec v3.18.0 added balance_dollars as a required field + assert result.balance_dollars is not None def test_positions(self, sync_client: KalshiClient) -> None: result = sync_client.portfolio.positions() @@ -66,6 +74,34 @@ def test_total_resting_order_value(self, sync_client: KalshiClient) -> None: assert_model_fields(result) assert isinstance(result.total_resting_order_value, int) + def test_deposits(self, sync_client: KalshiClient) -> None: + page = sync_client.portfolio.deposits(limit=5) + assert isinstance(page, Page) + for item in page.items: + assert isinstance(item, Deposit) + assert_model_fields(item) + + def test_deposits_all(self, sync_client: KalshiClient) -> None: + for count, deposit in enumerate(sync_client.portfolio.deposits_all(limit=2)): + assert isinstance(deposit, Deposit) + assert_model_fields(deposit) + if count >= 2: + break + + def test_withdrawals(self, sync_client: KalshiClient) -> None: + page = sync_client.portfolio.withdrawals(limit=5) + assert isinstance(page, Page) + for item in page.items: + assert isinstance(item, Withdrawal) + assert_model_fields(item) + + def test_withdrawals_all(self, sync_client: KalshiClient) -> None: + for count, w in enumerate(sync_client.portfolio.withdrawals_all(limit=2)): + assert isinstance(w, Withdrawal) + assert_model_fields(w) + if count >= 2: + break + @pytest.mark.integration class TestPortfolioAsync: @@ -102,3 +138,35 @@ async def test_total_resting_order_value( """Auth-gated on demo (403 for non-FCM accounts).""" result = await async_client.portfolio.total_resting_order_value() assert isinstance(result, TotalRestingOrderValue) + + async def test_deposits(self, async_client: AsyncKalshiClient) -> None: + page = await async_client.portfolio.deposits(limit=5) + assert isinstance(page, Page) + for item in page.items: + assert isinstance(item, Deposit) + assert_model_fields(item) + + async def test_deposits_all(self, async_client: AsyncKalshiClient) -> None: + count = 0 + async for deposit in async_client.portfolio.deposits_all(limit=2): + assert isinstance(deposit, Deposit) + assert_model_fields(deposit) + count += 1 + if count >= 3: + break + + async def test_withdrawals(self, async_client: AsyncKalshiClient) -> None: + page = await async_client.portfolio.withdrawals(limit=5) + assert isinstance(page, Page) + for item in page.items: + assert isinstance(item, Withdrawal) + assert_model_fields(item) + + async def test_withdrawals_all(self, async_client: AsyncKalshiClient) -> None: + count = 0 + async for w in async_client.portfolio.withdrawals_all(limit=2): + assert isinstance(w, Withdrawal) + assert_model_fields(w) + count += 1 + if count >= 3: + break diff --git a/tests/test_account.py b/tests/test_account.py index 64dfcf2..257cd30 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -117,3 +117,90 @@ async def test_server_rejects_auth( ) with pytest.raises(KalshiAuthError): await async_account.limits() + + +class TestAccountEndpointCosts: + @respx.mock + def test_returns_costs(self, account: AccountResource) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/account/endpoint_costs", + ).mock( + return_value=httpx.Response( + 200, + json={ + "default_cost": 10, + "endpoint_costs": [ + {"method": "POST", "path": "/portfolio/orders", "cost": 50}, + {"method": "DELETE", "path": "/portfolio/orders/{order_id}", "cost": 2}, + ], + }, + ) + ) + costs = account.endpoint_costs() + assert costs.default_cost == 10 + assert len(costs.endpoint_costs) == 2 + assert costs.endpoint_costs[0].method == "POST" + assert costs.endpoint_costs[0].path == "/portfolio/orders" + assert costs.endpoint_costs[0].cost == 50 + + @respx.mock + def test_returns_empty_costs(self, account: AccountResource) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/account/endpoint_costs", + ).mock( + return_value=httpx.Response( + 200, json={"default_cost": 10, "endpoint_costs": []}, + ) + ) + costs = account.endpoint_costs() + assert costs.endpoint_costs == [] + + def test_requires_auth(self, unauth_account: AccountResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_account.endpoint_costs() + + +class TestAsyncAccountEndpointCosts: + @respx.mock + @pytest.mark.asyncio + async def test_returns_costs( + self, async_account: AsyncAccountResource, + ) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/account/endpoint_costs", + ).mock( + return_value=httpx.Response( + 200, + json={ + "default_cost": 10, + "endpoint_costs": [ + {"method": "POST", "path": "/portfolio/orders/batched", "cost": 100}, + ], + }, + ) + ) + costs = await async_account.endpoint_costs() + assert costs.default_cost == 10 + assert costs.endpoint_costs[0].cost == 100 + + @respx.mock + @pytest.mark.asyncio + async def test_returns_empty_costs( + self, async_account: AsyncAccountResource, + ) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/account/endpoint_costs", + ).mock( + return_value=httpx.Response( + 200, json={"default_cost": 10, "endpoint_costs": []}, + ) + ) + costs = await async_account.endpoint_costs() + assert costs.endpoint_costs == [] + + @pytest.mark.asyncio + async def test_requires_auth( + self, unauth_async_account: AsyncAccountResource, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_async_account.endpoint_costs() diff --git a/tests/test_async_orders.py b/tests/test_async_orders.py index aee029c..d0a648c 100644 --- a/tests/test_async_orders.py +++ b/tests/test_async_orders.py @@ -14,8 +14,22 @@ from kalshi.async_client import AsyncKalshiClient from kalshi.auth import KalshiAuth from kalshi.config import KalshiConfig -from kalshi.errors import KalshiNotFoundError, KalshiValidationError -from kalshi.models.orders import AmendOrderResponse, CreateOrderRequest +from kalshi.errors import ( + AuthRequiredError, + KalshiError, + KalshiNotFoundError, + KalshiValidationError, +) +from kalshi.models.orders import ( + AmendOrderResponse, + AmendOrderV2Request, + BatchCancelOrdersV2Request, + BatchCancelOrdersV2RequestOrder, + BatchCreateOrdersV2Request, + CreateOrderRequest, + CreateOrderV2Request, + DecreaseOrderV2Request, +) from kalshi.resources.orders import AsyncOrdersResource @@ -1137,3 +1151,401 @@ async def test_wraps_orders_key(self, orders: AsyncOrdersResource) -> None: assert "orders" in body assert len(body["orders"]) == 2 assert set(body.keys()) == {"orders"} + + +# ── V2 event-market orders (spec v3.18.0) ─────────────────── + + +class TestAsyncCreateOrderV2: + @respx.mock + @pytest.mark.asyncio + async def test_returns_response( + self, orders: AsyncOrdersResource, + ) -> None: + respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders", + ).mock( + return_value=httpx.Response( + 201, + json={ + "order_id": "ord-v2-1", + "fill_count": "0", + "remaining_count": "10", + "ts_ms": 1700000000000, + }, + ) + ) + result = await orders.create_v2( + request=CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ), + ) + assert result.order_id == "ord-v2-1" + + @respx.mock + @pytest.mark.asyncio + async def test_serializes_body( + self, orders: AsyncOrdersResource, + ) -> None: + """Async parity with TestCreateOrderV2.test_serializes_body — + guards DollarDecimal / FixedPointCount mode="json" serialization + on the async dispatch path. + """ + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders", + ).mock( + return_value=httpx.Response( + 201, + json={ + "order_id": "ord-v2-1", + "fill_count": "0", + "remaining_count": "10", + "ts_ms": 0, + }, + ) + ) + await orders.create_v2( + request=CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + exchange_index=0, + ), + ) + body = json.loads(route.calls[0].request.content) + assert body == { + "ticker": "MKT-A", + "client_order_id": "cli-1", + "side": "bid", + "count": "10", + "price": "0.50", + "time_in_force": "good_till_canceled", + "self_trade_prevention_type": "taker_at_cross", + "exchange_index": 0, + } + + +class TestAsyncCancelOrderV2: + @respx.mock + @pytest.mark.asyncio + async def test_returns_response( + self, orders: AsyncOrdersResource, + ) -> None: + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1", + ).mock( + return_value=httpx.Response( + 200, + json={ + "order_id": "ord-1", + "reduced_by": "5", + "ts_ms": 1700000000000, + }, + ) + ) + result = await orders.cancel_v2("ord-1") + assert result.reduced_by == Decimal("5") + + @respx.mock + @pytest.mark.asyncio + async def test_204_raises(self, orders: AsyncOrdersResource) -> None: + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1", + ).mock(return_value=httpx.Response(204)) + with pytest.raises(KalshiError, match="204 No Content"): + await orders.cancel_v2("ord-1") + + @respx.mock + @pytest.mark.asyncio + async def test_passes_query_params( + self, orders: AsyncOrdersResource, + ) -> None: + """Async parity with sync TestCancelOrderV2.test_passes_query_params: + cancel_v2 routes BOTH subaccount and exchange_index to query params + (unlike amend_v2/decrease_v2 where exchange_index is body). + """ + route = respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1", + ).mock( + return_value=httpx.Response( + 200, + json={"order_id": "ord-1", "reduced_by": "0", "ts_ms": 0}, + ) + ) + await orders.cancel_v2("ord-1", subaccount=3, exchange_index=0) + params = dict(route.calls[0].request.url.params) + assert params["subaccount"] == "3" + assert params["exchange_index"] == "0" + + +class TestAsyncAmendOrderV2: + @respx.mock + @pytest.mark.asyncio + async def test_returns_response( + self, orders: AsyncOrdersResource, + ) -> None: + respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/amend", + ).mock( + return_value=httpx.Response( + 200, json={"order_id": "ord-1", "ts_ms": 1700000000000}, + ) + ) + result = await orders.amend_v2( + "ord-1", + request=AmendOrderV2Request( + ticker="MKT-A", + side="ask", + price=Decimal("0.55"), + count=Decimal("10"), + ), + ) + assert result.order_id == "ord-1" + + +class TestAsyncDecreaseOrderV2: + @respx.mock + @pytest.mark.asyncio + async def test_returns_response( + self, orders: AsyncOrdersResource, + ) -> None: + respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/decrease", + ).mock( + return_value=httpx.Response( + 200, + json={ + "order_id": "ord-1", + "remaining_count": "5", + "ts_ms": 1700000000000, + }, + ) + ) + result = await orders.decrease_v2( + "ord-1", + request=DecreaseOrderV2Request(reduce_to=Decimal("5")), + ) + assert result.remaining_count == Decimal("5") + + +class TestAsyncBatchCreateV2: + @respx.mock + @pytest.mark.asyncio + async def test_returns_response( + self, orders: AsyncOrdersResource, + ) -> None: + respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock( + return_value=httpx.Response( + 201, + json={ + "orders": [ + { + "order_id": "ord-a", + "fill_count": "0", + "remaining_count": "10", + "ts_ms": 1700000000000, + }, + ], + }, + ) + ) + result = await orders.batch_create_v2( + request=BatchCreateOrdersV2Request( + orders=[ + CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ), + ], + ), + ) + assert result.orders[0].order_id == "ord-a" + + +class TestAsyncBatchCancelV2: + @respx.mock + @pytest.mark.asyncio + async def test_returns_response( + self, orders: AsyncOrdersResource, + ) -> None: + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock( + return_value=httpx.Response( + 200, + json={ + "orders": [ + { + "order_id": "ord-a", + "reduced_by": "10", + "ts_ms": 1700000000000, + }, + ], + }, + ) + ) + result = await orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[BatchCancelOrdersV2RequestOrder(order_id="ord-a")], + ), + ) + assert result.orders[0].reduced_by == Decimal("10") + + @respx.mock + @pytest.mark.asyncio + async def test_204_raises(self, orders: AsyncOrdersResource) -> None: + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock(return_value=httpx.Response(204)) + with pytest.raises(KalshiError, match="204 No Content"): + await orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[BatchCancelOrdersV2RequestOrder(order_id="ord-a")], + ), + ) + + +class TestAsyncAmendDecreaseV2QueryParams: + """Regression guard: subaccount must reach the query string (not the body) + on both amend_v2 and decrease_v2 — exchange_index stays in the body. + """ + + @respx.mock + @pytest.mark.asyncio + async def test_amend_v2(self, orders: AsyncOrdersResource) -> None: + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/amend", + ).mock( + return_value=httpx.Response( + 200, json={"order_id": "ord-1", "ts_ms": 0}, + ) + ) + await orders.amend_v2( + "ord-1", + request=AmendOrderV2Request( + ticker="MKT-A", side="bid", + price=Decimal("0.55"), count=Decimal("10"), + exchange_index=0, + ), + subaccount=7, + ) + request = route.calls[0].request + assert dict(request.url.params) == {"subaccount": "7"} + body = json.loads(request.content) + assert body.get("exchange_index") == 0 + + @respx.mock + @pytest.mark.asyncio + async def test_decrease_v2(self, orders: AsyncOrdersResource) -> None: + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/decrease", + ).mock( + return_value=httpx.Response( + 200, + json={"order_id": "ord-1", "remaining_count": "0", "ts_ms": 0}, + ) + ) + await orders.decrease_v2( + "ord-1", + request=DecreaseOrderV2Request( + reduce_by=Decimal("2"), exchange_index=0, + ), + subaccount=4, + ) + request = route.calls[0].request + assert dict(request.url.params) == {"subaccount": "4"} + body = json.loads(request.content) + assert body.get("exchange_index") == 0 + assert "exchange_index" not in request.url.params + + +class TestAsyncV2RequiresAuth: + @pytest.fixture + def _create_request(self) -> CreateOrderV2Request: + return CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) + + @pytest.mark.asyncio + async def test_create_v2( + self, + unauth_orders_async: AsyncOrdersResource, + _create_request: CreateOrderV2Request, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_orders_async.create_v2(request=_create_request) + + @pytest.mark.asyncio + async def test_cancel_v2( + self, unauth_orders_async: AsyncOrdersResource, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_orders_async.cancel_v2("ord-1") + + @pytest.mark.asyncio + async def test_amend_v2( + self, unauth_orders_async: AsyncOrdersResource, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_orders_async.amend_v2( + "ord-1", + request=AmendOrderV2Request( + ticker="MKT-A", side="bid", + price=Decimal("0.55"), count=Decimal("10"), + ), + ) + + @pytest.mark.asyncio + async def test_decrease_v2( + self, unauth_orders_async: AsyncOrdersResource, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_orders_async.decrease_v2( + "ord-1", + request=DecreaseOrderV2Request(reduce_by=Decimal("2")), + ) + + @pytest.mark.asyncio + async def test_batch_create_v2( + self, + unauth_orders_async: AsyncOrdersResource, + _create_request: CreateOrderV2Request, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_orders_async.batch_create_v2( + request=BatchCreateOrdersV2Request(orders=[_create_request]), + ) + + @pytest.mark.asyncio + async def test_batch_cancel_v2( + self, unauth_orders_async: AsyncOrdersResource, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_orders_async.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[BatchCancelOrdersV2RequestOrder(order_id="ord-a")], + ), + ) diff --git a/tests/test_communications.py b/tests/test_communications.py index d3159bb..a21ab98 100644 --- a/tests/test_communications.py +++ b/tests/test_communications.py @@ -462,6 +462,46 @@ def test_list_all_quotes_raises_without_creator_filter( with pytest.raises(ValueError): comms.list_all_quotes() + @respx.mock + def test_user_filter_alone_is_sufficient( + self, comms: CommunicationsResource, + ) -> None: + """Spec v3.18.0 added user_filter='self' as a server-side shorthand + for the caller's user-id, so it satisfies the filter requirement. + """ + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(200, json={"quotes": []})) + page = comms.list_quotes(user_filter="self") + assert page.items == [] + + @respx.mock + def test_rfq_user_filter_alone_is_sufficient( + self, comms: CommunicationsResource, + ) -> None: + """rfq_user_filter='self' (filter to quotes responding to the caller's + own RFQs) is also a valid standalone satisfier. + """ + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(200, json={"quotes": []})) + page = comms.list_quotes(rfq_user_filter="self") + assert page.items == [] + + def test_raises_lists_all_four_satisfiers( + self, comms: CommunicationsResource, + ) -> None: + """The updated error message must enumerate all four valid filters + so callers know about the user_filter / rfq_user_filter shortcuts. + """ + with pytest.raises(ValueError) as excinfo: + comms.list_quotes() + msg = str(excinfo.value) + assert "quote_creator_user_id" in msg + assert "rfq_creator_user_id" in msg + assert "user_filter" in msg + assert "rfq_user_filter" in msg + class TestGetQuote: @respx.mock @@ -706,6 +746,29 @@ async def test_list_all_quotes_raises_without_creator_filter( with pytest.raises(ValueError): async_comms.list_all_quotes() + async def test_list_quotes_user_filter_alone_is_sufficient( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + """Spec v3.18.0: user_filter='self' satisfies the filter requirement.""" + respx_mock.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(200, json={"quotes": []})) + page = await async_comms.list_quotes(user_filter="self") + assert page.items == [] + + async def test_list_quotes_rfq_user_filter_alone_is_sufficient( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(200, json={"quotes": []})) + page = await async_comms.list_quotes(rfq_user_filter="self") + assert page.items == [] + class TestCommunicationsAuthGuard: def test_get_id_requires_auth( diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 8077af3..87822c6 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -1078,6 +1078,21 @@ def _assert_params_match( "#/components/schemas/BatchCancelOrdersRequest": ( "kalshi.models.orders.BatchCancelOrdersRequest" ), + "#/components/schemas/CreateOrderV2Request": ( + "kalshi.models.orders.CreateOrderV2Request" + ), + "#/components/schemas/AmendOrderV2Request": ( + "kalshi.models.orders.AmendOrderV2Request" + ), + "#/components/schemas/DecreaseOrderV2Request": ( + "kalshi.models.orders.DecreaseOrderV2Request" + ), + "#/components/schemas/BatchCreateOrdersV2Request": ( + "kalshi.models.orders.BatchCreateOrdersV2Request" + ), + "#/components/schemas/BatchCancelOrdersV2Request": ( + "kalshi.models.orders.BatchCancelOrdersV2Request" + ), "#/components/schemas/CreateMarketInMultivariateEventCollectionRequest": ( "kalshi.models.multivariate." "CreateMarketInMultivariateEventCollectionRequest" diff --git a/tests/test_orders.py b/tests/test_orders.py index 3b16e77..c08a7eb 100644 --- a/tests/test_orders.py +++ b/tests/test_orders.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from decimal import Decimal from unittest.mock import patch @@ -13,8 +14,21 @@ from kalshi.auth import KalshiAuth from kalshi.client import KalshiClient from kalshi.config import KalshiConfig -from kalshi.errors import KalshiNotFoundError, KalshiValidationError -from kalshi.models.orders import CreateOrderRequest +from kalshi.errors import ( + AuthRequiredError, + KalshiError, + KalshiNotFoundError, + KalshiValidationError, +) +from kalshi.models.orders import ( + AmendOrderV2Request, + BatchCancelOrdersV2Request, + BatchCancelOrdersV2RequestOrder, + BatchCreateOrdersV2Request, + CreateOrderRequest, + CreateOrderV2Request, + DecreaseOrderV2Request, +) from kalshi.resources.orders import OrdersResource @@ -1080,3 +1094,460 @@ def test_wraps_orders_key(self, orders: OrdersResource) -> None: assert len(body["orders"]) == 2 # no phantom top-level keys assert set(body.keys()) == {"orders"} + + +# ── V2 event-market orders (spec v3.18.0) ─────────────────── + + +class TestCreateOrderV2: + @respx.mock + def test_returns_response(self, orders: OrdersResource) -> None: + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders", + ).mock( + return_value=httpx.Response( + 201, + json={ + "order_id": "ord-v2-1", + "client_order_id": "cli-1", + "fill_count": "0", + "remaining_count": "10", + "ts_ms": 1700000000000, + }, + ) + ) + result = orders.create_v2( + request=CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ), + ) + assert result.order_id == "ord-v2-1" + assert result.fill_count == Decimal("0") + assert result.remaining_count == Decimal("10") + assert route.calls.call_count == 1 + + def test_side_must_be_bid_or_ask(self) -> None: + with pytest.raises(ValueError): + CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="yes", # type: ignore[arg-type] # invalid for BookSide + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) + + @respx.mock + def test_serializes_body(self, orders: OrdersResource) -> None: + """V2 model_dump goes through DollarDecimal/FixedPointCount with + mode="json" — guard against accidental regression in price/count + wire shape or by_alias/exclude_none plumbing. + """ + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders", + ).mock( + return_value=httpx.Response( + 201, + json={ + "order_id": "ord-v2-1", + "fill_count": "0", + "remaining_count": "10", + "ts_ms": 0, + }, + ) + ) + orders.create_v2( + request=CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + exchange_index=0, + ), + ) + body = json.loads(route.calls[0].request.content) + # No phantom keys; DollarDecimal serializes as string with mode=json. + assert body == { + "ticker": "MKT-A", + "client_order_id": "cli-1", + "side": "bid", + "count": "10", + "price": "0.50", + "time_in_force": "good_till_canceled", + "self_trade_prevention_type": "taker_at_cross", + "exchange_index": 0, + } + + +class TestCancelOrderV2: + @respx.mock + def test_returns_response(self, orders: OrdersResource) -> None: + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1", + ).mock( + return_value=httpx.Response( + 200, + json={ + "order_id": "ord-1", + "reduced_by": "5", + "ts_ms": 1700000000000, + }, + ) + ) + result = orders.cancel_v2("ord-1") + assert result.order_id == "ord-1" + assert result.reduced_by == Decimal("5") + + @respx.mock + def test_204_raises(self, orders: OrdersResource) -> None: + """The V2 endpoint promises a body; 204 No Content is an SDK error.""" + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1", + ).mock(return_value=httpx.Response(204)) + with pytest.raises(KalshiError, match="204 No Content"): + orders.cancel_v2("ord-1") + + @respx.mock + def test_passes_query_params(self, orders: OrdersResource) -> None: + route = respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1", + ).mock( + return_value=httpx.Response( + 200, + json={"order_id": "ord-1", "reduced_by": "0", "ts_ms": 0}, + ) + ) + orders.cancel_v2("ord-1", subaccount=3, exchange_index=0) + params = dict(route.calls[0].request.url.params) + assert params["subaccount"] == "3" + assert params["exchange_index"] == "0" + + +class TestAmendOrderV2: + @respx.mock + def test_returns_response(self, orders: OrdersResource) -> None: + respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/amend", + ).mock( + return_value=httpx.Response( + 200, json={"order_id": "ord-1", "ts_ms": 1700000000000}, + ) + ) + result = orders.amend_v2( + "ord-1", + request=AmendOrderV2Request( + ticker="MKT-A", + side="bid", + price=Decimal("0.55"), + count=Decimal("10"), + ), + ) + assert result.order_id == "ord-1" + + def test_side_must_be_bid_or_ask(self) -> None: + with pytest.raises(ValueError): + AmendOrderV2Request( + ticker="MKT-A", + side="yes", # type: ignore[arg-type] + price=Decimal("0.55"), + count=Decimal("10"), + ) + + @respx.mock + def test_passes_subaccount_query(self, orders: OrdersResource) -> None: + """Spec puts subaccount in the query, exchange_index in the body. + + Regression guard against the params kwarg being dropped from _post. + """ + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/amend", + ).mock( + return_value=httpx.Response( + 200, json={"order_id": "ord-1", "ts_ms": 0}, + ) + ) + orders.amend_v2( + "ord-1", + request=AmendOrderV2Request( + ticker="MKT-A", side="bid", + price=Decimal("0.55"), count=Decimal("10"), + exchange_index=0, + ), + subaccount=7, + ) + request = route.calls[0].request + assert dict(request.url.params) == {"subaccount": "7"} + body = json.loads(request.content) + assert body.get("exchange_index") == 0 + # exchange_index is body-only, must not leak into query + assert "exchange_index" not in request.url.params + + +class TestDecreaseOrderV2: + @respx.mock + def test_returns_response(self, orders: OrdersResource) -> None: + respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/decrease", + ).mock( + return_value=httpx.Response( + 200, + json={ + "order_id": "ord-1", + "remaining_count": "5", + "ts_ms": 1700000000000, + }, + ) + ) + result = orders.decrease_v2( + "ord-1", + request=DecreaseOrderV2Request(reduce_by=Decimal("2")), + ) + assert result.remaining_count == Decimal("5") + + def test_xor_rejects_both(self) -> None: + with pytest.raises(ValueError, match="not both"): + DecreaseOrderV2Request( + reduce_by=Decimal("2"), reduce_to=Decimal("5"), + ) + + def test_xor_requires_one(self) -> None: + with pytest.raises(ValueError, match="requires either"): + DecreaseOrderV2Request() + + @respx.mock + def test_passes_subaccount_query(self, orders: OrdersResource) -> None: + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/decrease", + ).mock( + return_value=httpx.Response( + 200, + json={"order_id": "ord-1", "remaining_count": "0", "ts_ms": 0}, + ) + ) + orders.decrease_v2( + "ord-1", + request=DecreaseOrderV2Request( + reduce_by=Decimal("2"), exchange_index=0, + ), + subaccount=4, + ) + request = route.calls[0].request + assert dict(request.url.params) == {"subaccount": "4"} + body = json.loads(request.content) + assert body.get("exchange_index") == 0 + + +class TestBatchCreateV2: + @respx.mock + def test_returns_response(self, orders: OrdersResource) -> None: + respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock( + return_value=httpx.Response( + 201, + json={ + "orders": [ + { + "order_id": "ord-a", + "fill_count": "0", + "remaining_count": "10", + "ts_ms": 1700000000000, + }, + {"error": {"code": "invalid_market"}}, + ], + }, + ) + ) + result = orders.batch_create_v2( + request=BatchCreateOrdersV2Request( + orders=[ + CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ), + ], + ), + ) + assert len(result.orders) == 2 + assert result.orders[0].order_id == "ord-a" + assert result.orders[1].error == {"code": "invalid_market"} + + +class TestBatchCancelV2: + @respx.mock + def test_sends_body(self, orders: OrdersResource) -> None: + """Spec says DELETE /portfolio/events/orders/batched carries a JSON + body. Regression guard against the body silently dropping off the + request — httpx + the DELETE-with-body helper need to keep this wired. + """ + route = respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock( + return_value=httpx.Response(200, json={"orders": []}), + ) + orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[ + BatchCancelOrdersV2RequestOrder(order_id="ord-a", subaccount=3), + BatchCancelOrdersV2RequestOrder(order_id="ord-b"), + ], + ), + ) + body = json.loads(route.calls[0].request.content) + assert body == { + "orders": [ + {"order_id": "ord-a", "subaccount": 3}, + {"order_id": "ord-b"}, + ], + } + + @respx.mock + def test_error_entry_parses(self, orders: OrdersResource) -> None: + """Per spec, an errored cancel still carries order_id + reduced_by=0 + alongside the error block. Document the spec contract here so we + catch upstream divergence early. + """ + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock( + return_value=httpx.Response( + 200, + json={ + "orders": [ + { + "order_id": "ord-bad", + "reduced_by": "0", + "error": { + "code": "order_not_found", + "message": "no such order", + }, + }, + ], + }, + ) + ) + result = orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[BatchCancelOrdersV2RequestOrder(order_id="ord-bad")], + ), + ) + assert result.orders[0].order_id == "ord-bad" + assert result.orders[0].reduced_by == Decimal("0") + assert result.orders[0].error == { + "code": "order_not_found", + "message": "no such order", + } + + @respx.mock + def test_returns_response(self, orders: OrdersResource) -> None: + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock( + return_value=httpx.Response( + 200, + json={ + "orders": [ + { + "order_id": "ord-a", + "reduced_by": "10", + "ts_ms": 1700000000000, + }, + ], + }, + ) + ) + result = orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[BatchCancelOrdersV2RequestOrder(order_id="ord-a")], + ), + ) + assert result.orders[0].order_id == "ord-a" + assert result.orders[0].reduced_by == Decimal("10") + + @respx.mock + def test_204_raises(self, orders: OrdersResource) -> None: + respx.delete( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/batched", + ).mock(return_value=httpx.Response(204)) + with pytest.raises(KalshiError, match="204 No Content"): + orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[BatchCancelOrdersV2RequestOrder(order_id="ord-a")], + ), + ) + + +class TestV2RequiresAuth: + """Every V2 method must reject an unauthenticated client before + issuing the request (matches the V1 cancel/create/etc. tests). + """ + + @pytest.fixture + def _create_request(self) -> CreateOrderV2Request: + return CreateOrderV2Request( + ticker="MKT-A", + client_order_id="cli-1", + side="bid", + count=Decimal("10"), + price=Decimal("0.50"), + time_in_force="good_till_canceled", + self_trade_prevention_type="taker_at_cross", + ) + + def test_create_v2( + self, unauth_orders: OrdersResource, _create_request: CreateOrderV2Request, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_orders.create_v2(request=_create_request) + + def test_cancel_v2(self, unauth_orders: OrdersResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_orders.cancel_v2("ord-1") + + def test_amend_v2(self, unauth_orders: OrdersResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_orders.amend_v2( + "ord-1", + request=AmendOrderV2Request( + ticker="MKT-A", side="bid", + price=Decimal("0.55"), count=Decimal("10"), + ), + ) + + def test_decrease_v2(self, unauth_orders: OrdersResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_orders.decrease_v2( + "ord-1", + request=DecreaseOrderV2Request(reduce_by=Decimal("2")), + ) + + def test_batch_create_v2( + self, unauth_orders: OrdersResource, _create_request: CreateOrderV2Request, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_orders.batch_create_v2( + request=BatchCreateOrdersV2Request(orders=[_create_request]), + ) + + def test_batch_cancel_v2(self, unauth_orders: OrdersResource) -> None: + with pytest.raises(AuthRequiredError): + unauth_orders.batch_cancel_v2( + request=BatchCancelOrdersV2Request( + orders=[BatchCancelOrdersV2RequestOrder(order_id="ord-a")], + ), + ) diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 75cfed9..5c93773 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -11,7 +11,7 @@ from kalshi._base_client import AsyncTransport, SyncTransport from kalshi.auth import KalshiAuth from kalshi.config import KalshiConfig -from kalshi.errors import KalshiAuthError +from kalshi.errors import AuthRequiredError, KalshiAuthError from kalshi.resources.portfolio import AsyncPortfolioResource, PortfolioResource @@ -36,6 +36,16 @@ def async_portfolio( return AsyncPortfolioResource(AsyncTransport(test_auth, config)) +@pytest.fixture +def unauth_portfolio(config: KalshiConfig) -> PortfolioResource: + return PortfolioResource(SyncTransport(None, config)) + + +@pytest.fixture +def unauth_async_portfolio(config: KalshiConfig) -> AsyncPortfolioResource: + return AsyncPortfolioResource(AsyncTransport(None, config)) + + # ── Sync tests ────────────────────────────────────────────── @@ -47,6 +57,7 @@ def test_returns_balance(self, portfolio: PortfolioResource) -> None: 200, json={ "balance": 50000, + "balance_dollars": "500.00", "portfolio_value": 75000, "updated_ts": 1700000000, }, @@ -54,6 +65,7 @@ def test_returns_balance(self, portfolio: PortfolioResource) -> None: ) balance = portfolio.balance() assert balance.balance == 50000 + assert balance.balance_dollars == Decimal("500.00") assert balance.portfolio_value == 75000 assert balance.updated_ts == 1700000000 @@ -62,11 +74,17 @@ def test_zero_balance(self, portfolio: PortfolioResource) -> None: respx.get("https://test.kalshi.com/trade-api/v2/portfolio/balance").mock( return_value=httpx.Response( 200, - json={"balance": 0, "portfolio_value": 0, "updated_ts": 0}, + json={ + "balance": 0, + "balance_dollars": "0.00", + "portfolio_value": 0, + "updated_ts": 0, + }, ) ) balance = portfolio.balance() assert balance.balance == 0 + assert balance.balance_dollars == Decimal("0.00") assert balance.portfolio_value == 0 @respx.mock @@ -82,12 +100,58 @@ def test_balance_with_subaccount(self, portfolio: PortfolioResource) -> None: """v0.7.0 ADD: subaccount kwarg reaches the wire.""" route = respx.get("https://test.kalshi.com/trade-api/v2/portfolio/balance").mock( return_value=httpx.Response( - 200, json={"balance": 0, "portfolio_value": 0, "updated_ts": 0} + 200, + json={ + "balance": 0, + "balance_dollars": "0.00", + "portfolio_value": 0, + "updated_ts": 0, + }, ) ) portfolio.balance(subaccount=42) assert route.calls[0].request.url.params["subaccount"] == "42" + @respx.mock + def test_balance_breakdown(self, portfolio: PortfolioResource) -> None: + """Spec v3.18.0 adds balance_breakdown — optional per-shard split.""" + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/balance").mock( + return_value=httpx.Response( + 200, + json={ + "balance": 50000, + "balance_dollars": "500.00", + "portfolio_value": 75000, + "updated_ts": 1700000000, + "balance_breakdown": [ + {"exchange_index": 0, "balance": "500.00"}, + ], + }, + ) + ) + balance = portfolio.balance() + assert balance.balance_breakdown is not None + assert len(balance.balance_breakdown) == 1 + assert balance.balance_breakdown[0].exchange_index == 0 + assert balance.balance_breakdown[0].balance == Decimal("500.00") + + @respx.mock + def test_balance_breakdown_omitted(self, portfolio: PortfolioResource) -> None: + """balance_breakdown is optional — must default to None when absent.""" + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/balance").mock( + return_value=httpx.Response( + 200, + json={ + "balance": 50000, + "balance_dollars": "500.00", + "portfolio_value": 75000, + "updated_ts": 1700000000, + }, + ) + ) + balance = portfolio.balance() + assert balance.balance_breakdown is None + class TestPortfolioPositions: @respx.mock @@ -399,6 +463,159 @@ def test_unauthorized(self, portfolio: PortfolioResource) -> None: portfolio.total_resting_order_value() +_DEPOSIT = { + "id": "dep_1", "status": "applied", "type": "ach", + "amount_cents": 10000, "fee_cents": 0, "created_ts": 1700000000, + "finalized_ts": 1700001000, +} + +_WITHDRAWAL = { + "id": "wd_1", "status": "pending", "type": "wire", + "amount_cents": 5000, "fee_cents": 25, "created_ts": 1700000000, + "finalized_ts": None, +} + + +class TestPortfolioDeposits: + @respx.mock + def test_returns_page(self, portfolio: PortfolioResource) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/deposits").mock( + return_value=httpx.Response( + 200, json={"deposits": [_DEPOSIT], "cursor": "abc"}, + ) + ) + page = portfolio.deposits(limit=10) + assert len(page.items) == 1 + assert page.items[0].id == "dep_1" + assert page.items[0].status == "applied" + assert page.cursor == "abc" + + @respx.mock + def test_empty(self, portfolio: PortfolioResource) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/deposits").mock( + return_value=httpx.Response(200, json={"deposits": []}) + ) + page = portfolio.deposits() + assert page.items == [] + assert page.cursor is None + + @respx.mock + def test_all_paginates(self, portfolio: PortfolioResource) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/deposits").mock( + side_effect=[ + httpx.Response( + 200, + json={ + "deposits": [_DEPOSIT, {**_DEPOSIT, "id": "dep_2"}], + "cursor": "page2", + }, + ), + httpx.Response( + 200, + json={"deposits": [{**_DEPOSIT, "id": "dep_3"}], "cursor": ""}, + ), + ] + ) + items = list(portfolio.deposits_all(limit=2)) + assert [d.id for d in items] == ["dep_1", "dep_2", "dep_3"] + + @respx.mock + def test_all_max_pages_caps(self, portfolio: PortfolioResource) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/deposits").mock( + side_effect=[ + httpx.Response( + 200, json={"deposits": [_DEPOSIT], "cursor": "p2"}, + ), + httpx.Response( + 200, + json={"deposits": [{**_DEPOSIT, "id": "dep_2"}], "cursor": "p3"}, + ), + ] + ) + items = list(portfolio.deposits_all(max_pages=2)) + # max_pages=2 stops after 2 pages even though cursor "p3" is non-empty. + assert len(items) == 2 + + @respx.mock + def test_auth_failure(self, portfolio: PortfolioResource) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/deposits").mock( + return_value=httpx.Response(401, json={"error": "unauthorized"}) + ) + with pytest.raises(KalshiAuthError): + portfolio.deposits() + + def test_deposits_requires_auth( + self, unauth_portfolio: PortfolioResource, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_portfolio.deposits() + + def test_deposits_all_requires_auth( + self, unauth_portfolio: PortfolioResource, + ) -> None: + # *_all returns an iterator — must raise eagerly, not on first iteration. + with pytest.raises(AuthRequiredError): + unauth_portfolio.deposits_all() + + +class TestPortfolioWithdrawals: + @respx.mock + def test_returns_page(self, portfolio: PortfolioResource) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/portfolio/withdrawals", + ).mock( + return_value=httpx.Response( + 200, json={"withdrawals": [_WITHDRAWAL], "cursor": None}, + ) + ) + page = portfolio.withdrawals() + assert len(page.items) == 1 + assert page.items[0].id == "wd_1" + assert page.items[0].fee_cents == 25 + assert page.items[0].finalized_ts is None + assert page.cursor is None + + @respx.mock + def test_all_paginates(self, portfolio: PortfolioResource) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/portfolio/withdrawals", + ).mock( + side_effect=[ + httpx.Response( + 200, json={"withdrawals": [_WITHDRAWAL], "cursor": "p2"}, + ), + httpx.Response( + 200, + json={"withdrawals": [{**_WITHDRAWAL, "id": "wd_2"}], "cursor": ""}, + ), + ] + ) + items = list(portfolio.withdrawals_all()) + assert [w.id for w in items] == ["wd_1", "wd_2"] + + @respx.mock + def test_auth_failure(self, portfolio: PortfolioResource) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/portfolio/withdrawals", + ).mock( + return_value=httpx.Response(401, json={"error": "unauthorized"}) + ) + with pytest.raises(KalshiAuthError): + portfolio.withdrawals() + + def test_withdrawals_requires_auth( + self, unauth_portfolio: PortfolioResource, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_portfolio.withdrawals() + + def test_withdrawals_all_requires_auth( + self, unauth_portfolio: PortfolioResource, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_portfolio.withdrawals_all() + + # ── Async tests ───────────────────────────────────────────── @@ -411,11 +628,17 @@ async def test_returns_balance( respx.get("https://test.kalshi.com/trade-api/v2/portfolio/balance").mock( return_value=httpx.Response( 200, - json={"balance": 50000, "portfolio_value": 75000, "updated_ts": 1700000000}, + json={ + "balance": 50000, + "balance_dollars": "500.00", + "portfolio_value": 75000, + "updated_ts": 1700000000, + }, ) ) balance = await async_portfolio.balance() assert balance.balance == 50000 + assert balance.balance_dollars == Decimal("500.00") assert balance.portfolio_value == 75000 @respx.mock @@ -428,7 +651,13 @@ async def test_balance_with_subaccount( "https://test.kalshi.com/trade-api/v2/portfolio/balance" ).mock( return_value=httpx.Response( - 200, json={"balance": 0, "portfolio_value": 0, "updated_ts": 0} + 200, + json={ + "balance": 0, + "balance_dollars": "0.00", + "portfolio_value": 0, + "updated_ts": 0, + }, ) ) await async_portfolio.balance(subaccount=42) @@ -677,3 +906,122 @@ async def test_unauthorized( ).mock(return_value=httpx.Response(401, json={"error": "unauthorized"})) with pytest.raises(KalshiAuthError): await async_portfolio.total_resting_order_value() + + +class TestAsyncPortfolioDeposits: + @respx.mock + @pytest.mark.asyncio + async def test_returns_page( + self, async_portfolio: AsyncPortfolioResource, + ) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/deposits").mock( + return_value=httpx.Response( + 200, json={"deposits": [_DEPOSIT], "cursor": None}, + ) + ) + page = await async_portfolio.deposits() + assert page.items[0].id == "dep_1" + assert page.cursor is None + + @respx.mock + @pytest.mark.asyncio + async def test_all_paginates( + self, async_portfolio: AsyncPortfolioResource, + ) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/portfolio/deposits").mock( + side_effect=[ + httpx.Response( + 200, json={"deposits": [_DEPOSIT], "cursor": "p2"}, + ), + httpx.Response( + 200, + json={"deposits": [{**_DEPOSIT, "id": "dep_2"}], "cursor": ""}, + ), + ] + ) + items = [d async for d in async_portfolio.deposits_all()] + assert [d.id for d in items] == ["dep_1", "dep_2"] + + +class TestAsyncPortfolioWithdrawals: + @respx.mock + @pytest.mark.asyncio + async def test_returns_page( + self, async_portfolio: AsyncPortfolioResource, + ) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/portfolio/withdrawals", + ).mock( + return_value=httpx.Response( + 200, json={"withdrawals": [_WITHDRAWAL]}, + ) + ) + page = await async_portfolio.withdrawals() + assert page.items[0].id == "wd_1" + + @respx.mock + @pytest.mark.asyncio + async def test_all_max_pages_caps( + self, async_portfolio: AsyncPortfolioResource, + ) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/portfolio/withdrawals", + ).mock( + side_effect=[ + httpx.Response( + 200, json={"withdrawals": [_WITHDRAWAL], "cursor": "p2"}, + ), + httpx.Response( + 200, + json={ + "withdrawals": [{**_WITHDRAWAL, "id": "wd_2"}], + "cursor": "p3", + }, + ), + ] + ) + items = [w async for w in async_portfolio.withdrawals_all(max_pages=2)] + assert len(items) == 2 + + @respx.mock + @pytest.mark.asyncio + async def test_auth_failure( + self, async_portfolio: AsyncPortfolioResource, + ) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/portfolio/withdrawals", + ).mock( + return_value=httpx.Response(401, json={"error": "unauthorized"}) + ) + with pytest.raises(KalshiAuthError): + await async_portfolio.withdrawals() + + @pytest.mark.asyncio + async def test_withdrawals_requires_auth( + self, unauth_async_portfolio: AsyncPortfolioResource, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_async_portfolio.withdrawals() + + def test_withdrawals_all_requires_auth( + self, unauth_async_portfolio: AsyncPortfolioResource, + ) -> None: + # withdrawals_all is plain `def` returning AsyncIterator; auth check + # must fire at call time, not on first iteration. + with pytest.raises(AuthRequiredError): + unauth_async_portfolio.withdrawals_all() + + +class TestAsyncPortfolioDepositsAuth: + @pytest.mark.asyncio + async def test_deposits_requires_auth( + self, unauth_async_portfolio: AsyncPortfolioResource, + ) -> None: + with pytest.raises(AuthRequiredError): + await unauth_async_portfolio.deposits() + + def test_deposits_all_requires_auth( + self, unauth_async_portfolio: AsyncPortfolioResource, + ) -> None: + with pytest.raises(AuthRequiredError): + unauth_async_portfolio.deposits_all()