March 29, 2026
Kalshi API Authentication in Python: RSA-PSS Signing Guide
The Kalshi REST API
Kalshi exposes a REST API for market data, order management, and portfolio queries. The base URL is https://api.elections.kalshi.com/trade-api/v2. All endpoints are standard JSON over HTTPS. The tricky part is authentication.
Unlike most APIs that use an API key in a header or a Bearer token, Kalshi uses RSA-PSS signature-based authentication. Every request must be signed with your private key. This is not optional and there is no simpler fallback mode.
RSA-PSS Key Signing
Kalshi requires you to generate an RSA key pair and register your public key in the dashboard. For each API request, you sign a message containing the timestamp and HTTP method + path using your private key with the PSS padding scheme and SHA-256.
# Key signing pattern
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import time
timestamp_ms = str(int(time.time() * 1000))
msg = timestamp_ms + method.upper() + path
signature = private_key.sign(
msg.encode('utf-8'),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH
),
hashes.SHA256()
)
The resulting signature is base64-encoded and placed in the KALSHI-ACCESS-SIGNATURE header alongside your key ID and the timestamp.
Authenticated Request Patterns
The cleanest way to handle Kalshi authentication in Python is a pair of helper functions: authenticated_get() and authenticated_post(). Both follow the same signature pattern and return a tuple of (response, payload).
# Return pattern
def authenticated_get(path, params={}):
headers = build_auth_headers('GET', path)
response = requests.get(BASE_URL + path,
headers=headers, params=params)
return response, response.json()
The params argument is important: always pass an explicit empty dict {} when there are no query parameters. Omitting it entirely can cause signature mismatches on some endpoints because the path used to generate the signature must exactly match the request path including any query string.
Common Pitfalls
Timestamp drift. The Kalshi API rejects requests with timestamps more than a few seconds old. If your server clock is skewed, every request will fail with a 401. Always use an NTP-synced clock, or use time.time() directly rather than caching the timestamp at startup.
Path includes query string in signature. When you have query parameters, the signed message uses the full path with parameters appended. Build the query string manually and include it in the message before signing, then pass the same parameters to requests separately. Getting this wrong produces 401 errors only on requests with query params, which can be confusing to debug.
Tuple unpacking. The return value is always (response, payload). It is easy to accidentally assign the whole tuple to a single variable and then try to call .json() on it. Always unpack immediately: res, data = authenticated_get(...).
Rate Limiting
Kalshi enforces rate limits on the API. For paginated endpoints like market listings or portfolio queries, add a 0.25-second sleep between pages to stay well under the limit. The bot scans every 5 minutes, so a brief sleep per page has no meaningful impact on execution latency.
# Paginated fetch with rate limiting
cursor = None
all_markets = []
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
_, data = authenticated_get("/markets", params)
all_markets.extend(data["markets"])
cursor = data.get("cursor")
if not cursor:
break
time.sleep(0.25)
Example: Fetching Open Positions
Open positions live at /portfolio/positions. The response includes ticker symbols, your current YES/NO holdings, average entry price, and current market value. This is what the bot queries at startup and after each order to update its internal state.
# Fetch current positions
_, data = authenticated_get("/portfolio/positions", {})
positions = data.get("market_positions", [])
for pos in positions:
print(pos["ticker"], pos["position"])
The full working implementation — including RSA key loading, header construction, order placement, and error handling — is included in the Predict & Profit Python source code. It handles the authentication boilerplate so you can focus on the strategy layer.