Skip to content

Errors

All SDK exceptions inherit from KalshiError. HTTP responses are mapped to typed exceptions in the transport layer before any resource code sees them, so you can try/except against the specific failure mode instead of inspecting status codes.

from kalshi import KalshiClient, KalshiNotFoundError, KalshiRateLimitError

try:
    market = client.markets.get("DOES-NOT-EXIST")
except KalshiNotFoundError as e:
    print(e.status_code, str(e))
except KalshiRateLimitError as e:
    print("backoff hint:", e.retry_after)

Hierarchy

KalshiError                          # base, .status_code: int | None
├── KalshiAuthError                  # 401 / 403
│   └── AuthRequiredError            # preflight on unauth'd client
├── KalshiNotFoundError              # 404
├── KalshiValidationError            # 400 (carries .details: dict[str, str])
├── KalshiRateLimitError             # 429 (carries .retry_after: float | None)
├── KalshiConflictError              # 409 (e.g., duplicate client_order_id)
├── KalshiTimeoutError                # request timed out; commit-status unknown on POST
├── KalshiPoolExhaustedError         # local pool full; request never sent
├── KalshiNetworkError               # TCP/TLS/DNS/protocol fault after retries
├── KalshiServerError                # 5xx
└── KalshiWebSocketError             # base for WS errors
    ├── KalshiConnectionError        # handshake / reconnect failure
    ├── KalshiSequenceGapError       # exposed for custom resync handlers
    ├── KalshiBackpressureError      # queue full with ERROR overflow
    └── KalshiSubscriptionError      # subscribe / unsubscribe rejected

The KalshiError base carries an optional status_code: int | None. HTTP-derived exceptions populate it; WebSocket and AuthRequiredError leave it None.

HTTP status → exception

Status Exception Notes
400 KalshiValidationError .details is populated from body["details"] or body["errors"] when present and dict-shaped.
401 / 403 KalshiAuthError Bad signature, expired key, missing scope.
404 KalshiNotFoundError Unknown ticker, missing order, etc.
409 KalshiConflictError Duplicate client_order_id or other state conflict.
429 KalshiRateLimitError .retry_after parsed from the Retry-After header if it's a non-negative finite numeric (HTTP-date form falls back to computed backoff).
5xx KalshiServerError All server-side failures.
anything else KalshiError Catch-all, with status_code set.

AuthRequiredError is the one HTTP-shaped exception that fires before the network — calling a private endpoint on an unauthenticated client raises it preflight, without sending the request. status_code is None. Since it subclasses KalshiAuthError, catching the parent covers both.

The mapping is performed by _map_error in kalshi/_base_client.py.

Validation errors

Two distinct things can go wrong with payloads:

  • Server-side request validation (400 Bad Request) — surfaces as KalshiValidationError, with details: dict[str, str] populated from the server's response when available. Use it to report field-level problems back to the user.
  • Pydantic validation on the response — if the server returns a body that doesn't match the SDK's typed model (a wire-format drift), Pydantic's own ValidationError bubbles up. It is not a subclass of KalshiError — treat it as a bug report against the SDK's model layer, not as a transient error.

Client-side validation on request bodies (Pydantic models with extra="forbid") also raises Pydantic's ValidationError directly, before the network. A misspelled kwarg in a resource method raises TypeError first; phantom keys passed via request=Model(...) fail at Model(...) construction.

Transport-level wrapping

Non-HTTP failures are wrapped to a typed exception with the original as __cause__:

  • Timeouts raise KalshiTimeoutError. On retryable verbs (GET, HEAD, OPTIONS), the transport retries first and only raises once retries are exhausted. On POST / DELETE the timeout is raised immediately — the server may or may not have processed the request. For order create, query with client_order_id to determine whether the request committed.
  • Connection pool exhaustion raises KalshiPoolExhaustedError. The request never reached the wire, so it's safe to retry regardless of HTTP method. Persistent pool exhaustion means you should raise KalshiConfig.limits.max_connections.
  • Network failures (DNS, TLS, TCP RST, HTTP/2 RST_STREAM, half-close) raise KalshiNetworkError. On idempotent verbs (GET, HEAD, OPTIONS) the transport retries first; on POST / DELETE / PUT only httpx.ConnectError is retried (request never reached the wire — mirrors KalshiPoolExhaustedError). All other transport faults on non-idempotent verbs surface immediately so the caller can reconcile a possibly-committed request via client_order_id. The original httpx exception is preserved via __cause__.

