🚧 AfterTrade is in early development.Privacy Policy |Terms of Service

Kalshi API Integration Guide

How to authenticate with Kalshi's trading API using RSA-PSS signatures

Note: This guide is written for developers comfortable with cryptography libraries and API integration. Most Kalshi traders won't implement this themselves—that's the gap AfterTrade is designed to fill.

1. The Authentication Problem

Most trading APIs use OAuth2 or JWT tokens. Kalshi doesn't. Their API uses RSA-PSS digital signatures to authenticate every single request.

This means:

  • There's no "login" endpoint that returns a token
  • You must cryptographically sign each request individually
  • The signature proves you possess the private key
  • Standard HTTP libraries won't work out of the box

Why RSA-PSS? Kalshi uses RSA-PSS because it's more secure for financial APIs. Signatures prove possession of the private key without transmitting a token that could be intercepted or replayed.

2. How Kalshi Authentication Works

For each API request, you need to:

  1. Create a message to sign:
    timestamp + method + path

    Example: 1704398400000GET/trade-api/v2/portfolio/fills

  2. Sign the message using your private key with RSA-PSS (SHA-256)
  3. Base64-encode the signature
  4. Add three headers to your HTTP request:
    • KALSHI-ACCESS-KEY: Your public API key ID
    • KALSHI-ACCESS-TIMESTAMP: Unix timestamp in milliseconds
    • KALSHI-ACCESS-SIGNATURE: Base64-encoded signature

💡 Technical Details

Signature Algorithm: RSA-PSS with SHA-256 hashing

Timestamp Format: Unix epoch in milliseconds (13 digits)

3. Step-by-Step Implementation

Step 1: Get API Credentials

Log into Kalshi via a browser (not the mobile app) and navigate to Account & Security and then scroll down to API Keys and select Create Key. Nickname it, select either Read-only or Read and Write, then copy both the API key ID and private key file and save them somewhere safe.

Recommendation: For analytics, journaling, or trade review use cases, always select Read-only. Read-and-write should only be used by code that actually places trades.

Step 2: Load Your Private Key

Store your private key securely as an environment variable or secret. Do NOT commit it to version control.

# Store in .env file
KALSHI_API_KEY_ID=your-key-id-here
KALSHI_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAo...
-----END PRIVATE KEY-----"

Step 3: Create Signature Function

Implement a function that takes the HTTP method and path, generates a timestamp, creates the message string, signs it with RSA-PSS, and returns the required headers.

Step 4: Sign Each Request

Call your signature function before every API request and include the headers. The signature is only valid for that specific request (timestamp + method + path).

Step 5: Test Your Integration

Start with a simple GET request to /trade-api/v2/portfolio/balance. If you get authentication errors, verify your key format and message construction.

4. Code Examples

Python Example

Using the cryptography library:

import os
import time
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import requests

# Load credentials
API_KEY_ID = os.environ['KALSHI_API_KEY_ID']
PRIVATE_KEY_PEM = os.environ['KALSHI_PRIVATE_KEY']
BASE_URL = 'https://api.elections.kalshi.com'

# Parse private key
private_key = serialization.load_pem_private_key(
    PRIVATE_KEY_PEM.encode(),
    password=None
)

def create_signature(method: str, path: str):
    """Generate Kalshi API signature headers"""
    timestamp = str(int(time.time() * 1000))
    message = f"{timestamp}{method}{path}"

    # Sign with RSA-PSS
    signature = private_key.sign(
        message.encode(),
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )

    # Base64 encode
    signature_b64 = base64.b64encode(signature).decode()

    return {
        'KALSHI-ACCESS-KEY': API_KEY_ID,
        'KALSHI-ACCESS-TIMESTAMP': timestamp,
        'KALSHI-ACCESS-SIGNATURE': signature_b64
    }

# Example: Get all portfolio fills (with pagination)
def get_all_fills():
    all_fills = []
    cursor = None

    while True:
        path = '/trade-api/v2/portfolio/fills'
        if cursor:
            path += f'?cursor={cursor}'

        headers = create_signature('GET', path)
        response = requests.get(f"{BASE_URL}{path}", headers=headers)
        data = response.json()

        all_fills.extend(data.get('fills', []))
        cursor = data.get('cursor')
        if not cursor:
            break

    return all_fills

fills = get_all_fills()
print(f"Fetched {len(fills)} fills")

Node.js/TypeScript Example

Using Node's built-in crypto module:

import crypto from 'crypto';
import axios from 'axios';

const API_KEY_ID = process.env.KALSHI_API_KEY_ID!;
const PRIVATE_KEY_PEM = process.env.KALSHI_PRIVATE_KEY!;
const BASE_URL = 'https://api.elections.kalshi.com';

function createSignature(method: string, path: string) {
  const timestamp = Date.now().toString();
  const message = `${timestamp}${method}${path}`;

  // Sign with RSA-PSS
  const signature = crypto.sign(
    'sha256',
    Buffer.from(message),
    {
      key: PRIVATE_KEY_PEM,
      padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
      saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
    }
  );

  // Base64 encode
  const signatureB64 = signature.toString('base64');

  return {
    'KALSHI-ACCESS-KEY': API_KEY_ID,
    'KALSHI-ACCESS-TIMESTAMP': timestamp,
    'KALSHI-ACCESS-SIGNATURE': signatureB64,
  };
}

