<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DeskBot</title>
    <description>The latest articles on DEV Community by DeskBot (@deskbot_store).</description>
    <link>https://dev.to/deskbot_store</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4217875%2F748a898e-bc06-4957-b375-67c701e33b64.png</url>
      <title>DEV Community: DeskBot</title>
      <link>https://dev.to/deskbot_store</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deskbot_store"/>
    <language>en</language>
    <item>
      <title>Why I built DeskBot as local Kalshi desk automation (and a quick Capital vote ask)</title>
      <dc:creator>DeskBot</dc:creator>
      <pubDate>Sat, 31 Oct 2026 16:33:22 +0000</pubDate>
      <link>https://dev.to/deskbot_store/why-i-built-deskbot-as-local-kalshi-desk-automation-and-a-quick-capital-vote-ask-5453</link>
      <guid>https://dev.to/deskbot_store/why-i-built-deskbot-as-local-kalshi-desk-automation-and-a-quick-capital-vote-ask-5453</guid>
      <description>&lt;p&gt;I'm Joshua, solo maker of DeskBot Multi15 — a Mac/Windows desktop app that automates Kalshi desk workflows &lt;strong&gt;locally&lt;/strong&gt;, using the trader's own API key. Keys stay on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Local automation for Kalshi desk workflows (not a hosted black box)&lt;/li&gt;
&lt;li&gt;Your own API key&lt;/li&gt;
&lt;li&gt;$1 first month, then $19.99/mo (not a free trial)&lt;/li&gt;
&lt;li&gt;Store download is Multi15&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why local
&lt;/h2&gt;

&lt;p&gt;Prediction-market tooling often wants your keys in the cloud. I wanted the opposite: run on your Mac/Windows, keep credentials local, and stay honest about risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capital this week
&lt;/h2&gt;

&lt;p&gt;DeskBot is in this week's &lt;a href="https://aitoolscapital.com/the-capital?utm_source=capital_share&amp;amp;utm_medium=outreach&amp;amp;utm_campaign=week_2026-10-26&amp;amp;utm_content=deskbot" rel="noopener noreferrer"&gt;AI Tools Capital&lt;/a&gt; vote (closes Sunday morning ET). If the local-first angle resonates, an upvote helps a solo maker a lot.&lt;/p&gt;

&lt;p&gt;Product: &lt;a href="https://deskbot.store/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=get-deskbot" rel="noopener noreferrer"&gt;deskbot.store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trading involves full risk of loss. Not affiliated with Kalshi.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>productivity</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Kalshi API Rate Limits and WebSockets: A Bot Builder's Guide</title>
      <dc:creator>DeskBot</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:18:12 +0000</pubDate>
      <link>https://dev.to/deskbot_store/kalshi-api-rate-limits-and-websockets-a-bot-builders-guide-4kj5</link>
      <guid>https://dev.to/deskbot_store/kalshi-api-rate-limits-and-websockets-a-bot-builders-guide-4kj5</guid>
      <description>&lt;p&gt;If you are building a bot for Kalshi's 15-minute crypto markets, the two API details that decide whether it works are the &lt;strong&gt;rate limits&lt;/strong&gt; and the &lt;strong&gt;WebSocket feed&lt;/strong&gt;. Get them wrong and your bot either gets throttled with &lt;code&gt;429 Too Many Requests&lt;/code&gt; at the exact moment a market moves, or it trades on an order book that stopped updating 20 seconds ago. This guide covers how Kalshi's token-bucket limits work, how to budget a bot against them, and how to use the WebSocket API so you poll less and see more. Everything here was checked against &lt;a href="https://docs.kalshi.com" rel="noopener noreferrer"&gt;docs.kalshi.com&lt;/a&gt; on October 23, 2026.&lt;/p&gt;

&lt;p&gt;New to the markets themselves? Start with &lt;a href="https://deskbot.store/blogs/news/kalshi-15-minute-markets-explained" rel="noopener noreferrer"&gt;Kalshi 15-minute markets explained&lt;/a&gt; and the &lt;a href="https://deskbot.store/blogs/news/kalshi-api-tutorial-python" rel="noopener noreferrer"&gt;Kalshi API tutorial in Python&lt;/a&gt;, then come back here.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Kalshi rate limits work: tokens, not request counts
&lt;/h2&gt;

&lt;p&gt;Kalshi does not give you "N requests per minute." Every authenticated request costs &lt;strong&gt;tokens&lt;/strong&gt;, and your account tier sets a &lt;strong&gt;budget&lt;/strong&gt;: how many tokens per second refill into your balance. Most requests cost the default of &lt;strong&gt;10 tokens&lt;/strong&gt;. Some cost less; for example, Kalshi's docs list a batch cancel at 2 tokens per order. The authoritative list of non-default costs is the &lt;code&gt;GET /account/endpoint_costs&lt;/code&gt; endpoint, so a careful bot reads it rather than hard-coding numbers.&lt;/p&gt;

