Authentication¶
Kalshi uses RSA-PSS request signing. You'll need:
- A key ID (string, identifies the key on Kalshi's side).
- A private key — RSA, PEM-encoded, PKCS#8, unencrypted.
Generate the pair in your Kalshi account settings and download the PEM file. The signing scheme used internally is RSA-PSS / SHA256 / MGF1(SHA256) / salt = digest length / base64 — you don't need to implement any of that yourself; the SDK does it for you.
You can also mint keys programmatically once authenticated; see API keys.
Option 1 — Key file path (most common)¶
from kalshi import KalshiClient
with KalshiClient(
key_id="your-key-id",
private_key_path="~/.kalshi/private_key.pem",
) as client:
...
~ is expanded for you. Pass a pathlib.Path or a string. The constructor is
keyword-only; an empty key_id raises ValueError immediately.
Option 2 — Environment variables¶
The from_env() constructors read credentials and configuration from the
environment:
export KALSHI_KEY_ID="..."
export KALSHI_PRIVATE_KEY_PATH="~/.kalshi/private_key.pem"
# Optional knobs:
export KALSHI_DEMO=true # use the sandbox environment
export KALSHI_API_BASE_URL="..." # override the base URL entirely
If KALSHI_KEY_ID / KALSHI_PRIVATE_KEY_PATH are unset, from_env() returns
an unauthenticated client. Public endpoints still work. See
Environment variables for the full table and
precedence rules.
Option 3 — In-memory PEM (env var)¶
If you store the private key in a secret manager (Vault, AWS Secrets Manager,
GCP Secret Manager, …), set KALSHI_PRIVATE_KEY to the PEM contents:
Then KalshiClient.from_env() will load the key directly without touching the
filesystem. KALSHI_PRIVATE_KEY takes precedence over KALSHI_PRIVATE_KEY_PATH
when both are set.
Option 4 — In-memory PEM (constructor)¶
You can also pass the PEM string straight to the constructor:
from kalshi import KalshiClient
pem = secret_manager.get("kalshi/private_key") # str returning the PEM body
with KalshiClient(key_id="...", private_key=pem) as client:
...
The constructor accepts both private_key_path= and private_key=; supply
exactly one.
Demo vs. production¶
from kalshi import KalshiClient
# Sandbox — for development. Hits https://demo-api.kalshi.co/trade-api/v2.
KalshiClient(key_id="...", private_key_path="...", demo=True)
# Production — the default. Hits https://api.elections.kalshi.com/trade-api/v2.
KalshiClient(key_id="...", private_key_path="...")
You can also flip via the KALSHI_DEMO=true env var when using from_env().
Demo and production keys are different
Kalshi issues separate keys for the demo and production environments. Make
sure the demo flag matches the key you're using, or every request will
401.
Async¶
Identical, with AsyncKalshiClient:
import asyncio
from kalshi import AsyncKalshiClient
async def main() -> None:
async with AsyncKalshiClient.from_env() as client:
page = await client.markets.list(status="open", limit=5)
for market in page:
print(market.ticker)
asyncio.run(main())
Public / unauthenticated usage¶
You don't need credentials for most public market data:
from kalshi import KalshiClient
with KalshiClient(demo=True) as client:
assert client.is_authenticated is False
markets = client.markets.list(status="open", limit=5)
A handful of "public-looking" endpoints still require auth at the server
(markets.orderbook, markets.bulk_orderbooks,
series.forecast_percentile_history, exchange.user_data_timestamp).
Calling those on an unauthenticated client raises
AuthRequiredError preflight — no network round-trip.
If you instead call a private endpoint with the wrong scope or expired
credentials, the server returns 401/403 and the transport maps it to
KalshiAuthError. AuthRequiredError is a subclass of
KalshiAuthError, so catching the parent covers both.
Direct KalshiAuth usage¶
For the WebSocket client and other lower-level use cases you
may want to construct KalshiAuth directly:
from kalshi import KalshiAuth
# From a key file
auth = KalshiAuth.from_key_path("your-key-id", "~/.kalshi/private_key.pem")
# From a PEM string already in memory
auth = KalshiAuth.from_pem("your-key-id", pem_string)
# From the environment — raises if vars are missing
auth = KalshiAuth.from_env()
# From the environment — returns None on missing creds, but still raises
# KalshiAuthError if vars are set but malformed
maybe_auth = KalshiAuth.try_from_env()
Key format constraints¶
from_pem and from_key_path are strict about format. If your key fails to
load, check:
- Must be RSA. EC, Ed25519, DSA keys are rejected.
- Must be PKCS#8 (
-----BEGIN PRIVATE KEY-----). Legacy PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) is supported by the underlyingcryptographylibrary on a best-effort basis. -
Must be unencrypted. Passphrase-protected keys raise
KalshiAuthErrorwith a hint. Strip the passphrase withopenssl pkey:
Manual signing¶
KalshiAuth.sign_request(method, path, timestamp_ms=None) is part of the
public API for callers building custom transports. The path is the URL path
only — query string stripped, trailing slash stripped (except for the literal
/), with percent-encoded sequences normalized to uppercase hex per RFC 3986.
from kalshi import KalshiAuth
auth = KalshiAuth.from_env()
headers = auth.sign_request("GET", "/trade-api/v2/exchange/status")
# headers = {"KALSHI-ACCESS-KEY": ..., "KALSHI-ACCESS-SIGNATURE": ...,
# "KALSHI-ACCESS-TIMESTAMP": ...}
See the API reference for the full surface.