// Example: Get all portfolio fills (with pagination)
async function getAllFills() {
  const allFills = [];
  let cursor: string | undefined;

  while (true) {
    const path = '/trade-api/v2/portfolio/fills' + (cursor ? `?cursor=${cursor}` : '');
    const headers = createSignature('GET', path);

    const response = await axios.get(`${BASE_URL}${path}`, { headers });
    const data = response.data;

    allFills.push(...(data.fills || []));
    cursor = data.cursor;
    if (!cursor) break;
  }

  return allFills;
}

getAllFills().then(fills => console.log(`Fetched ${fills.length} fills`));

4.5 Response Shape

Each fill object returned from /trade-api/v2/portfolio/fills has the following shape:

{
  "action": "buy",                    // "buy" or "sell"
  "count": "10",                      // STRING, not number - contracts traded
  "created_time": "2025-03-14T...",   // ISO 8601 timestamp
  "is_taker": true,                   // boolean
  "no_price": "45",                   // STRING cents (0-100), or null
  "order_id": "abc-123-...",          // UUID
  "side": "yes",                      // "yes" or "no"
  "ticker": "KXBTC-25MAR-50000",      // market ticker
  "trade_id": "def-456-...",          // UUID
  "yes_price": "55"                   // STRING cents (0-100), or null
}

Important: Numeric fields are strings

Fields like count, yes_price, and no_price are returned as strings, not numbers. Always parse them: parseInt(fill.count, 10).

5. Common Pitfalls

❌ Using JWT Instead of RSA-PSS

Kalshi does NOT use JWT tokens. You must use RSA-PSS signature algorithm with SHA-256. Libraries like jsonwebtoken will not work.

❌ Modifying the Private Key Format

Use the private key exactly as downloaded. Do not strip newlines, remove headers/footers, or reformat it. The key should include -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----.

❌ Wrong Message Format

The message MUST be exactly: timestamp + method + path. No spaces, no separators, uppercase method. Path must include query parameters if present.

❌ Timestamp Format

Timestamp must be Unix epoch in milliseconds (13 digits), not seconds (10 digits). Use Date.now() in JavaScript or int(time.time() * 1000) in Python.

❌ Assuming Numeric Fields Are Numbers

Fields like count, yes_price, and no_price are strings, not numbers. Adding them directly (e.g., total += fill.count) will produce "01010" instead of 30. Always parse: parseInt(fill.count, 10).

❌ Forgetting to Paginate

The fills endpoint returns up to 1000 items per request. If cursor is present in the response, there are more results. You must loop until cursor is null or missing to fetch all data.

6. Faster Path: Use Claude Code

If you'd rather not implement this yourself, Claude Code can build the integration for you in about 20 minutes. Paste the prompt below into Claude Code, provide your API credentials when prompted, and you'll have a working trade history export. This is the fastest way to get your Kalshi data into a usable format if you're comfortable running a command-line tool.

Requirements: Node.js installed, Claude Code CLI installed, Kalshi API credentials (read-only).

Create a Node.js script that connects to the Kalshi API and exports my trade history as a CSV.

Requirements:
- Use Node's built-in crypto module (no external signing libraries)
- Read KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY from a .env file
- Authenticate using RSA-PSS with SHA-256, salt length RSA_PSS_SALTLEN_DIGEST
- The signature message format is: timestamp + method + path (no separators)
- Timestamp must be Unix epoch in milliseconds
- Required headers: KALSHI-ACCESS-KEY, KALSHI-ACCESS-TIMESTAMP, KALSHI-ACCESS-SIGNATURE
- Base URL: https://api.elections.kalshi.com
- Fetch all fills from /trade-api/v2/portfolio/fills
- IMPORTANT: Paginate using the cursor field - loop while cursor exists
- IMPORTANT: Numeric fields (count, yes_price, no_price) are STRINGS, not numbers - parse them
- Write the results to trade-history.csv with columns: trade_id, ticker, side, action, count, yes_price, no_price, created_time
- Include clear error handling for auth failures and rate limits
- Add a README explaining how to set up the .env file and run the script

Before writing parsing code, log one raw fill object to see actual field names and types.
Do not include any trading or write operations. Read-only access only.

Note: Review the generated code before running it, especially the credential handling. Your Kalshi private key should never be committed to version control or shared.

7. Quick Reference

API Base URL

https://api.elections.kalshi.com

Required Headers

  • KALSHI-ACCESS-KEY: <your-api-key-id>
  • KALSHI-ACCESS-TIMESTAMP: <unix-ms>
  • KALSHI-ACCESS-SIGNATURE: <base64-signature>

Message Format

timestamp + method + path

Signature Algorithm

RSA-PSS with SHA-256

Salt Length

Python: PSS.MAX_LENGTH
Node.js: RSA_PSS_SALTLEN_DIGEST

Changelog

April 2026

  • Fixed base URL from trading-api.kalshi.com to api.elections.kalshi.com
  • Fixed Node.js salt length from RSA_PSS_SALTLEN_MAX_SIGN to RSA_PSS_SALTLEN_DIGEST
  • Added pagination to code examples (loop while cursor exists)
  • Added Section 4.5: Response Shape with fill object structure
  • Added pitfall: numeric fields (count, yes_price, no_price) are strings, not numbers
  • Added pitfall: forgetting to paginate
  • Updated Claude Code prompt with correct parameters and field inspection guidance
  • Added read-only key recommendation for analytics use cases