&lt;p&gt;Your sustained rate for any endpoint is simply:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sustained requests per second = budget Ã· cost&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Two separate buckets: Read and Write
&lt;/h3&gt;

&lt;p&gt;You have two independent budgets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read&lt;/strong&gt;: &lt;code&gt;GET&lt;/code&gt; endpoints (markets, order books, positions, fills) and anything not routed to Write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write&lt;/strong&gt;: placing, amending and cancelling orders, order groups, and the RFQ quote flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The split is by what the request does, not how it is sent. REST and FIX requests drain the same buckets. Polling the order book too hard therefore cannot block your order placement, but it can stop you reading positions and fills. And &lt;code&gt;GET /portfolio/balance&lt;/code&gt; sits in its own small bucket: once per second on the Basic and Advanced tiers. Kalshi's own advice is to react to fills and settlements (for example through the fills WebSocket channel) instead of polling balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tier table
&lt;/h3&gt;

&lt;p&gt;Per-second token budgets for event contracts (predictions), from Kalshi's rate-limit page:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Read budget&lt;/th&gt;
&lt;th&gt;Write budget&lt;/th&gt;
&lt;th&gt;Orders/sec at 10 tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expert&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premier&lt;/td&gt;
&lt;td&gt;1,200&lt;/td&gt;
&lt;td&gt;1,200&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paragon&lt;/td&gt;
&lt;td&gt;2,400&lt;/td&gt;
&lt;td&gt;2,400&lt;/td&gt;
&lt;td&gt;240&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prime&lt;/td&gt;
&lt;td&gt;4,800&lt;/td&gt;
&lt;td&gt;4,800&lt;/td&gt;
&lt;td&gt;480&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prestige&lt;/td&gt;
&lt;td&gt;12,000&lt;/td&gt;
&lt;td&gt;9,600&lt;/td&gt;
&lt;td&gt;960&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Basic is what you get at signup. Advanced is available by calling Kalshi's "Upgrade Account API Usage Level" endpoint. The higher tiers are granted automatically from trading volume (your trailing 30-day volume as a share of exchange volume, reviewed daily) or assigned by Kalshi. You can see your current tier and grants with &lt;code&gt;GET /account/limits&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bursting: why an idle bot can fire a block of orders
&lt;/h3&gt;

&lt;p&gt;Each budget is a token bucket that refills continuously; there are no fixed windows or per-second resets. Bucket size matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic and Advanced &lt;strong&gt;Read&lt;/strong&gt; buckets, and &lt;strong&gt;Write&lt;/strong&gt; buckets above Basic, hold &lt;strong&gt;three seconds&lt;/strong&gt; of budget. After three quiet seconds the bucket is full and you can spend up to 3Ã— your per-second budget at once.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Basic Write&lt;/strong&gt; bucket holds only &lt;strong&gt;one second&lt;/strong&gt; of budget. Idle time banks nothing beyond that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Worked example (Basic tier):&lt;/strong&gt; your Write bucket refills at 100 tokens/second and holds 100. You can send 10 default-cost orders in one instant, then 10 per second after that. On Advanced, the Write bucket refills at 300/second and holds 900, so after three idle seconds you could send 90 orders at once, then 30 per second.&lt;/p&gt;

&lt;p&gt;For a 15-minute crypto bot this is usually plenty. A bot trading BTC, ETH, SOL and XRP 15-minute markets that places one entry and maybe one cancel per market per window needs a handful of writes every 15 minutes. You hit the limit only with loops that re-quote or cancel-and-replace on every tick.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch endpoints do not save tokens
&lt;/h3&gt;

&lt;p&gt;This catches many bot builders. Batch requests cost the same as the individual calls: Kalshi's docs give 25 batched order creates as &lt;code&gt;25 Ã— 10 = 250&lt;/code&gt; tokens. Worse, &lt;strong&gt;the whole batch must fit in the bucket at once&lt;/strong&gt;. On Basic, where the Write bucket holds 100 tokens, a 25-order create batch needs 250 tokens and will be rejected whole, every time. Batching helps with round trips, not with your budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a 429 looks like and how to handle it
&lt;/h3&gt;