Catching everything from the SDK

from kalshi import KalshiError

try:
    do_things(client)
except KalshiError as e:
    log.exception("SDK call failed (status=%s)", e.status_code)

A bare except KalshiError covers every SDK-raised exception except the Pydantic ValidationError you'd get from a malformed response (that one is a bug, not a runtime error).

WebSocket errors

WebSocket failures are a separate sub-hierarchy under KalshiWebSocketError:

  • KalshiConnectionError — raised when the initial connect fails, when the auth handshake is rejected, or when ws_max_retries is exhausted on a reconnect attempt. Also surfaces from ConnectionManager.send() / recv() if you call them without being connected.
  • KalshiSubscriptionError — server rejected a subscribe / unsubscribe / update_subscription command. Carries:
    • error_code: int | None — the server's machine-readable code (also accepted positionally for back-compat).
    • channel: str | None — the channel name involved.
    • client_id: int | None — the durable client-side id used for the command.
    • op: Literal["subscribe", "unsubscribe", "update_subscription"] | None — which operation was rejected.
  • KalshiBackpressureError — raised from MessageQueue.put() when the queue is full and the overflow strategy is ERROR. The receive loop treats this as fatal: it broadcasts sentinels to every active iterator and exits. See WebSocket → Backpressure. Carries:
    • channel: str | None — the channel whose queue overflowed.
    • sid: int | None — server-side subscription id (populated at the broadcast_error site since the queue itself doesn't track sid).
    • client_id: int | None — durable client-side id.
    • maxsize: int | None — the configured queue ceiling at the time of overflow.
  • KalshiSequenceGapError — exposed for callers wiring their own resync logic on top of the SDK's primitives. The built-in receive loop does not raise this — it recovers from gaps silently (drops the message, clears local orderbook state, waits for the next snapshot). Carries:
    • channel: str | None — the channel where the gap appeared.
    • sid: int | None — server-side subscription id.
    • client_id: int | None — durable client-side id.
    • last_seq: int | None — last in-order sequence successfully consumed.
    • next_seq: int | None — sequence number observed that exposed the gap.

A subscription's iterator continues to yield across reconnects — the SDK re-issues the subscribe and patches the new server-side sid into the durable client-side id. You won't see KalshiConnectionError from inside async for; you'll see it from the connect() context manager if the socket can't be re-established at all.

FIX errors

The FIX subsystem has its own sub-hierarchy under KalshiFixError (itself a subclass of KalshiError, so except KalshiError still catches it). Import them from kalshi.fix. FIX is a TCP/TLS protocol with no HTTP status, so status_code is always None.

  • FixConnectionError — TCP/TLS connect failed, was refused, or the reconnect attempts were exhausted. Original transport error via __cause__.
  • FixLogonError — the gateway rejected the logon; .reason carries the Text from the Logout when present (bad signature, CompID, SendingTime skew, a missing ResetSeqNumFlag=Y).
  • FixSequenceError — an unrecoverable sequence condition (a backwards MsgSeqNum, or a forward gap on a non-retransmission session); carries .expected / .received.
  • FixCodecError — a malformed frame (BeginString / BodyLength / CheckSum / tag=value); .raw holds the offending bytes when available.
  • FixDecodeError — a registered inbound message failed schema validation (one off-spec field); carries .raw + .msg_type, original via __cause__. See FIX → Error handling.
  • FixRejectError — the gateway rejected a message we sent (session Reject 35=3 or BusinessMessageReject 35=j); carries the structured reject fields.
  • FixSessionError — a session-level protocol violation or unexpected lifecycle event.

See also

  • Retries & idempotency — what does and doesn't get retried, why POST/DELETE never retry, recommended patterns for safely retrying writes.

Exception reference

kalshi.errors.KalshiError

KalshiError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: Exception

Base exception for all Kalshi SDK errors.

retry_after carries the server's RFC 7231 §7.1.3 Retry-After hint (parsed to seconds) when present. Populated for 408/429/503/504 and other retryable 5xx responses; None otherwise. The transport retry loop reads it via plain attribute access regardless of error subclass (#322).

kalshi.errors.KalshiAuthError

KalshiAuthError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

Authentication or authorization failure (401/403).

kalshi.errors.AuthRequiredError

AuthRequiredError(message: str | None = None)

Bases: KalshiAuthError

Raised when an unauthenticated client calls a private endpoint.

kalshi.errors.KalshiNotFoundError

KalshiNotFoundError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

Resource not found (404).

kalshi.errors.KalshiValidationError

KalshiValidationError(
    message: str,
    status_code: int | None = None,
    details: dict[str, str] | None = None,
)

Bases: KalshiError

Request validation failure (400). May include field-level details.

kalshi.errors.KalshiRateLimitError

KalshiRateLimitError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

Rate limit exceeded (429). Check retry_after for backoff hint.

kalshi.errors.KalshiConflictError

KalshiConflictError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

409 Conflict (e.g., duplicate client_order_id).

kalshi.errors.KalshiTimeoutError

KalshiTimeoutError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

Request timed out. The server may or may not have processed it.

On POST endpoints like order create, query the server using client_order_id to determine whether the request committed.

kalshi.errors.KalshiPoolExhaustedError

KalshiPoolExhaustedError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

Connection pool exhausted; request never reached the wire.

Safe to retry regardless of HTTP method. Raise KalshiConfig.limits.max_connections if this fires under normal load.

kalshi.errors.KalshiNetworkError

KalshiNetworkError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

Transport-level network failure (TCP/TLS/DNS/protocol).

Raised after the retry loop has exhausted attempts on a transient httpx.TransportErrorConnectError, ReadError, WriteError, RemoteProtocolError, etc. The original httpx exception is chained via __cause__.

kalshi.errors.KalshiServerError

KalshiServerError(
    message: str,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Bases: KalshiError

Server-side error (5xx).

kalshi.errors.KalshiWebSocketError

KalshiWebSocketError(message: str)

Bases: KalshiError

Base exception for all WebSocket errors.

kalshi.errors.KalshiConnectionError

KalshiConnectionError(message: str)

Bases: KalshiWebSocketError

Connection failed, handshake rejected, or max retries exceeded.

kalshi.errors.KalshiSequenceGapError

KalshiSequenceGapError(
    message: str,
    *,
    channel: str | None = None,
    sid: int | None = None,
    client_id: int | None = None,
    last_seq: int | None = None,
    next_seq: int | None = None
)

Bases: KalshiWebSocketError

Sequence gap detected that could not be resolved via resync.

kalshi.errors.KalshiBackpressureError

KalshiBackpressureError(
    message: str,
    *,
    channel: str | None = None,
    sid: int | None = None,
    client_id: int | None = None,
    maxsize: int | None = None
)

Bases: KalshiWebSocketError

Message queue overflow with ERROR strategy.

Carries structured context so consumers iterating multiple subscriptions can route the failure by channel/sid/client_id without parsing the error message string.

kalshi.errors.KalshiOrderbookUnavailableError

KalshiOrderbookUnavailableError(
    message: str, *, ticker: str | None = None
)

Bases: KalshiWebSocketError

Local orderbook is empty between teardown and resync snapshot.

Raised by :class:KalshiWebSocket.orderbook when the underlying delta stream yields an item but the orderbook manager has no book for the requested ticker. This can happen between a gap-triggered remove_by_sid and the resync snapshot landing — the iterator previously yielded an empty :class:Orderbook, which is indistinguishable from a real market with zero liquidity and could cause a strategy to place orders against an empty book.

kalshi.errors.KalshiSubscriptionError

KalshiSubscriptionError(
    message: str,
    error_code: int | None = None,
    *,
    channel: str | None = None,
    client_id: int | None = None,
    op: (
        Literal[
            "subscribe",
            "unsubscribe",
            "update_subscription",
        ]
        | None
    ) = None
)

Bases: KalshiWebSocketError

Subscribe/unsubscribe request rejected by server.

kalshi.fix.KalshiFixError

KalshiFixError(message: str)

Bases: KalshiError

Base exception for all Kalshi FIX errors.

FIX is a TCP/TLS protocol with no HTTP status, so status_code is always None (the field exists on :class:kalshi.errors.KalshiError for the REST transport and is kept for cross-surface except KalshiError use).

kalshi.fix.FixConnectionError

FixConnectionError(message: str)

Bases: KalshiFixError

TCP/TLS connection failed, was refused, or max reconnect attempts exceeded.

The original transport exception (OSError / ssl.SSLError / asyncio.TimeoutError) is chained via __cause__.

kalshi.fix.FixLogonError

FixLogonError(message: str, *, reason: str | None = None)

Bases: KalshiFixError

Logon (35=A) was rejected by the acceptor.

The acceptor answers a failed Logon with a Logout (35=5) carrying a human-readable Text (58); that text is surfaced in reason when present. Common causes: bad RawData signature, SendingTime outside the 30s skew window (SessionRejectReason=10), unknown CompID, or a missing ResetSeqNumFlag=Y on a non-retransmission session.

kalshi.fix.FixSequenceError

FixSequenceError(
    message: str,
    *,
    expected: int | None = None,
    received: int | None = None
)

Bases: KalshiFixError

An unrecoverable sequence-number condition.

Two cases, both fatal to the session:

  • The acceptor's MsgSeqNum is lower than expected (per spec the connection is terminated — a lower number means the peer's view of the session is corrupt).
  • A forward gap was detected on a session that does not support retransmission (KalshiNR / KalshiDC), so it cannot be recovered via ResendRequest.

kalshi.fix.FixCodecError

FixCodecError(message: str, *, raw: bytes | None = None)

Bases: KalshiFixError

A FIX frame could not be encoded or decoded.

Raised on malformed framing — missing BeginString / BodyLength / CheckSum, a BodyLength that disagrees with the bytes on the wire, a CheckSum mismatch, or a field that is not tag=value. raw carries the offending bytes (truncated) for debugging when available.

kalshi.fix.FixDecodeError

FixDecodeError(
    message: str, *, raw: RawMessage, msg_type: str
)

Bases: KalshiFixError

A registered inbound application message failed schema validation.

Distinguishes a malformed known message (a real message with an off-spec field — e.g. a bad DollarDecimal / FixedPointCount) from an unregistered message type. :func:~kalshi.fix.messages.decode_app_message collapses both to None; :func:~kalshi.fix.messages.decode_app_message_strict raises this for the former so the failure is observable (see FixSession's on_decode_error hook). raw carries the offending message and msg_type its MsgType (both always present); the underlying validation error chains via __cause__.

kalshi.fix.FixRejectError

FixRejectError(
    message: str,
    *,
    ref_seq_num: int | None = None,
    ref_tag_id: int | None = None,
    ref_msg_type: str | None = None,
    reject_reason: int | None = None,
    text: str | None = None
)

Bases: KalshiFixError

The acceptor rejected a message we sent.

Raised for an inbound session-level Reject (35=3) or BusinessMessageReject (35=j). Carries the structured reject fields so callers can route on them without parsing Text.

kalshi.fix.FixSessionError

FixSessionError(message: str)

Bases: KalshiFixError

A session-level protocol violation or unexpected lifecycle event.

Covers an unexpected Logout, a liveness failure (no Heartbeat/TestRequest response within the interval), or an inbound message that breaks the session state machine.