DEV Community

DeskBot
DeskBot

Posted on Originally published at deskbot.store

Kalshi API Keys and Authentication: How Request Signing Works

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 401 at 3 a.m. Everything below was checked against docs.kalshi.com on October 27, 2026.

Public endpoints vs. authenticated endpoints

Kalshi's Trade API v2 splits into two kinds of calls:

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

The recommended production base URL is https://external-api.kalshi.com/trade-api/v2, and the demo environment uses https://external-api.demo.kalshi.co/trade-api/v2. 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 demo environment guide covers switching between the two.)

Production and demo hosts

Environment REST base URL (recommended) REST (also supported) WebSocket (recommended)
Production https://external-api.kalshi.com/trade-api/v2 https://api.elections.kalshi.com/trade-api/v2 wss://external-api-ws.kalshi.com/trade-api/ws/v2
Demo https://external-api.demo.kalshi.co/trade-api/v2 https://demo-api.kalshi.co/trade-api/v2 wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2

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

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

How to create a Kalshi API key

Most people create a key in the web app:

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

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

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, POST /trade-api/v2/api_keys registers a public key and POST /trade-api/v2/api_keys/generate 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 403 and use the web page instead.

Ed25519 or RSA: which key type?

Ed25519 RSA (2048-bit)
Default in the web app Yes No
Signature scheme Ed25519 (RFC 8032), 64-byte signature RSA-PSS, SHA-256, MGF1-SHA-256, salt = digest length
Private key PEM header -----BEGIN PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY----- (PKCS#1)
When to choose it New code; Kalshi recommends it Older clients that only do RSA-PSS, including Kalshi SDK versions before 3.31.0

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

The three headers on every authenticated request

  • KALSHI-ACCESS-KEY: your Key ID.
  • KALSHI-ACCESS-TIMESTAMP: the current time in milliseconds since the Unix epoch.
  • KALSHI-ACCESS-SIGNATURE: the base64-encoded signature of the request.

Exactly what gets signed

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

A worked example. Say your code wants your open positions at 1,761,580,800,000 ms:

  • Full URL: https://external-api.kalshi.com/trade-api/v2/portfolio/positions?limit=100
  • Method: GET
  • Path to sign: /trade-api/v2/portfolio/positions (the ?limit=100 is dropped)
  • Message: 1761580800000GET/trade-api/v2/portfolio/positions

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

A minimal Python signer

This sketch uses the widely used cryptography and requests 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.

import base64, time, requests
from urllib.parse import urlparse
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa

BASE = "https://external-api.kalshi.com/trade-api/v2"
KEY_ID = "your-key-id"                      # from Profile Settings
with open("kalshi_private_key.txt", "rb") as f:
    PRIVATE_KEY = serialization.load_pem_private_key(f.read(), password=None)

def sign(message: bytes) -> str:
    if isinstance(PRIVATE_KEY, ed25519.Ed25519PrivateKey):
        sig = PRIVATE_KEY.sign(message)
    elif isinstance(PRIVATE_KEY, rsa.RSAPrivateKey):
        sig = PRIVATE_KEY.sign(
            message,
            padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
                        salt_length=padding.PSS.DIGEST_LENGTH),
            hashes.SHA256())
    else:
        raise ValueError("unsupported key type")
    return base64.b64encode(sig).decode()

def get(path: str, **params):
    url = BASE + path
    ts = str(int(time.time() * 1000))
    signed_path = urlparse(url).path          # no query string
    headers = {
        "KALSHI-ACCESS-KEY": KEY_ID,
        "KALSHI-ACCESS-TIMESTAMP": ts,
        "KALSHI-ACCESS-SIGNATURE": sign((ts + "GET" + signed_path).encode()),
    }
    return requests.get(url, headers=headers, params=params, timeout=10)

print(get("/portfolio/balance").json())
Enter fullscreen mode Exit fullscreen mode

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

Scopes: give a key only the access it needs

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

  • read and write, the broad scopes. If you grant write you must also grant read.
  • Narrower child scopes such as read::portfolio_balance, write::trade and write::transfer, plus block-trade and FCM-specific ones. Child scopes can be granted without their parent.

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 write::transfer. The smaller the scope, the less a leaked key can do.

Binding a key to one sub-account

The same request takes an optional subaccount 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 fcm_subtrader_id option, which cannot be combined with subaccount.)

Keeping the private key safe

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

Common authentication errors and their causes

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

Once auth works, the next limit you will meet is request volume; our rate limits and WebSocket guide covers that, and the Python API tutorial walks through reading markets and placing orders.

Or use software that already handles signing

Writing your own signer is a good way to learn how Kalshi's API works. If you would rather start from finished software, DeskBot for Kalshi 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 Try DeskBot for $1: first month $1, then $19.99/month, cancel anytime. It does not guarantee profits or win rates.

FAQ

Do I need an API key to read Kalshi market prices?

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.

Can I recover a lost Kalshi private key?

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

Should I choose Ed25519 or RSA?

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.

Are query parameters part of the Kalshi signature?

No. Sign timestamp + method + path, with everything from the ? onward removed. Including the query string is one of the most common causes of a 401.

What unit is KALSHI-ACCESS-TIMESTAMP in?

Milliseconds since the Unix epoch, sent as a string, and the same value you used in the signed message.

Can I stop a key from moving money?

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

Do I need to change my signing code if I switch Kalshi API hosts?

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

Does a demo API key work on production?

No. Kalshi does not share credentials between environments. Demo keys only work against demo hosts and production keys only against production hosts.

Is DeskBot affiliated with Kalshi?

No. DeskBot is independent software. It uses Kalshi's public Trade API on the customer's own account.

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.


Originally published on the DeskBot blog. 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 (get DeskBot). 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.

Top comments (0)