&lt;p&gt;A throttled request returns HTTP 429 with the body &lt;code&gt;{"error": "too many requests"}&lt;/code&gt;. Per the docs, 429s currently carry no &lt;code&gt;Retry-After&lt;/code&gt; or &lt;code&gt;X-RateLimit-*&lt;/code&gt; headers and there is no penalty or cooldown: the next request succeeds once the bucket refills enough. Kalshi recommends exponential backoff. In practice, a good bot also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Budgets itself on the client side&lt;/strong&gt;, keeping a local token counter that mirrors the server's, so it rarely sees a 429 at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gives writes priority.&lt;/strong&gt; If a cancel is waiting, it goes before a nice-to-have refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never retries an order blindly.&lt;/strong&gt; A timeout is not a rejection. Send a &lt;code&gt;client_order_id&lt;/code&gt; and check your open orders before you re-send, or you risk a duplicate position.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exchange shards and per-shard Write budgets
&lt;/h2&gt;

&lt;p&gt;Kalshi runs prediction markets across &lt;strong&gt;exchange shards&lt;/strong&gt;, and each market reports an &lt;code&gt;exchange_index&lt;/code&gt;. When we pulled an open BTC 15-minute market (series &lt;code&gt;KXBTC15M&lt;/code&gt;) from the public markets endpoint this evening, it reported &lt;code&gt;exchange_index: 2&lt;/code&gt;. That matters for budgets: a single REST order create, cancel, decrease or amend that targets shard 1 or higher draws from a Write bucket &lt;strong&gt;scoped to that shard&lt;/strong&gt;, and each shard's bucket carries your full tier budget. Shard 0 bills your unscoped Write bucket. Auto-routed traffic (&lt;code&gt;exchange_index: -1&lt;/code&gt;, or omitted when &lt;code&gt;market_ticker&lt;/code&gt; is given) is billed to &lt;strong&gt;every&lt;/strong&gt; shard's bucket. Batch creates and cancels always bill the unscoped bucket.&lt;/p&gt;

&lt;p&gt;The practical takeaway: if your bot trades markets on more than one shard, passing the market's own &lt;code&gt;exchange_index&lt;/code&gt; on single orders keeps each shard's traffic in its own budget instead of charging all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the WebSocket API instead of polling
&lt;/h2&gt;

&lt;p&gt;The fastest way to burn your Read budget is to poll the order book of four markets every 250 ms: that is 16 requests and 160 tokens per second, most of your 200-token Basic Read budget, for data that often hasn't changed. Kalshi's WebSocket API pushes changes to you instead: one subscription replaces that whole polling loop, and your Read budget is left for the occasional REST call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting
&lt;/h3&gt;

&lt;p&gt;The production WebSocket endpoint is &lt;code&gt;wss://external-api-ws.kalshi.com/trade-api/ws/v2&lt;/code&gt; (demo: &lt;code&gt;wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2&lt;/code&gt;; the older shared hosts are still supported). The handshake is authenticated with the same three headers as REST: &lt;code&gt;KALSHI-ACCESS-KEY&lt;/code&gt;, &lt;code&gt;KALSHI-ACCESS-TIMESTAMP&lt;/code&gt; (milliseconds) and &lt;code&gt;KALSHI-ACCESS-SIGNATURE&lt;/code&gt;. The signature is over &lt;code&gt;timestamp + "GET" + "/trade-api/ws/v2"&lt;/code&gt;, signed with your Ed25519 key (Kalshi's recommended type) or with RSA-PSS SHA-256 for an RSA key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Channels that matter for a 15-minute bot
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;orderbook_delta&lt;/code&gt;&lt;/strong&gt; (private): an &lt;code&gt;orderbook_snapshot&lt;/code&gt; first, then incremental &lt;code&gt;orderbook_delta&lt;/code&gt; messages for the markets you list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ticker&lt;/code&gt;&lt;/strong&gt; (public data): best yes bid/ask and last price updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;trade&lt;/code&gt;&lt;/strong&gt;: executions as they print.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fill&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;market_positions&lt;/code&gt;&lt;/strong&gt; (private): your own fills and positions, which replace most portfolio polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;market_lifecycle_v2&lt;/code&gt;&lt;/strong&gt;: markets opening, closing and settling, which is how a bot knows the next 15-minute window exists without polling the markets list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You subscribe by sending a command such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"orderbook_delta"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"market_tickers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"KXBTC15M-..."&lt;/span&gt;&lt;span class="p"&gt;]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each subscription gets a server-generated &lt;code&gt;sid&lt;/code&gt;. You can add or remove markets with &lt;code&gt;update_subscription&lt;/code&gt;, and ask for a fresh snapshot with its &lt;code&gt;get_snapshot&lt;/code&gt; action without changing the subscription.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sequence numbers: how to know your book is real
&lt;/h3&gt;

