Skip to content

Orders

Place, amend, cancel, and inspect orders. The largest single resource in the SDK and the surface you'll spend the most time with as a trader.

Every method here requires auth.

Quick reference

Order writes go through the V2 event-market family (/portfolio/events/orders/*). Reads stay on /portfolio/orders/*.

Method Endpoint Retry
create_v2(*, request) POST /portfolio/events/orders never — see Retries & idempotency
batch_create_v2(*, request) POST /portfolio/events/orders/batched never
cancel_v2(order_id, *, subaccount, exchange_index, market_ticker) DELETE /portfolio/events/orders/{order_id} never
batch_cancel_v2(*, request) DELETE /portfolio/events/orders/batched never
amend_v2(order_id, *, request, subaccount) POST /portfolio/events/orders/{order_id}/amend never
decrease_v2(order_id, *, request, subaccount) POST /portfolio/events/orders/{order_id}/decrease never
get(order_id) GET /portfolio/orders/{order_id} yes (GET)
list(...) / list_all(...) GET /portfolio/orders yes
~~fills(...) / fills_all(...)~~ moved to PortfolioResource in v3.0.0 — see Portfolio › Fills; the old methods remain as deprecated aliases until removal in a future release.
queue_positions(*, market_tickers, event_ticker) GET /portfolio/orders/queue_positions yes
queue_position(order_id) GET /portfolio/orders/{order_id}/queue_position yes

V2 uses BookSideLiteral ("bid" / "ask"), a single price field (not paired yes/no), and requires client_order_id as a server-side idempotency key. The surface is model-only — every write method takes a pre-built request model rather than kwargs.

Place an order

import uuid
from decimal import Decimal
from kalshi import KalshiClient, CreateOrderV2Request

with KalshiClient.from_env() as client:
    resp = client.orders.create_v2(request=CreateOrderV2Request(
        ticker="KXPRES-24-DJT",
        client_order_id=str(uuid.uuid4()),    # required — idempotency key
        side="bid",                           # BookSideLiteral — "bid" / "ask"
        count=Decimal("10"),                  # Decimal — never float
        price=Decimal("0.65"),                # Decimal dollars — never float
        time_in_force="good_till_canceled",   # TimeInForceLiteral
        self_trade_prevention_type="taker_at_cross",
        # subaccount=1,                        # route to subaccount 1
        # exchange_index=0,
    ))
    print(resp.order_id, resp.remaining_count, resp.fill_count)

The write surface is model-only: there is no kwarg overload. Always build a CreateOrderV2Request. side is the book side "bid" / "ask" (not "yes" / "no"), count and price are Decimal, and client_order_id is required.

Time-in-force

Value Behavior
"fill_or_kill" Fill instantly in full or cancel.
"good_till_canceled" Rest until canceled or filled. Server default if you omit.
"immediate_or_cancel" Fill whatever you can immediately, cancel the rest.

client_order_id for safe retries

POST /portfolio/events/orders is never automatically retried. Set a fresh client_order_id per call so you can safely retry from your app layer without double-filling. Reusing a client_order_id for a second call returns the original order rather than placing a new one — generate a fresh UUID4 per request. See Retries & idempotency.

Self-trade prevention

SelfTradePreventionTypeLiteral values:

  • "taker_at_cross" — your taker order is canceled if it would cross your own resting order.
  • "maker" — your resting maker order is canceled if a taker side of yours crosses it.

Batch create

import uuid
from decimal import Decimal
from kalshi import BatchCreateOrdersV2Request, CreateOrderV2Request

result = client.orders.batch_create_v2(request=BatchCreateOrdersV2Request(
    orders=[
        CreateOrderV2Request(
            ticker="X-A", client_order_id=str(uuid.uuid4()),
            side="bid", count=Decimal("10"), price=Decimal("0.60"),
            time_in_force="good_till_canceled", self_trade_prevention_type="taker_at_cross",
        ),
        CreateOrderV2Request(
            ticker="X-B", client_order_id=str(uuid.uuid4()),
            side="ask", count=Decimal("10"), price=Decimal("0.42"),
            time_in_force="good_till_canceled", self_trade_prevention_type="taker_at_cross",
        ),
    ],
))
for entry in result.orders:
    if entry.error is not None:
        print("failed:", entry.error)
    else:
        print(entry.order_id, entry.fill_count)

Each child CreateOrderV2Request has extra="forbid", so a typo in any leg fails at construction before the round trip.

Cancel

result = client.orders.cancel_v2("ord_abc")
print(result.reduced_by, result.ts_ms)   # FixedPointCount, int

cancel_v2 returns a CancelOrderV2Response (with the count actually canceled). A 204 No Content from the server is treated as a protocol violation and raises KalshiError.

For batch cancels, build a BatchCancelOrdersV2Request. Each entry can carry its own subaccount / market_ticker:

from kalshi import BatchCancelOrdersV2Request, BatchCancelOrdersV2RequestOrder

result = client.orders.batch_cancel_v2(request=BatchCancelOrdersV2Request(
    orders=[
        BatchCancelOrdersV2RequestOrder(order_id="ord_abc", subaccount=0),
        BatchCancelOrdersV2RequestOrder(order_id="ord_def", subaccount=1),
    ],
))
for entry in result.orders:
    if entry.error is not None:
        print("failed:", entry.order_id, entry.error)
    else:
        print(entry.order_id, "canceled")

Cancel is not server-idempotent

cancel_v2(...) (and batch_cancel_v2(...)) propagate a 404 as KalshiNotFoundError when the order is already canceled, fully filled, or never existed. The SDK does not automatically swallow that — the caller owns safe-retry idempotency. Treat 404 as "already canceled" only if you can rule out a typo'd order_id:

from kalshi.errors import KalshiNotFoundError

try:
    client.orders.cancel_v2(order_id)
except KalshiNotFoundError:
    pass  # already canceled — idempotent

Amend

from decimal import Decimal
from kalshi import AmendOrderV2Request

resp = client.orders.amend_v2(
    "ord_abc",
    subaccount=0,                          # query param
    request=AmendOrderV2Request(
        ticker="KXPRES-24-DJT",            # same shape as create
        side="bid",                        # BookSideLiteral
        price=Decimal("0.66"),
        count=Decimal("12"),               # total/max fillable count
        exchange_index=0,                  # body field
    ),
)
print(resp.old_order.order_id, resp.order.order_id)

AmendOrderV2Response carries both the previous and the new order. ticker and side are part of the API's amend payload.

Decrease

Reduce the size of a resting order without canceling. Exactly one of reduce_by or reduce_to:

from decimal import Decimal
from kalshi import DecreaseOrderV2Request

resp = client.orders.decrease_v2(
    "ord_abc",
    subaccount=0,
    request=DecreaseOrderV2Request(reduce_by=Decimal("3")),   # decrease size by 3
)

resp = client.orders.decrease_v2(
    "ord_abc",
    request=DecreaseOrderV2Request(reduce_to=Decimal("5")),   # decrease size to 5
)

DecreaseOrderV2Request enforces XOR on reduce_by / reduce_to at construction — passing both or neither raises ValidationError.

List orders

for order in client.orders.list_all(status="resting"):
    print(order.order_id, order.ticker, order.remaining_count)

status accepts an OrderStatusLiteral: "resting", "canceled", "executed". min_ts / max_ts (Unix seconds) bound by created time.

Fills (fills / fills_all) moved to PortfolioResource in v3.0.0 — see Portfolio › Fills. client.orders.fills(...) / client.orders.fills_all(...) still work in v3.0.0 but emit a DeprecationWarning and will be removed in a future release.

Queue position

positions = client.orders.queue_positions(market_tickers=["KXPRES-24-DJT"])
for p in positions:
    print(p.order_id, p.queue_position)

q = client.orders.queue_position("ord_abc")  # returns Decimal

queue_position(order_id) returns a bare Decimal, not a model — that's the shape Kalshi's endpoint emits.

Batch error handling

Per-entry error handling is built into BatchCreateOrdersV2ResponseEntry and BatchCancelOrdersV2ResponseEntry — successful entries carry the order/fill data, failed entries carry an error dict with the rest of the fields nulled (create) or zeroed (cancel).

subaccount / exchange_index routing

This trips people up. The V2 spec routes subaccount and exchange_index to different places depending on the endpoint:

Endpoint subaccount exchange_index
cancel_v2 query query
amend_v2 query body (on AmendOrderV2Request)
decrease_v2 query body (on DecreaseOrderV2Request)
create_v2 / batch_*_v2 n/a — body on request model body

This is faithful to the OpenAPI spec — amend_v2/decrease_v2 only declare SubaccountQueryDefaultPrimary in their parameters list, while cancel_v2 declares both that and ExchangeIndexQuery. Don't try to "normalize" it; the SDK matches the spec.

Reference

kalshi.resources.orders.OrdersResource

OrdersResource(transport: SyncTransport)

Bases: SyncResource

Sync orders API.

fills

fills(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    cursor: str | None = None,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> Page[Fill]

List trade fills.

fills_all

fills_all(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> Iterator[Fill]

Auto-paginate trade fills.

cancel_v2

cancel_v2(
    order_id: str,
    *,
    subaccount: int | None = None,
    exchange_index: int | None = None,
    market_ticker: str | None = None,
    extra_headers: dict[str, str] | None = None
) -> CancelOrderV2Response

Cancel an event-market order (V2).

market_ticker (spec v3.22.0) is required when exchange_index is -1 (auto-route by ticker); omit it otherwise.

amend_v2

amend_v2(
    order_id: str,
    *,
    request: AmendOrderV2Request,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | 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.

decrease_v2

decrease_v2(
    order_id: str,
    *,
    request: DecreaseOrderV2Request,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | 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.

kalshi.resources.orders.AsyncOrdersResource

AsyncOrdersResource(transport: AsyncTransport)

Bases: AsyncResource

Async orders API.

list_all

list_all(
    *,
    ticker: str | None = None,
    event_ticker: list[str] | str | None = None,
    status: OrderStatusLiteral | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> AsyncIterator[Order]

Returns an async iterator — use async for.

fills async

fills(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    cursor: str | None = None,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> Page[Fill]

List trade fills (async).

fills_all

fills_all(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> AsyncIterator[Fill]

Auto-paginate trade fills (async). Use async for.

cancel_v2 async

cancel_v2(
    order_id: str,
    *,
    subaccount: int | None = None,
    exchange_index: int | None = None,
    market_ticker: str | None = None,
    extra_headers: dict[str, str] | None = None
) -> CancelOrderV2Response

Cancel an event-market order (V2).

market_ticker (spec v3.22.0) is required when exchange_index is -1 (auto-route by ticker); omit it otherwise.

amend_v2 async

amend_v2(
    order_id: str,
    *,
    request: AmendOrderV2Request,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | 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.

decrease_v2 async

decrease_v2(
    order_id: str,
    *,
    request: DecreaseOrderV2Request,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | 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.