Placing and Canceling Kalshi Orders via the REST API
Move from legacy endpoints to V2 by May 2026 or face higher write-rate limits.

Kalshi now places and cancels orders through a distinct set of V2 endpoints, and an integration that still relies on the older request shape will break when traffic increases. Create Order (V2), Cancel Order (V2), Decrease Order (V2), Batch Create Orders (V2), Batch Cancel Orders (V2), and Amend Order (V2) are not incremental revisions layered onto a familiar structure. They are a discrete surface, rooted under POST /trade-api/v2/portfolio/events/orders and DELETE /trade-api/v2/portfolio/events/orders/{order_id}, along with their batched counterparts. Clients that stayed on the legacy path faced elevated write-rate-limit costs starting May 25, 2026, well ahead of full deprecation, a clear signal from Kalshi that migration carried a deadline. What follows is a walkthrough of what that V2 surface actually demands at each stage of an order's life, from finding a market to canceling a resting position, and the specific places where a developer coming from the old pattern will trip.
How market discovery works before placing any order
No order can be submitted without a valid market ticker, and finding one requires no credentials. Market data is the one part of the Kalshi API open to unauthenticated calls: a GET to /trade-api/v2/markets?status=open returns open markets in paginated form, without any signing or key exchange required. An empty page back from that call is a legitimate response rather than a sign of malfunction, and any collector built against this endpoint needs to walk the full pagination rather than assume the first page contains everything available. The value that matters out of this response is the ticker string itself, something like HIGHNY-24JAN01-T60, and that string, not the market's title, is what belongs in the ticker field of the order request that follows. Integrations that need to scan several markets before choosing one can also reach for the batched orderbook endpoint, GET /markets/orderbooks, added to the API in March 2026, which accepts multiple tickers in a single call.
What the V2 create-order request requires
A POST to /trade-api/v2/portfolio/events/orders will not succeed unless a specific set of fields is present and correctly typed, and the quick-start guide's Python example lays out that full set alongside the signing call that has to accompany it. The ticker comes straight from market discovery. client_order_id is a UUID generated fresh for each call, with the quick-start guide recommending UUID4, and it does more than tag the request: it is the mechanism that makes retries safe, a point worth returning to below. side takes the value "bid" or "ask", never "yes" or "no", and this single substitution accounts for the most common error developers carry over from the V1 pattern. count is a fixed-point string, such as "1" or "10.00". price is also a fixed-point dollar string, for example "0.0100" for one cent, and the valid range runs from one cent up to just under one dollar. time_in_force takes a value like "good_till_canceled" or "immediate_or_cancel," and an optional expiration_time field accepts a Unix timestamp in seconds as an integer, which can later be set or cleared independently through an amend call. self_trade_prevention_type, with values such as "taker_at_cross" or "maker," rounds out the required set. post_only, cancel_order_on_pause, reduce_only, subaccount, and exchange_index are available but optional.
For anyone used to thinking in terms of YES and NO contracts, the side field maps as follows. Buying YES at a price p means submitting side: "bid" at that price p. Buying NO at a price p means submitting side: "ask" at a price of 1 minus p. Sells mirror both of these relationships. Getting this backward does not produce an error; it produces a filled order on the wrong side of the market, which is a far more expensive mistake to catch.
The client_order_id field is where the request format quietly solves a problem that trips up a lot of REST integrations: what to do when a request times out and you don't know if it landed. Resubmitting the same create-order call with the same client_order_id after a network failure causes the API to return 409 Conflict instead of creating a second order, so the endpoint can be retried from the application layer without risk of double-filling, as long as the UUID is reused deliberately on each retry. A successful call returns HTTP 201 along with a server-generated order_id and a remaining_count, and both values need to be stored locally, since every subsequent cancel or amend call depends on having the order_id on hand.
Testing this deduplication logic properly, rather than trusting that it works, is its own discipline. A platform that remembers order state across calls, tracking which UUIDs have already been submitted, returning 409 on resubmission, and keeping the order ledger consistent, lets a developer confirm idempotency works as described without risking a double fill against a live account. This is precisely the kind of behavior that is hard to verify safely in production and easy to verify against a stateful simulator like Rystic's.
The signing procedure that blocks most first integrations
Every authenticated call to the Kalshi API needs three headers: KALSHI-ACCESS-KEY, which identifies the key being used; KALSHI-ACCESS-SIGNATURE, the computed signature itself; and KALSHI-ACCESS-TIMESTAMP, a millisecond Unix timestamp. The message that gets signed is built by concatenating the timestamp in milliseconds, the HTTP method, and the request path, where that path runs from the API root, something like /trade-api/v2/portfolio/events/orders, excluding the query string.
Two mistakes account for nearly every 401 a new integration produces. The first is supplying the timestamp in seconds instead of milliseconds, since the signing specification calls for milliseconds specifically and a seconds-based timestamp will simply fail the check with no further explanation. The second is including query parameters in the signed path, when the specification requires the signed string to stop before the question mark. Both errors produce the same opaque authentication failure, which makes them easy to confuse with a credentials problem when the actual issue is in how the pre-sign string was assembled.
Kalshi supports two signing algorithms. RSA uses a 2048-bit key, signed with RSA-PSS, SHA-256 for both the hash and the MGF1 function, a salt length equal to the digest length, and base64 encoding of the result; RSA is the default algorithm when key_type is left off a POST to /trade-api/v2/api_keys/generate, even though the web app itself now defaults new users to Ed25519. Ed25519, specified in RFC 8032, signs the identical pre-sign text at lower computational cost on the client side, and a developer can either register an existing public key through POST /trade-api/v2/api_keys or generate a new key pair through the same /api_keys/generate endpoint with key_type set to "ed25519." Whichever algorithm is chosen, the private key is returned exactly once at generation and cannot be retrieved again afterward, so it needs to be captured immediately and kept out of committed files, screenshots, and support tickets. Production and demo credentials are fully separate from one another, and a signature built with a production key will fail against the demo environment just as surely as the reverse.
Debugging a bad signature against the live API means burning a real request and getting back a generic 401 with no indication of which part of the signing chain is wrong. A locally hosted simulator that replicates Kalshi's authentication surface, accepting valid signatures, rejecting malformed ones, and returning the same error shapes as the live system, gives a developer a way to work through the RSA and Ed25519 signing logic offline before ever making a real authenticated call.
Canceling an order: single and batch cancel responses
Cancel Order (V2) is a DELETE to /trade-api/v2/portfolio/events/orders/{order_id}, and it cancels every remaining resting contract tied to that order in one action. The response it returns is compact rather than a full order record: order_id, client_order_id, reduced_by, and ts_ms. reduced_by is worth reading carefully, because it is the field most likely to be misunderstood. It reports the number of contracts that were still resting at the moment of cancellation, returned as a fixed-point string such as "10.00," not the order's original size at the time it was placed. An order that started at 10 contracts and had already filled 7 before being canceled will return a reduced_by of "3.00," and treating that figure as the original count will throw off any downstream accounting. ts_ms is the matching-engine's timestamp for the cancellation itself.
A single cancel call accepts the order_id as a required path parameter and an optional subaccount query parameter, an integer where 0 represents the primary account and 1 through 63 identify subaccounts, defaulting to 0 when omitted. Canceling a single order costs 2 rate-limit tokens, well below the 10-token default that applies to most write calls, which makes cancellation one of the cheaper actions available in the API relative to placing new orders. Batch cancel endpoints extend this to all resting Predictions or margin orders, either across every subaccount or scoped to one selected subaccount, and orders placed in the minute following a batch cancel request may also get swept up and canceled.
Looking up what happened to a canceled order afterward means querying GET /portfolio/orders with a status=canceled filter, though orders that were canceled or fully executed before the historical cutoff only show up through GET /historical/orders instead.
Rate limiting: two independent buckets
Kalshi's rate limiting runs on two separate cost structures rather than one uniform cap, and the asymmetry between them shapes how a trading loop should be built. Writes carry a default token cost, cancels cost less per call than that default, and single-order reads are cheaper still. Batch endpoints consume tokens per item inside the batch, so bundling ten cancels into one batch call reduces round trips to the API but does not waive the cost of each individual cancellation inside it.
So if a strategy polls order state aggressively through reads while canceling frequently through cheap writes, it will run into its read budget well before its write budget becomes the binding constraint. A loop built around that reality should treat the read allowance, not the cancel allowance, as the resource to budget carefully.
When to use WebSocket alongside REST for order management
Kalshi's own documentation draws a clear line between what REST is for and what WebSocket is for in order management, and conflating the two leads to either wasteful polling or blind spots in order state. Placing, amending, and canceling orders belong on REST, because commands need an explicit response, a stable client identifier for deduplication, and a bounded retry path if something goes wrong. Streaming updates, meaning orderbook changes, trade prints, and order lifecycle events, belong on WebSocket, where a persistent connection avoids the cost of tight polling loops and reduces the risk of acting on stale data.
A production integration uses both channels together: REST handles placing and reconciling orders, while WebSocket tracks orderbook and fill updates without requiring a poll of every market every few seconds. The user_orders channel now attaches an exchange_index to each order update, identifying the specific exchange shard where that order lives, and this matters if your strategy operates across multiple shards at once.
Recovering from a WebSocket disconnect has a specific correct sequence: reconnect, resubscribe to the relevant channels, and then rebuild any uncertain state through REST calls before taking further action. Resuming optimistically from the last event received before the drop risks acting on state that changed while the connection was down, since there is no guarantee that nothing moved during the gap.
Order groups as an automated cancel backstop
Order groups give a trading strategy an automated circuit breaker on top of whatever cancel logic exists at the application layer. A group moves into its triggered state under any of three conditions: rolling 15-second volume exceeds the group's contracts limit, a manual Trigger action is issued against it, or the limit itself is updated downward to a value below the current rolling volume.
In its active state, a group allows orders to be placed normally while tracking rolling volume against a configurable limit, which can be set anywhere from 1 to 1,000,000 contracts. Once triggered, every resting order in the group gets canceled and new orders are rejected outright until a Reset clears both the triggered state and the rolling volume counter back to zero. Deleting a group removes it entirely and cancels every resting order inside it, functioning as the full unwind option when a position needs to come off completely.
This mechanism exists alongside explicit cancel calls. For a strategy with a defined limit on acceptable exposure, an order group acts as a safer backstop than counting on the application layer to issue cancels correctly under load or in the aftermath of a disconnect, precisely the conditions under which application-level logic is most likely to fail.
Using the demo environment to validate plumbing before touching production
The demo environment runs on mock funds and a credential set fully separate from production, and it is the right place to work through RSA or Ed25519 signing, the full order lifecycle, WebSocket reconnection behavior, and exponential-backoff retry logic before any of it touches real money. Batch order endpoints, notably, are open to every API user in demo, while the same endpoints in production require an Advanced tier account or higher, which makes demo the only place some developers can exercise batch behavior at all before upgrading.
Demo prices will not necessarily track what real markets are doing, so fills observed in demo cannot be used to validate a strategy's edge, its slippage assumptions, or how it behaves under partial fills against a realistic book. Testing in demo needs to be understood as a plumbing exercise: it confirms that signing works, that the order lifecycle behaves as documented, and that reconnection logic holds up, without confirming anything about whether a strategy is actually good. If you test against portfolio responses, note that selected _dollars fields now emit up to six decimal places, sourced from micro-cent values upstream, so naive string comparison in your test assertions will break against these values, and you need numeric comparison with a sensible tolerance instead.
That split leaves a real gap. Edge-case behaviors, such as partial fills, post-only orders that cross and cancel, or order-group triggers under realistic volume, are only fully observable in production, where observing them means taking on real financial risk to do it. The sound approach is to use demo for plumbing and validate the edge cases with small live positions rather than assuming demo fills are a stand-in for production behavior. This is the specific gap a locally hosted, stateful simulator is built to close: Rystic's Kalshi simulator remembers state across calls, so placing an order, listing it, and canceling it all update the book consistently, and it ships with pre-seeded realistic data while being checked against the live API before each release. That combination lets a developer exercise partial fills, order-group triggers, and cancel edge cases without live credentials, rate-limit exposure, or real money on the line, and for teams running automated strategies or AI agents that call Kalshi as a tool, it scales horizontally across containers with state that stays deterministic and controllable throughout.