&lt;p&gt;This is the part most homemade bots skip. Every snapshot and delta carries a &lt;code&gt;seq&lt;/code&gt;, and per Kalshi's docs &lt;code&gt;seq&lt;/code&gt; counts messages &lt;strong&gt;on one subscription only&lt;/strong&gt;. If you see a gap (seq 41 followed by seq 43), you missed a message and your local book is wrong. Don't keep trading on it: request a new snapshot and rebuild. Messages also carry an exchange &lt;code&gt;clock&lt;/code&gt;, which is the same on every connection. That lets you match updates across two redundant connections. Clocks skip values and are not comparable across markets, so always use &lt;code&gt;seq&lt;/code&gt; to detect gaps.&lt;/p&gt;

&lt;p&gt;Two more details worth building in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kalshi's book lists bids on both sides.&lt;/strong&gt; The snapshot has YES bids and NO bids. The YES ask is $1 minus the best NO bid. (Our guide to &lt;a href="https://deskbot.store/blogs/news/how-to-read-kalshi-order-book" rel="noopener noreferrer"&gt;reading the Kalshi order book&lt;/a&gt; walks through it.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep-alive.&lt;/strong&gt; Kalshi notes that Python's &lt;code&gt;websockets&lt;/code&gt; library answers ping/pong frames automatically; other libraries may need you to handle them. Either way, plan for disconnects: on reconnect, re-sign, re-subscribe and wait for a fresh snapshot before trading.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A sane budget for a four-market 15-minute bot
&lt;/h2&gt;

&lt;p&gt;Here is a design that fits comfortably inside the &lt;strong&gt;Basic&lt;/strong&gt; tier:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One WebSocket connection&lt;/strong&gt; subscribed to &lt;code&gt;orderbook_delta&lt;/code&gt; and &lt;code&gt;ticker&lt;/code&gt; for the current BTC, ETH, SOL and XRP 15-minute markets, plus &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;market_positions&lt;/code&gt; and &lt;code&gt;market_lifecycle_v2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST reads only on events&lt;/strong&gt;: when a new window opens (lifecycle message), read that market's details once. That is a few reads per 15 minutes, not per second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes&lt;/strong&gt;: at most a few per market per window, such as one limit order and an occasional cancel. Even 20 writes in a burst spread over a couple of seconds stays under 100 tokens/second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A local token counter&lt;/strong&gt; for both buckets, with writes taking priority and backoff on any 429.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rate limits and data freshness are only half the job, though. The other half is whether a trade is worth taking after fees. At a 50Â¢ price Kalshi's trading fee works out to about 2Â¢ per contract after rounding, and near 90Â¢ a single loss wipes out many small wins. See &lt;a href="https://deskbot.store/blogs/news/kalshi-fees-break-even-win-rate" rel="noopener noreferrer"&gt;Kalshi fees and breakeven win rate&lt;/a&gt; for the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it or start from one that's already built
&lt;/h2&gt;

&lt;p&gt;Everything above is buildable in a weekend if you enjoy it, and the Kalshi docs are good. If you'd rather see a working bot first, &lt;a href="https://deskbot.store/products/deskbot-kalshi-bot?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=20261031" rel="noopener noreferrer"&gt;DeskBot for Kalshi&lt;/a&gt; ($1 the first month, then $19.99/month) runs on your own Windows or Mac machine with your own Kalshi API key, so the market feed and order plumbing are already done. You choose the settings and decide whether it trades.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the Kalshi API rate limit?
&lt;/h3&gt;

&lt;p&gt;It's token-based. Most requests cost 10 tokens. The Basic tier refills 200 Read tokens and 100 Write tokens per second, so about 20 reads and 10 order actions per second at the default cost. Higher tiers raise both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Kalshi API return a Retry-After header on 429?
&lt;/h3&gt;

&lt;p&gt;Not currently. Kalshi's docs say 429 responses have no &lt;code&gt;Retry-After&lt;/code&gt; or &lt;code&gt;X-RateLimit-*&lt;/code&gt; headers and no cooldown. Back off exponentially and retry once the bucket has refilled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Kalshi batch orders reduce rate-limit usage?
&lt;/h3&gt;

&lt;p&gt;No. Each item is billed separately, and the whole batch must fit in your bucket at once. On Basic, a 25-order create batch (250 tokens) is larger than the 100-token Write bucket and will be rejected.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I get a higher Kalshi API tier?
&lt;/h3&gt;

&lt;p&gt;Advanced is self-serve through the API usage-level upgrade endpoint. Expert and above are granted automatically from your 30-day trading volume share, reviewed daily, or assigned by Kalshi.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Kalshi WebSocket URL?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;wss://external-api-ws.kalshi.com/trade-api/ws/v2&lt;/code&gt; for production and &lt;code&gt;wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2&lt;/code&gt; for demo. The handshake is signed like a REST request, over the path &lt;code&gt;/trade-api/ws/v2&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know if I missed an order book update?
&lt;/h3&gt;

