Protocol overview
Our websockets api is only served over an encrypted connection.
We provide multiple channels of information over a single connection. So after establishing a WS connection, the client has the freedom to chose which channels they are interested in at any given moment. To express the intention of receiving messages for a specific channel the client has to send subscription messages that will be defined latter on in this document.
This endpoint only can be used by signed-up users (private channels only).
The communication between the user (client) and Kalshi's backend (server) is async. All the messages exchanged should be encoded in JSON format.
We call messages in the client -> server direction commands, these commands specify the set of message types and specific markets the client is interested on.
Typically, the client starts the connection and sends commands to the server and eventually receives messages related to the commands whenever there is an update on the server.
Example:
(cli => srv)
(srv => cli)
=> cmd1
=> cmd2
<= msg_related_to_cmd_2
<= msg_related_to_cmd_1
<= msg_related_to_cmd_2
...
Connecting
You probably want to connect to our websockets api in your favorite language.
We cannot provide language specific code samples, but there is a very helpful CLI tool called Websocat that can be used to get you started with the websockets api very quickly with low friction.
Try the snippet below after installing websocat:
(PROD)
local LOGIN_RESPONSE=$(curl -s --request POST \
--url https://trading-api.kalshi.com/trade-api/v2/login \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{ "email": "YOUR_EMAIL", "password": "YOUR_PASSWORD"}')
local KALSHI_API_TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"token":"[^"]*' | grep -o '[^"]*$')
websocat -v wss://trading-api.kalshi.com/trade-api/ws/v2 --header="Authorization:Bearer $KALSHI_API_TOKEN"
(DEMO)
local LOGIN_RESPONSE=$(curl -s --request POST \
--url https://demo-api.kalshi.co/trade-api/v2/login \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{ "email": "YOUR_EMAIL", "password": "YOUR_PASSWORD"}')
local KALSHI_API_TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"token":"[^"]*' | grep -o '[^"]*$')
websocat -v wss://demo-api.kalshi.co/trade-api/ws/v2 --header="Authorization:Bearer $KALSHI_API_TOKEN"
You should see the output below in case it works:
websocat -v wss://trading-api.kalshi.com/v1/ws --header="Authorization:Bearer $KALSHI_API_TOKEN" --origin=https://kalshi.com
[INFO websocat::lints] Auto-inserting the line mode
[INFO websocat::stdio_threaded_peer] get_stdio_peer (threaded)
[INFO websocat::ws_client_peer] get_ws_client_peer
[INFO websocat::ws_client_peer] Connected to ws
If you saw the output above it means you are connected.
A few helpful observations:
- Websocket will mix the messages sent with the messages received in your terminal session.
- You can copy and paste json messages in your shell to talk to the websocket api.
- Remember to minify the json messages in a single line before pasting in the shell. If you try passing a multi line message without proper escaping it won't work.
- You can remove the -v flag in the command above, if you want to see only the data messages and remove the debugging / heartbeat logs.
Commands
The initial version of the protocol provides only subscription commands.
Eventually, we may want to offer a complete trading API through WebSocket, but, for now, you should continue using the REST API for trading, while the WS interface provides a stream of notifications/updates.
Without WS, you would have to be constantly polling the GET end-points from api v2 to keep yourself up to date with what is going on in the markets.
Each command uses the following format:
{
"id": <int>, // Unique ID of the command request.
"cmd": <string>, // Command name: "subscribe" or "update_subscription" or "unsubscribe".
"params": {...} // Params that are specific to the given cmd.
}
Given that our protocol is async, we use the id field to correlate commands with further data messages from the server. When the client includes an id in a command, the server returns that id value in messages related to that command, just so you could recognize them in the message stream. The id is 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.
Command "subscribe"
Subscription to one or many channels. Example:
{"id": 1,"cmd": "subscribe","params": {"channels": ["orderbook_delta", "ticker"], "market_ticker": "CPI-22DEC-TN0.1"}}
If the subscription is successful, the client will receive a separate "subscribed" message for each channel in the "channels" param field of the "subscribe" message. These are confirmation messages to indicate that the subscription was accepted successfully. The order of those messages is not guaranteed, as the protocol is async.
Note that the ID will be the same in both message, as they "respond" to the same command.
{
"id": 1,
"type": "subscribed",
"msg": {
"channel": "orderbook_delta",
"sid": 1
}
}
{
"id": 1,
"type": "subscribed",
"msg": {
"channel": "ticker",
"sid": 2
}
}
Important notes:
- You can only subscribe a single time to any channel.
- Some channels support subscribing to all markets, which is indicated by not passing any market ticket (more details will be provided in next sections).
If you retry the subscription message in the beginning of this section, but using "id": 2. Then you should get the error below as a response:
{"type":"error","id":2,"msg":{"code":6,"msg":"Already subscribed"}}
Each established subscription is identified by another ID (sid
) returned by the server.
This subscription ID can later be used by the client to cancel the subscription (equals to unsubscribe) or to update the subscription adding or removing markets.
The subscription id (sid
) is also used in messages sent by the server within each subscription so you can identify which message belong to which subscription when you receive them.
One or both of the subscription attempts can fail. For every failure, the server returns an error message, the format of which is described later on this page.
Subscription Modes
The subscribe command allows for three different subscription modes:
- All markets: Do not pass "market_ticker" or "market_tickers". You will get all fills for you account.
- List of markets: Pass the list of markets on "market_tickers". You will get updates only for this list of markets.
- Single market: Pass a single market ticker on "market_ticker". You will get updates only for this market.
Important notes:
- Availability of the "All markets" mode will depend on the specific channel, this is disclosed in the specific channel sections latter on.
- When the "All markets" mode is supported it will automatically track new markets that happen to open after your subscription was initiated.
Command "unsubscribe"
The client can cancel more than one or more subscriptions at once:
{"id": 124,"cmd": "unsubscribe","params": {"sids": [1, 2]}}
Confirmation messages are sent to the client independently, and, again, the order is not guaranteed:
{
"sid": 2,
"type": "unsubscribed"
}
{
"sid": 1,
"type": "unsubscribed"
}
Command "update_subscription"
The client can update an existing subscription. The update action should be specified in params in the “action” field.
All the channels (besides the "fill" channel) allow updates and supports two types of actions: add_markets
and delete_markets
.
Updating a subscription to include new markets is preferred over starting new subscriptions.
{
"id": 124,
"cmd": "update_subscription",
"params": {
"sids": [456], // Exactly one sid is required, even though it is an array
"market_tickers": ["<string>", ..., "<string>"], // market_ticker is also supported for a single market
"action": "add_markets|delete_markets"
}
}
In case of success an "ok" message will be received containing the list of market tickers after the update:
{
"id": 123,
"sid": 456,
"seq": 222,
"type": "ok",
"market_tickers": ["<string>", ..., "<string>"] // full list of market_tickers this subscription is tracking
}
Client-Side Recovery
The client should handle unexpected behavior in the following manner:
- Connection Closed:
Attempt to reconnect periodically. After successful reconnect, immediately resubscribe to all previously subscribed channels. Also, re-send all subscription commands from before the disconnection. - Server-Forced Unsubscription
Immediately attempt to re-subscribe to channel. If response is an error, handle it . - Subscribe Command Not Confirmed by Server
If the client does not receive a confirmation during a predefined window, it should close the connection and attempt to reconnect. - Connection Health Deteriorates
At any indication of an unhealthy connection, the client will close the connection and attempt to reconnect as described above.
Server messages
All specific message types are described in detail below and later in the "Subscription Channels" section.
Server message format
This section describes the general structure of messages.
The fields market as Required should be sent when responding to client commands or sending messages in subscription channels.
{
"id": <int>, // Optional command ID
"sid": <int>, // Optional subscription ID (described below)
"seq": <int>, // Optional sequence number (described below)
"type": <string>, // Required message type (described below)
"msg": {...} // Required body of the message
}
The id
field is optional and only included in a server message if the client provided it in the corresponding command.
When a channel subscription is established, messages within the channel will not have the id
field. Instead, we use sid
to identify the channel.
Error messages
Sometimes commands can't be executed on the server.
For example, a client cannot subscribe to a channel if the client is already subscribed to that channel (besides the fill channel where that is allowed). Or, symmetrically, it's impossible to cancel a non-existent subscription.
Here is an example error message that the server returns upon an attempt to open a second subscription to the same channel:
{
"id": 123,
"type": "error",
"msg": {
"code": 6,
"msg": "Already subscribed"
},
}
Subscription Channels
A subscription channel is a feed of logically related messages. It can include, for example, all trades on a specific market, or all orderbook offers on a specific market.
We are free to define channels and their logic however we see fit. Though, it's easier to reason about a channel if its purpose is clearly defined.
Below is a provisional list of channels. New channels will be added progressively.
Channels for a signed in user:
orderbook_delta
: price level updates on a market.ticker
: market price ticks.trade
: public trades.fill
: user fills.
Important observation: We standardize the channels with singular name form.
If you try to subscribe to an unexisting channel you should receive this message:
{
"id": 123,
"type": "error",
"msg": {
"code": 8,
"msg": "Unknown channel name"
},
}
Snapshot + Deltas
Some channels have to provide data in a highly reliable way so that the client could build a correct and consistent view from the received messages at any time.
Take, for example, the orderbook_delta
channel (which is going to be detailed in the next section). There are two types of messages that can be received from the server in an orderbook_delta
subscription.
- A "snapshot": a complete view of the order book.
- A "delta": an update to be applied to the current view of the order book.
Any missed "delta" message can result in a wrong state of the world on the client. To avoid the problem, we use sequential numbering for all snapshots and updates. The number is included as seq
field in every message.
The server ensures that, within a channel, messages are sent strictly sequentially. So the client would receive the snapshot in a message with seq: 1
, then an update message with seq: 2
, then all other updates with seq
values 3, 4, 5, etc. When the client detects a gap in the numbering, it has to re-subscribe to the channel and start again from a snapshot. Otherwise, the client might see wrong info.
If we don't need a snapshot for a particular channel and don't care if any message is accidentally lost, then you don't have to use the seq
feature. Whether a channel is seq-enabled or not will be explicitly noted below.
Order book channel
Channel: orderbook_delta
Purpose: A complete view of the order book's aggregated price levels on a given market and all further updates to it.
Subscription command (client => server)
Schema:
{
"id": <int>, // Required: Sequential command id maintained by the client.
"cmd": "subscribe", // Required: Specifies the subscription operation.
"params": {
"channels": ["orderbook_delta"], // Required: Specifies the orderbook_delta channel.
"market_tickers": ["<string>", "<string>", ...] // Required: Specifies the list of markets you wanna listen to.
}
}
This channel does not support the "All Markets" mode, it only supports "List of Markets" mode. So you always have to pass the list of markets you are subscribing to.
Example:
{"id": 23,"cmd": "subscribe","params": {"channels": ["orderbook_delta"], "market_tickers": ["FED-23DEC-T3.00", "CORIVER-2024-T1030"]}}
After the subscription is established, first, the client receives a snapshot. An example for the orderbook_snapshot message is shown below:
Ordebook snapshot message (server => client)
Schema:
{
"sid": <int>, // Required: Id of the subscription.
"type": "orderbook_snapshot", // Required: Message identifier, what you use to recognize this message type that arrive in your websocket connection.
"seq": 1, // Required: Sequential number, should be checked if you wanna guarantee you received all the messages.
"msg": {
"market_ticker": <string>, // Required: Market_ticker string, what you use to differentiate updates for different markets.
"yes": [
// Optional: This key will not exist if there is no Yes offers in the orderbook.
// In case it exists there will be many price levels, so for compactness, every element in the list is
// an array, with two integers not an object.
[<int>, <int>], // [Price in cents, Number of resting contracts]
...
[<int>, <int>]
],
"no": [
... // Optional. Same format as "yes" but for the NO side of the orderbook.
]
}
}
Example:
{
"type": "orderbook_snapshot",
"sid": 2,
"seq": 2,
"msg": {
"market_ticker": "FED-23DEC-T3.00",
"yes": [
[8, 300],
[22, 333]
],
"no": [
[54, 20],
[56, 146]
]
}
}
The client is expected to store the orderbook_snapshot and then keep listening for incoming "orderbook_delta" messages.
Ordebook delta message (server => client)
Schema:
{
"type": "orderbook_delta", // Required: Message identifier, what you use to recognize the message type for messages that arrive in your websocket connection.
"sid": <int>, // Required: Id of the subscription.
"seq": <int>, // Required: Sequential number, should be checked if you wanna guarantee you received all the messages.
"msg": {
"market_ticker": <string>, // Required: Market_ticker string, what you use to differentiate updates on different markets.
"price": <int>, // Required: Indicates the price level that is being changed in cents
"delta": <int>, // Required: Positive means increase, negative means decrease
"side": <string> // Required: "yes" or "no" to indicate the side of the orderbook that changed
}
}
Example:
{
"type": "orderbook_delta",
"sid": 2,
"seq": 3,
"msg": {
"market_ticker": "FED-23DEC-T3.00",
"price": 96,
"delta": -54,
"side": "yes"
}
}
The message above signals that the number of resting contracts at price 23 for Yes contracts on this market is now 88 contracts, which is 100 - 22.
Important note: You should still be tracking "orderbook_snapshot" messages as the subscription can update the server can resend the full state of the orderbook in case there are multiple changes at the same avoid. It can pack multiple delta messages in a single "orderbook_snapshot" message. In that case you should throw away whatever view of the orderbook you had and use the content of the message as the current state.
Ticker channel
Channel: ticker
Purpose: The list price ticker for a given market.
No snapshot is required. The server just sends the last price on the market when the price changes. On an active market, when the price changes a few times per second, only the most recent change is sent for that second.
Subscription command (client => server)
Schema:
{
"id": <int>, // Required: Sequential command id maintained by the client.
"cmd": "subscribe", // Required: Specifies the subscription operation.
"params": {
"channels": ["ticker"], // Required: Specifies the ticker channel.
"market_tickers": ["<string>", ..., "<string>"] // Not required: If you do not pass this field it will subscribe to ticker updates on all markets. If you do it will only send updates for the list of markets provided.
}
}
This channel supports two subscription modes:
List of markets mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["ticker"],
"market_tickers": ["FED-23DEC-T3.00", "CORIVER-2024-T1030"]
}
}
All markets mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["ticker"],
}
}
Ticker message (server => client)
A message with a ticker update from the server:
Schema:
{
"type": "ticker", // Required: Message identifier, what you use to recognize this message type that arrive in your websocket connection.
"sid": <int>, // Required: Id of the subscription.
"msg": {
"market_ticker": <string>, // Required: Market_ticker string, what you use to differentiate updates for different markets.
"price": <int>, // Between 1 and 99 (inclusive)
"yes_bid": <int>, // Between 1 and 99 (inclusive)
"yes_ask": <int>, // Between 1 and 99 (inclusive)
"volume": <int>, // Number of individual contracts traded on the market so far YES and NO count separately
"open_interest": <int>, // Number of active contracts in the market currently
"dollar_volume": <int>, // Number of dollars traded in the market so far
"dollar_open_interest": <int>, // Number of dollars positioned in the market currently
"ts": <int> // Unix timestamp for when the update happened (in seconds)
}
}
Example:
{
"type": "ticker",
"sid": 11,
"msg": {
"market_ticker": "FED-23DEC-T3.00",
"price": 48,
"yes_bid": 45,
"yes_ask": 53,
"volume": 33896,
"open_interest": 20422,
"dollar_volume": 16948,
"dollar_open_interest": 10211,
"ts": 1669149841
}
}
A ticker message also will be sent whenever there is a new trade or the bid / ask values move on any of the markets tracked by the subscription.
Trade channel
Channel: trade
Purpose: Update the client with the most recent trades that occur in the markets that the client is interested on.
The subscription process is similar to the ticker channel, the client subscription specifying the market_tickers he is interested on and the server will start sending trade data messages.
Subscription command (client => server)
Schema:
{
"id": <int>, // Required: Sequential command id maintained by the client.
"cmd": "subscribe", // Required: Specifies the subscription operation.
"params": {
"channels": ["trade"], // Required: Specifies the trade channel.
"market_tickers": [<string>, ..., <string>] // Not required: If you do not pass this field it will subscribe to trade updates on all markets. If you do it will only send updates for the list of markets provided.
}
}
This channel supports two subscription modes:
List of markets mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["trade"],
"market_tickers": ["FED-23DEC-T3.00", "HIGHNY-22DEC23-B53.5"]
}
}
All markets mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["trade"]
}
}
Trade message (server => client)
A trade message will be sent for each trade that happens on the server for the markets included in the subscription. Regardless of whether you participated in the trade or not, this channel exposes public trades.
For security reason, fields that could be used to potentially identify the users or orders involved in the trades like user, order or trade ids are intentionally removed from the response.
Each trade message is equivalent to an entry in the GetTrades endpoint from the trade api.
Schema:
{
"type": "trade", // Required: Message identifier, what you use to recognize this message type that arrive in your websocket connection.
"sid": <int>, // Required: Id of the subscription.
"msg": {
"market_ticker": <string>, // Ticker for the market that this trade belongs to. This is what you use to differentiate updates for different markets.
"yes_price": <int>, // Price for the yes side. Between 1 and 99 (inclusive).
"no_price": <int>, // Price for the no side. Between 1 and 99 (inclusive).
"count": <int>, // Number of contracts traded.
"taker_side": <string>, // Side of the taker user on this trade. Either "yes" or "no".
"ts": <int> // Unix timestamp for when the update happened (in seconds).
}
}
Example:
{
"type": "trade",
"sid": 11,
"msg": {
"market_ticker": "HIGHNY-22DEC23-B53.5",
"yes_price": 36,
"no_price": 64,
"count": 136,
"taker_side": "no",
"ts": 1669149841
}
}
Fill channel
Channel: fill
Purpose: Update the client with the most recent fills, that means trades in that occur in the markets that the client is interested on.
The subscription process is similar to the ticker channel, the client subscription specifying the market_tickers he is interested on and the server will start sending trade data messages.
Subscription command (client => server)
Schema:
{
"id": <int>, // Required: Sequential command id maintained by the client.
"cmd": "subscribe", // Required: Specifies the subscription operation.
"params": {
"channels": ["fill"], // Required: Specifies the fill channel.
"market_ticker": <string>, // Not required: If you do not pass this field it will. If you do it specifies a single market to be subscribed to.
"market_tickers": <string> // Not required: If you do not pass this field it will subscribe to ticker updates on all markets. If you do it will only send updates for the list of markets provided.
}
}
This channel supports all subscription modes:
Single market mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["fill"],
"market_ticker": "CPI-22DEC-TN0.1"
}
}
List of markets mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["fill"],
"market_tickers": ["CPI-22DEC-TN0.1", "INXY-23DEC29-T2700"]
}
}
All markets mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["fill"]
}
}
Fill message (server => client)
A fill message will be sent for each trade that happens for your account on the server limited to the markets that are included in the subscription.
Since this channel is private it will have all the identifiers so you can uniquely recognize each trade.
Each fill message is equivalent to an entry in the GetFills endpoint from the trade api.
Schema:
{
"type": "fill", // Required: Message identifier, what you use to recognize this message type that arrive in your websocket connection.
"sid": <int>, // Required: Id of the subscription.
"msg": {
"trade_id": <string>, // Unique identifier for fills. This is what you use to differentiate fills.
"order_id": <string>, // Unique identifier for orders. This is what you use to differentiate fills for different orders.
"market_ticker": <string>, // Unique identifier for markets. This is what you use to differentiate fills for different markets.
"is_taker": <bool>, // If you were a taker on this fill.
"side": <string>, // Side of your fill. Either "yes" or "no".
"yes_price": <int>, // Price for the yes side of the fill. Between 1 and 99 (inclusive).
"no_price": <int>, // Price for the no side of the fill. Between 1 and 99 (inclusive).
"count": <int>, // Number of contracts filled.
"action": <string>, // Action that initiated the fill. Either "buy" or "sell".
"ts": <int> // Unix timestamp for when the update happened (in seconds).
}
}
Example:
{
"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": 75,
"no_price": 25,
"count": 278,
"action": "buy",
"ts": 1671899397
}
}
Market Lifecycle channel
Channel: market_lifecycle
Purpose: Update the client with new market lifecycle events of the following types: open, pause, close, determination and settlement with corresponding details for each event.
Subscription command (client => server)
Schema:
{
"id": <int>, // Required: Sequential command id maintained by the client.
"cmd": "subscribe", // Required: Specifies the subscription operation.
"params": {
"channels": ["market_lifecycle"], // Required: Specifies the market lifecycle channel.
}
}
This channel only supports the All markets mode:
All markets mode:
{
"id": 2,
"cmd": "subscribe",
"params": {
"channels": ["market_lifecycle"]
}
}
Market lifecycle message (server => client)
A market lifecycle message will be sent for every market on each of the following events:
- When a new market is created
- When a market's trading is paused
- When a market's close date is updated (early close)
- When a market is determined
- When a market is settled
Schema:
{
"type": "market_lifecycle", // Required: Message identifier, what you use to recognize this message type that arrive in your websocket connection.
"sid": <int>, // Required: Id of the subscription.
"msg": {
"market_ticker": <string>, // Unique identifier for markets. This is what you use to differentiate updates for different markets.
"open_ts": <int>, // Unix timestamp for when the market opened (in seconds).
"close_ts": <int>, // Unix timestamp for when the market is scheduled to close (in seconds). Will be updated in case of early determination markets.
"determination_ts": <int>, // Optional: This key will not exist before the market is determined. Unix timestamp for when the market is determined (in seconds).
"settled_ts": <int>, // Optional: This key will not exist before the market is settled. Unix timestamp for when the market is settled (in seconds).
"result": <string>, // Optional: This key will not exist before the market is determined. Result of the market.
"is_deactivated": <bool> // Boolean flag to indicate if trading is paused on an open market. This should only be interpreted for an open market.
}
}
Example:
{
"type": "market_lifecycle",
"sid": 13,
"msg": {
"market_ticker": "INXD-23SEP14-B4487",
"open_ts": 1694635200,
"close_ts": 1694721600,
"determination_ts": 1694732586,
"settled_ts": 0,
"result": "no",
"is_deactivated": false
}
}
Heartbeats
We need a way to detect broken connections both on the client and the server sides. The WS protocol has standard ping/pong frames specifically for this purpose.
Server-side:
The server will send PING messages every 10 seconds to check for the connectivity with your client. Your client should respond with a PONG message or else the connection will be dropped on the server side.
Client-side:
The client should also send PING messages every 10 seconds to avoid trusting a stale connection in the client side. The programming languages usually support enabling heartbeat in the client, in such a way that you don't have to write the heartbeat messages yourself, if your programming language supports this we recommend that you enable it in order to have a reliable connection.