&lt;p&gt;Check &lt;code&gt;seq&lt;/code&gt; on every message of a subscription. A gap means you missed a message, so request a fresh snapshot and rebuild your local book before trading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a faster bot a profitable bot?
&lt;/h3&gt;

&lt;p&gt;No. Good API handling keeps a bot from acting on stale data or getting throttled, but it creates no edge by itself. Fees and spread still have to be beaten, and many strategies don't beat them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://deskbot.store/products/deskbot-kalshi-bot?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=20261031&amp;amp;utm_content=kalshi-api-rate-limits-websocket" rel="noopener noreferrer"&gt;Try DeskBot for $1&lt;/a&gt;&lt;/strong&gt; â€” first month $1, then $19.99/month, cancel anytime. Card required; it renews at $19.99/month unless you cancel.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Risk: Trading on Kalshi or Polymarket involves risk, and you can lose money, including your entire stake. There are no guaranteed profits or returns, and past or paper results do not predict future results. This is educational, not financial advice. DeskBot is independent software and is not affiliated with, endorsed by, or sponsored by Kalshi or Polymarket. API details were taken from Kalshi's published documentation on October 23, 2026. Limits and endpoints change, so check docs.kalshi.com before you build.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://deskbot.store/blogs/news/kalshi-api-rate-limits-websocket" rel="noopener noreferrer"&gt;DeskBot blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>websockets</category>
      <category>trading</category>
    </item>
    <item>
      <title>Kalshi API Keys and Authentication: How Request Signing Works</title>
      <dc:creator>DeskBot</dc:creator>
      <pubDate>Sat, 31 Oct 2026 03:14:55 +0000</pubDate>
      <link>https://dev.to/deskbot_store/kalshi-api-keys-and-authentication-how-request-signing-works-41ia</link>
      <guid>https://dev.to/deskbot_store/kalshi-api-keys-and-authentication-how-request-signing-works-41ia</guid>
      <description>&lt;p&gt;Every Kalshi trading bot starts with the same piece of plumbing: an API key and a signed request. Public market data needs no key. The moment your code reads your balance or places an order, Kalshi wants three headers and a cryptographic signature on every call. This guide explains how Kalshi API keys work, which key type to pick, exactly what string gets signed, how scopes and sub-account binding limit what a key can do, and the mistakes that produce a &lt;code&gt;401&lt;/code&gt; at 3 a.m. Everything below was checked against docs.kalshi.com on October 27, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public endpoints vs. authenticated endpoints
&lt;/h2&gt;

&lt;p&gt;Kalshi's Trade API v2 splits into two kinds of calls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public market data&lt;/strong&gt; such as &lt;code&gt;/markets&lt;/code&gt;, &lt;code&gt;/events&lt;/code&gt;, &lt;code&gt;/series&lt;/code&gt; and order books. You can read these without a key, which is why most tutorials start there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account endpoints&lt;/strong&gt; such as &lt;code&gt;/portfolio/balance&lt;/code&gt;, &lt;code&gt;/portfolio/positions&lt;/code&gt;, &lt;code&gt;/portfolio/fills&lt;/code&gt; and order placement. These require a signed request tied to an API key on your account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recommended production base URL is &lt;code&gt;https://external-api.kalshi.com/trade-api/v2&lt;/code&gt;, and the demo environment uses &lt;code&gt;https://external-api.demo.kalshi.co/trade-api/v2&lt;/code&gt;. Kalshi says the key process is the same on both. Keys are per environment, though: a demo key only works against demo hosts, and a production key only works against production hosts. (Our &lt;a href="https://deskbot.store/blogs/news/kalshi-demo-environment-api" rel="noopener noreferrer"&gt;demo environment guide&lt;/a&gt; covers switching between the two.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Production and demo hosts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;REST base URL (recommended)&lt;/th&gt;
&lt;th&gt;REST (also supported)&lt;/th&gt;
&lt;th&gt;WebSocket (recommended)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://external-api.kalshi.com/trade-api/v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://api.elections.kalshi.com/trade-api/v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wss://external-api-ws.kalshi.com/trade-api/ws/v2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://external-api.demo.kalshi.co/trade-api/v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://demo-api.kalshi.co/trade-api/v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: docs.kalshi.com, API Environments and Endpoints, checked October 29, 2026. The &lt;code&gt;external-api&lt;/code&gt; hosts are dedicated to the Trade API; the older shared hosts stay supported for existing clients. Despite the &lt;code&gt;elections&lt;/code&gt; subdomain, the production API covers all Kalshi markets, not only election markets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The host does not change the signature.&lt;/strong&gt; Whichever host you call, you sign the same path from the API root, for example &lt;code&gt;/trade-api/v2/portfolio/orders&lt;/code&gt;. Moving from &lt;code&gt;api.elections.kalshi.com&lt;/code&gt; to &lt;code&gt;external-api.kalshi.com&lt;/code&gt; means changing the base URL only, not your signing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to create a Kalshi API key
&lt;/h2&gt;

&lt;p&gt;Most people create a key in the web app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in and open &lt;strong&gt;Profile Settings&lt;/strong&gt; (kalshi.com/account/profile).&lt;/li&gt;
&lt;li&gt;Find the &lt;strong&gt;API Keys&lt;/strong&gt; section and click &lt;strong&gt;Create New API Key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pick the key type. Ed25519 is the default; choose RSA only if your client library supports RSA-PSS and nothing else.&lt;/li&gt;
&lt;li&gt;Save the private key right away. Kalshi downloads it as a &lt;code&gt;.txt&lt;/code&gt; file and does not keep a copy. Once the page closes, you cannot get it back; you would have to create a new key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will end up with two things: a &lt;strong&gt;Key ID&lt;/strong&gt; (safe to put in a config file) and a &lt;strong&gt;private key&lt;/strong&gt; in PEM format (treat it like a password, because it is one).&lt;/p&gt;

&lt;p&gt;There is a second route for advanced users. You can generate your own key pair with OpenSSL and register only the public half, so the private key never leaves your machine. Through the API, &lt;code&gt;POST /trade-api/v2/api_keys&lt;/code&gt; registers a public key and &lt;code&gt;POST /trade-api/v2/api_keys/generate&lt;/code&gt; returns a new pair. Kalshi's reference notes that creating keys through the API is limited to Premier and Market Maker API usage levels; other accounts get a &lt;code&gt;403&lt;/code&gt; and use the web page instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ed25519 or RSA: which key type?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Ed25519&lt;/th&gt;
&lt;th&gt;RSA (2048-bit)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default in the web app&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature scheme&lt;/td&gt;
&lt;td&gt;Ed25519 (RFC 8032), 64-byte signature&lt;/td&gt;
&lt;td&gt;RSA-PSS, SHA-256, MGF1-SHA-256, salt = digest length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private key PEM header&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-----BEGIN PRIVATE KEY-----&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-----BEGIN RSA PRIVATE KEY-----&lt;/code&gt; (PKCS#1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When to choose it&lt;/td&gt;
&lt;td&gt;New code; Kalshi recommends it&lt;/td&gt;
&lt;td&gt;Older clients that only do RSA-PSS, including Kalshi SDK versions before 3.31.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One trap: the PEM header alone does not tell you the type, because RSA keys saved as PKCS#8 also start with &lt;code&gt;BEGIN PRIVATE KEY&lt;/code&gt;. Load the key with a crypto library and check the parsed object's type, not the text of the first line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three headers on every authenticated request
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;KALSHI-ACCESS-KEY&lt;/code&gt;: your Key ID.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KALSHI-ACCESS-TIMESTAMP&lt;/code&gt;: the current time in &lt;strong&gt;milliseconds&lt;/strong&gt; since the Unix epoch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KALSHI-ACCESS-SIGNATURE&lt;/code&gt;: the base64-encoded signature of the request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exactly what gets signed
&lt;/h2&gt;

&lt;p&gt;The message is three pieces glued together with no separators: &lt;strong&gt;timestamp + HTTP method + path&lt;/strong&gt;. Query parameters are &lt;em&gt;not&lt;/em&gt; part of it; strip the &lt;code&gt;?&lt;/code&gt; and everything after it before signing.&lt;/p&gt;

&lt;p&gt;A worked example. Say your code wants your open positions at 1,761,580,800,000 ms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full URL: &lt;code&gt;https://external-api.kalshi.com/trade-api/v2/portfolio/positions?limit=100&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Method: &lt;code&gt;GET&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Path to sign: &lt;code&gt;/trade-api/v2/portfolio/positions&lt;/code&gt; (the &lt;code&gt;?limit=100&lt;/code&gt; is dropped)&lt;/li&gt;
&lt;li&gt;Message: &lt;code&gt;1761580800000GET/trade-api/v2/portfolio/positions&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sign those bytes with your private key, base64-encode the result, and send it as &lt;code&gt;KALSHI-ACCESS-SIGNATURE&lt;/code&gt; alongside the same timestamp. Note the path starts at &lt;code&gt;/trade-api/v2&lt;/code&gt;, not at the host and not at &lt;code&gt;/portfolio&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal Python signer
&lt;/h2&gt;

&lt;p&gt;This sketch uses the widely used &lt;code&gt;cryptography&lt;/code&gt; and &lt;code&gt;requests&lt;/code&gt; packages and handles both key types. It is written for this article as an illustration; test it on the demo host before pointing it at real money.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serialization&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.asymmetric&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ed25519&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsa&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://external-api.kalshi.com/trade-api/v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;KEY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-key-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                      &lt;span class="c1"&gt;# from Profile Settings
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kalshi_private_key.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ed25519&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ed25519PrivateKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RSAPrivateKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PSS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mgf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MGF1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
                        &lt;span class="n"&gt;salt_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DIGEST_LENGTH&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported key type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;signed_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;          &lt;span class="c1"&gt;# no query string
&lt;/span&gt;    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KALSHI-ACCESS-KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KALSHI-ACCESS-TIMESTAMP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KALSHI-ACCESS-SIGNATURE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;signed_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/portfolio/balance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the key type is detected from the parsed key object, which avoids the PEM-header trap above. For a &lt;code&gt;POST&lt;/code&gt; the same rule applies: sign timestamp + &lt;code&gt;POST&lt;/code&gt; + path; the JSON body is not part of the signed string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scopes: give a key only the access it needs
&lt;/h2&gt;

&lt;p&gt;When you register a key through the API, the request accepts an optional &lt;code&gt;scopes&lt;/code&gt; list. Leave it out and the key gets full access (&lt;code&gt;read&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt;). The allowed values in Kalshi's reference are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt;, the broad scopes. If you grant &lt;code&gt;write&lt;/code&gt; you must also grant &lt;code&gt;read&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Narrower child scopes such as &lt;code&gt;read::portfolio_balance&lt;/code&gt;, &lt;code&gt;write::trade&lt;/code&gt; and &lt;code&gt;write::transfer&lt;/code&gt;, plus block-trade and FCM-specific ones. Child scopes can be granted without their parent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical use: a dashboard that only displays your balance does not need the power to trade or move funds. A trading bot needs to trade, but it has no reason to hold &lt;code&gt;write::transfer&lt;/code&gt;. The smaller the scope, the less a leaked key can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binding a key to one sub-account
&lt;/h2&gt;

&lt;p&gt;The same request takes an optional &lt;code&gt;subaccount&lt;/code&gt; number from 0 to 63. A key bound to a sub-account can only act inside that sub-account. That is a clean way to give an automated strategy its own pot of money: whatever the bot does, it cannot touch positions or cash outside its sub-account. (FCM members have a separate &lt;code&gt;fcm_subtrader_id&lt;/code&gt; option, which cannot be combined with &lt;code&gt;subaccount&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the private key safe
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never commit it to git.&lt;/strong&gt; Keep it outside your repo and load it from a path or a secrets manager. Add the filename to &lt;code&gt;.gitignore&lt;/code&gt; anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock down file permissions&lt;/strong&gt; on macOS and Linux (&lt;code&gt;chmod 600&lt;/code&gt;), so other users on the machine cannot read it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never paste it&lt;/strong&gt; into chats, support tickets, screenshots or AI tools. Your Key ID alone is harmless; the private key is the secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One key per tool.&lt;/strong&gt; If you run a dashboard and a bot, give each its own key so you can revoke one without breaking the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoke and replace&lt;/strong&gt; a key in Profile Settings the moment you suspect it leaked. A new key takes a minute to create.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common authentication errors and their causes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Usual cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401&lt;/code&gt; on every call&lt;/td&gt;
&lt;td&gt;Timestamp in seconds instead of milliseconds, or the wrong Key ID for the private key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401&lt;/code&gt; only on calls with filters&lt;/td&gt;
&lt;td&gt;Query string included in the signed path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401&lt;/code&gt; that comes and goes&lt;/td&gt;
&lt;td&gt;Computer clock drifting; turn on automatic time sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401&lt;/code&gt; with an RSA key&lt;/td&gt;
&lt;td&gt;PKCS#1 v1.5 padding used instead of PSS, or the wrong salt length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on demo, fails on production&lt;/td&gt;
&lt;td&gt;Demo key used against the production host (or the reverse)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; on an order&lt;/td&gt;
&lt;td&gt;Key scoped without trade permission, or bound to a different sub-account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; creating a key by API&lt;/td&gt;
&lt;td&gt;Account below the Premier API usage level; use the web page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once auth works, the next limit you will meet is request volume; our &lt;a href="https://deskbot.store/blogs/news/kalshi-api-rate-limits-websocket" rel="noopener noreferrer"&gt;rate limits and WebSocket guide&lt;/a&gt; covers that, and the &lt;a href="https://deskbot.store/blogs/news/kalshi-api-tutorial-python" rel="noopener noreferrer"&gt;Python API tutorial&lt;/a&gt; walks through reading markets and placing orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or use software that already handles signing
&lt;/h2&gt;

&lt;p&gt;Writing your own signer is a good way to learn how Kalshi's API works. If you would rather start from finished software, &lt;strong&gt;DeskBot for Kalshi&lt;/strong&gt; is a desktop app that runs on your own computer and trades Kalshi's 15-minute markets on your own account through your own API key; the key stays on your machine. It starts in paper mode until you switch a market live, and you choose the limits. The current offer is &lt;a href="https://deskbot.store/products/deskbot-kalshi-bot?selling_plan=8219295972&amp;amp;utm_source=site&amp;amp;utm_medium=blog&amp;amp;utm_campaign=dollar1&amp;amp;utm_content=kalshi-api-keys" rel="noopener noreferrer"&gt;Try DeskBot for $1: first month $1, then $19.99/month, cancel anytime&lt;/a&gt;. It does not guarantee profits or win rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need an API key to read Kalshi market prices?
&lt;/h3&gt;

&lt;p&gt;No. Public market data such as markets, events and order books can be read without a key. You need a key for anything tied to your account: balance, positions, fills and orders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I recover a lost Kalshi private key?
&lt;/h3&gt;

&lt;p&gt;No. Kalshi does not store your private key. If you lose it, delete the old key in Profile Settings and create a new one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I choose Ed25519 or RSA?
&lt;/h3&gt;

&lt;p&gt;Ed25519 is Kalshi's default and recommendation. Choose RSA only if your client supports RSA-PSS and not Ed25519, such as Kalshi SDK versions before 3.31.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are query parameters part of the Kalshi signature?
&lt;/h3&gt;

&lt;p&gt;No. Sign timestamp + method + path, with everything from the &lt;code&gt;?&lt;/code&gt; onward removed. Including the query string is one of the most common causes of a &lt;code&gt;401&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What unit is KALSHI-ACCESS-TIMESTAMP in?
&lt;/h3&gt;

&lt;p&gt;Milliseconds since the Unix epoch, sent as a string, and the same value you used in the signed message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I stop a key from moving money?
&lt;/h3&gt;

&lt;p&gt;Yes, when you register a key through the API you can pass a &lt;code&gt;scopes&lt;/code&gt; list that leaves out &lt;code&gt;write::transfer&lt;/code&gt;, and you can bind it to a single sub-account. Without a scopes list the key gets full read and write access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to change my signing code if I switch Kalshi API hosts?
&lt;/h3&gt;

&lt;p&gt;No. The signed path is the same on every host, for example &lt;code&gt;/trade-api/v2/portfolio/orders&lt;/code&gt;. Moving from &lt;code&gt;api.elections.kalshi.com&lt;/code&gt; to the recommended &lt;code&gt;external-api.kalshi.com&lt;/code&gt; only changes the base URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a demo API key work on production?
&lt;/h3&gt;

&lt;p&gt;No. Kalshi does not share credentials between environments. Demo keys only work against demo hosts and production keys only against production hosts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is DeskBot affiliated with Kalshi?
&lt;/h3&gt;

&lt;p&gt;No. DeskBot is independent software. It uses Kalshi's public Trade API on the customer's own account.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Risk: Trading on Kalshi involves risk, and you can lose money. Nothing here guarantees profits, returns, win rates or income. This article explains API mechanics. It is not legal, tax or investment advice. DeskBot is independent software and is not affiliated with or endorsed by Kalshi. Sources, checked October 27, 2026: docs.kalshi.com API Keys guide (getting_started/api_keys) and the Create API Key reference (api-reference/api-keys/create-api-key); API Environments and Endpoints (getting_started/api_environments), checked October 29, 2026. APIs change, so check the current documents before you rely on any detail.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://deskbot.store/blogs/news/kalshi-api-keys-authentication?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;DeskBot blog&lt;/a&gt;. DeskBot is a desktop app that automates Kalshi 15-minute crypto trading on your own computer and account, with paper mode and daily loss limits: $1 for the first month, then $19.99/month (&lt;a href="https://deskbot.store/pages/get-deskbot?utm_source=devto&amp;amp;utm_medium=crosspost" rel="noopener noreferrer"&gt;get DeskBot&lt;/a&gt;). Trading on Kalshi involves risk and you can lose money; no profits or returns are guaranteed. DeskBot is independent software and is not affiliated with or endorsed by Kalshi or Polymarket.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>trading</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
