Polymarket API vs Kalshi API for Real-Time Prediction Market Data 2025

Quick Summary: Polymarket vs Kalshi API at a Glance

If you're building a US-regulated fintech dashboard or an election forecast widget where CFTC compliance is non-negotiable, Kalshi is your API. If you're a researcher, DeFi developer, or building a global prediction market aggregator that needs deep liquidity and open blockchain data without KYC friction, Polymarket is the stronger choice. Both APIs are production-ready in 2025, but they serve fundamentally different builder profiles.

Side-by-side feature comparison table

| Dimension | Kalshi | Polymarket CLOB | |---|---|---| | Auth method | API key (header) + KYC required | ECDSA wallet signature (L1/L2) or API key | | Rate limits | 10 req/s REST; varies by tier | 10 req/s REST; WebSocket unlimited | | Data freshness | ~100ms REST polling; real-time WS | ~50–80ms WebSocket; REST near-real-time | | WebSocket support | ✓ (orderbook_delta, ticker) | ✓ (price_change, book_delta) | | Regulatory status | CFTC-designated DCM (US regulated) | Crypto-native, CFTC gray area | | Geographic restrictions | US only for trading; API global | US users blocked from trading | | Free tier / pricing | Free API key; trading fees apply | Free read access; no KYC for data | | Official SDK | kalshi-python (official) | py-clob-client, gamma-client | | Sandbox / testnet | ✓ demo.kalshi.com | ✓ staging endpoints available | | Historical data | REST /trades endpoint (limited depth) | The Graph subgraph (full on-chain history) |

Who should use Polymarket API

Polymarket suits developers who need global reach, don't want to put users through KYC just to read market data, and want on-chain verifiability. Read access requires zero authentication — you can hit the CLOB API or query The Graph without an account.

Who should use Kalshi API

Kalshi is the right call when your app serves US users in a regulated context. With Minnesota's 2025 prediction market ban adding to an already patchwork state regulatory landscape, having a CFTC-designated exchange behind your data feed gives legal cover that a crypto-native platform cannot. Kalshi's fiat settlement and onboarding friction (KYC, entity verification) are features, not bugs, for compliance-first teams.


Authentication and API Access Setup

Kalshi API key generation and authentication flow

Sign up at kalshi.com, complete KYC, navigate to Settings → API Keys, and generate a key. Every request passes the key as an Authorization: Token <key> header. No OAuth dance, no token refresh — it's stateless and dead simple.

import requests

KALSHI_API_KEY = "your_kalshi_api_key_here"
BASE_URL = "https://trading-api.kalshi.com/trade-api/v2"

def get_kalshi_markets(status: str = "open") -> list:
    """Fetch active markets from Kalshi REST API."""
    headers = {
        "Authorization": f"Token {KALSHI_API_KEY}",
        "Accept": "application/json",
    }
    params = {"status": status, "limit": 100}
    response = requests.get(
        f"{BASE_URL}/markets",
        headers=headers,
        params=params,
        timeout=10,
    )
    response.raise_for_status()
    return response.json().get("markets", [])

if __name__ == "__main__":
    markets = get_kalshi_markets()
    print(f"Fetched {len(markets)} open Kalshi markets")
    for m in markets[:5]:
        print(m["ticker"], m["title"])

Polymarket CLOB API authentication with wallet signing

Polymarket's CLOB uses ECDSA signatures derived from an Ethereum wallet. For read-only data, you can skip auth entirely. For order placement, you derive a CLOB API key by signing a nonce with your L1 private key. The JavaScript ecosystem has the best tooling for this:

// Node.js: Initialize Polymarket CLOB client with ethers.js wallet signing
import { ethers } from "ethers";
import fetch from "node-fetch";

const CLOB_BASE = "https://clob.polymarket.com";
const PRIVATE_KEY = process.env.POLY_PRIVATE_KEY; // 0x-prefixed

async function getClobApiKey(wallet) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = "0";
  const message = `Polymarket CLOB API Key\ntimestamp: ${timestamp}\nnonce: ${nonce}`;
  const signature = await wallet.signMessage(message);

  const response = await fetch(`${CLOB_BASE}/auth/api-key`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      address: wallet.address,
      signature,
      timestamp,
      nonce,
    }),
  });
  const data = await response.json();
  return data; // { apiKey, secret, passphrase }
}

const provider = new ethers.JsonRpcProvider("https://polygon-rpc.com");
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);

getClobApiKey(wallet).then(console.log);

Rate limits and throttling behavior compared

Both platforms enforce 10 requests/second on REST endpoints per API key. Kalshi returns 429 Too Many Requests with a Retry-After header. Polymarket's CLOB returns a 429 without Retry-After — implement exponential backoff starting at 200ms. For real-time feeds, WebSocket is the right tool on both platforms; REST polling at scale will get you throttled quickly.


Fetching Real-Time Market Data: REST Endpoints

Kalshi: GET /markets, /orderbook, /trades endpoints

Kalshi's three core read endpoints cover everything for a data feed. GET /markets returns market metadata including the ticker, resolution criteria, and yes_bid/yes_ask in cents (0–100). GET /markets/{ticker}/orderbook returns full depth. GET /markets/{ticker}/trades returns recent executions.

Polymarket: /markets, /book, /last-trade-price endpoints

Polymarket's CLOB exposes similar endpoints but prices are 0–1 decimal probabilities (e.g., 0.65 = 65¢ implied probability). GET /markets returns condition IDs and token IDs. GET /book?token_id={id} returns the orderbook. GET /last-trade-price?token_id={id} gives the last fill.

Response schema differences and normalization strategies

The price denomination mismatch (cents vs decimals) is the biggest gotcha when consuming both. Here's a normalization function that irons it out:

import requests
from typing import TypedDict

KALSHI_API_KEY = "your_kalshi_api_key_here"
KALSHI_BASE = "https://trading-api.kalshi.com/trade-api/v2"
CLOB_BASE = "https://clob.polymarket.com"

class NormalizedMarket(TypedDict):
    market_id: str
    yes_bid: float   # always 0.0–1.0
    yes_ask: float   # always 0.0–1.0
    volume_24h: float
    expiry: str      # always ISO 8601

def fetch_kalshi_orderbook(ticker: str) -> NormalizedMarket:
    headers = {"Authorization": f"Token {KALSHI_API_KEY}"}
    r = requests.get(f"{KALSHI_BASE}/markets/{ticker}/orderbook", headers=headers, timeout=10)
    r.raise_for_status()
    ob = r.json()["orderbook"]
    # Kalshi bids/asks are in cents; convert to 0-1
    yes_bids = ob.get("yes", [[]])
    yes_asks = ob.get("no", [[]])  # "no" side = inverse of "yes" ask
    best_bid = yes_bids[0][0] / 100 if yes_bids else 0.0
    best_ask = (100 - yes_asks[0][0]) / 100 if yes_asks else 1.0
    # Fetch volume separately
    mr = requests.get(f"{KALSHI_BASE}/markets/{ticker}", headers=headers, timeout=10)
    mdata = mr.json()["market"]
    return NormalizedMarket(
        market_id=f"kalshi:{ticker}",
        yes_bid=best_bid,
        yes_ask=best_ask,
        volume_24h=mdata.get("volume_24h", 0),
        expiry=mdata["close_time"],  # already ISO 8601
    )

def fetch_polymarket_orderbook(token_id: str, condition_id: str) -> NormalizedMarket:
    r = requests.get(f"{CLOB_BASE}/book", params={"token_id": token_id}, timeout=10)
    r.raise_for_status()
    ob = r.json()
    # Polymarket prices are already 0-1 decimals
    bids = ob.get("bids", [])
    asks = ob.get("asks", [])
    best_bid = float(bids[0]["price"]) if bids else 0.0
    best_ask = float(asks[0]["price"]) if asks else 1.0
    # Volume from /markets endpoint
    mr = requests.get(f"{CLOB_BASE}/markets/{condition_id}", timeout=10)
    mdata = mr.json()
    return NormalizedMarket(
        market_id=f"polymarket:{condition_id}",
        yes_bid=best_bid,
        yes_ask=best_ask,
        volume_24h=float(mdata.get("volume_24h", 0)),
        expiry=mdata.get("end_date_iso", ""),
    )

Timestamp note: Kalshi returns ISO 8601 strings throughout. Polymarket REST returns ISO 8601 in market metadata but Unix milliseconds in WebSocket events — normalize to ISO 8601 at ingestion time.


WebSocket Streaming and Live Order Book Updates

Kalshi WebSocket subscription model and channel types

Kalshi's WebSocket endpoint is wss://trading-api.kalshi.com/trade-api/ws/v2. After connecting, send a JSON subscription message specifying the channel (orderbook_delta, ticker, trade) and the market tickers you want. Authentication uses the same API key, passed as a token field in the subscription message.

Polymarket CLOB WebSocket and event-driven order book diffing

Polymarket's WebSocket is at wss://ws-subscriptions-clob.polymarket.com/ws/. Subscribe with a JSON message specifying type: "subscribe", channel: "live_activity" or "book", and an array of asset IDs. No auth needed for public channels. The price_change event gives you top-of-book snapshots; book events give full depth diffs you apply client-side.

Latency benchmarks and reconnect handling

| Metric | Kalshi WS | Polymarket WS | |---|---|---| | Median latency | ~85ms | ~55ms | | p95 latency | ~140ms | ~95ms | | p99 latency | ~220ms | ~160ms | | Reconnect behavior | Server closes on auth fail; client must re-auth | Server sends ping; close on 3 missed pongs |

Latencies are community-benchmarked from US-East (AWS us-east-1) and vary by network conditions.

Here's an async Python script that subscribes to both simultaneously and merges the price feeds:

import asyncio
import json
import websockets
from datetime import datetime

KALSHI_WS = "wss://trading-api.kalshi.com/trade-api/ws/v2"
POLY_WS = "wss://ws-subscriptions-clob.polymarket.com/ws/"
KALSHI_API_KEY = "your_kalshi_api_key_here"
KALSHI_TICKER = "KXBTCD-25DEC31-T50000"  # example
POLY_ASSET_ID = "your_polymarket_token_id"  # example

price_feed: dict = {}

async def subscribe_kalshi():
    async with websockets.connect(KALSHI_WS) as ws:
        sub_msg = {
            "id": 1,
            "cmd": "subscribe",
            "params": {
                "channels": ["orderbook_delta"],
                "market_tickers": [KALSHI_TICKER],
                "token": KALSHI_API_KEY,
            },
        }
        await ws.send(json.dumps(sub_msg))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("type") == "orderbook_delta":
                bids = msg["msg"].get("yes", [])
                price_feed["kalshi"] = {
                    "price": bids[0][0] / 100 if bids else None,
                    "ts": datetime.utcnow().isoformat(),
                }
                print(f"[Kalshi] {price_feed['kalshi']}")

async def subscribe_polymarket():
    async with websockets.connect(POLY_WS) as ws:
        sub_msg = {
            "type": "subscribe",
            "channel": "price_change",
            "assets_ids": [POLY_ASSET_ID],
        }
        await ws.send(json.dumps(sub_msg))
        async for raw in ws:
            msg = json.loads(raw)
            if isinstance(msg, list):
                for event in msg:
                    if event.get("type") == "price_change":
                        price_feed["polymarket"] = {
                            "price": float(event.get("price", 0)),
                            "ts": datetime.utcnow().isoformat(),
                        }
                        print(f"[Polymarket] {price_feed['polymarket']}")

async def main():
    await asyncio.gather(
        subscribe_kalshi(),
        subscribe_polymarket(),
    )

if __name__ == "__main__":
    asyncio.run(main())

For production, wrap each coroutine in a retry loop with exponential backoff. Kalshi drops the connection after 30 seconds of inactivity — send a ping every 20 seconds.


Historical Data Access and Backtesting Capabilities

Kalshi historical trades and settlement data depth

Kalshi's GET /markets/{ticker}/trades endpoint returns paginated trade history with cursor-based pagination. Practically, you get 90 days of trades via REST before hitting retention limits. Settlement outcomes are available via GET /markets/{ticker} once the market resolves. This is sufficient for short-horizon backtests but inadequate for multi-year strategy research.

Polymarket The Graph subgraph for on-chain historical data

Polymarket's entire order and settlement history is on-chain (Polygon), queryable via The Graph Protocol's hosted service. The official subgraph at https://api.thegraph.com/subgraphs/name/polymarket/omen-xdai gives you resolved markets, trade history, and LP positions going back to launch — far deeper than any REST endpoint could provide.

import requests

THEGRAPH_URL = "https://api.thegraph.com/subgraphs/name/polymarket/polymarket-matic"

def get_resolved_markets(first: int = 50) -> list:
    query = """
    {
      fixedProductMarketMakers(first: %d, where: {resolutionTimestamp_gt: "0"}, orderBy: collateralVolume, orderDirection: desc) {
        id
        question { title }
        collateralVolume
        resolutionTimestamp
        outcomes
        outcomeTokenAmounts
      }
    }
    """ % first
    r = requests.post(THEGRAPH_URL, json={"query": query}, timeout=15)
    return r.json()["data"]["fixedProductMarketMakers"]

Building a local SQLite cache for backtesting

Both APIs have rate limits that make live querying during backtests impractical. Ingest historical data once and cache it:

import sqlite3, requests, time

def build_kalshi_cache(tickers: list, db_path: str = "kalshi_history.db"):
    con = sqlite3.connect(db_path)
    con.execute("CREATE TABLE IF NOT EXISTS trades (ticker TEXT, ts TEXT, price REAL, count INTEGER)")
    headers = {"Authorization": f"Token your_kalshi_api_key_here"}
    for ticker in tickers:
        cursor = None
        while True:
            params = {"limit": 1000}
            if cursor:
                params["cursor"] = cursor
            r = requests.get(f"https://trading-api.kalshi.com/trade-api/v2/markets/{ticker}/trades",
                             headers=headers, params=params, timeout=10)
            data = r.json()
            trades = data.get("trades", [])
            if not trades:
                break
            con.executemany("INSERT INTO trades VALUES (?,?,?,?)",
                            [(ticker, t["created_time"], t["yes_price"] / 100, t["count"]) for t in trades])
            con.commit()
            cursor = data.get("cursor")
            if not cursor:
                break
            time.sleep(0.15)  # stay under rate limit

Critical backtesting note: State-level regulatory actions (Minnesota's 2025 ban, past CFTC enforcement) cause gaps and delistings in historical datasets. Mark these periods in your cache explicitly — a delisted market mid-series looks like zero volume but isn't the same as an illiquid market.


SDK and Tooling Ecosystem

Official Kalshi Python SDK: kalshi-python

Kalshi's official SDK (kalshi-python on PyPI) provides typed Pydantic models, automatic token injection, and sync/async client variants. Last commit cadence is roughly monthly, and it tracks the v2 API closely.

Polymarket py-clob-client and gamma-client libraries

py-clob-client handles CLOB authentication, order management, and market data. gamma-client wraps Polymarket's Gamma markets API (a higher-level metadata layer). Both are community-maintained but actively developed as of 2025.

Quick start and top markets comparison

pip install kalshi-python py-clob-client
# Print top 5 markets by 24h volume from each platform
from kalshi_python import Configuration, ApiClient, MarketApi
from py_clob_client.client import ClobClient

# --- Kalshi ---
kal_config = Configuration(host="https://trading-api.kalshi.com/trade-api/v2")
kal_config.api_key["Token"] = "your_kalshi_api_key_here"
with ApiClient(kal_config) as api_client:
    market_api = MarketApi(api_client)
    markets = market_api.get_markets(status="open", limit=200).markets
    top_kalshi = sorted(markets, key=lambda m: m.volume_24h or 0, reverse=True)[:5]
    print("=== Top 5 Kalshi Markets by 24h Volume ===")
    for m in top_kalshi:
        print(f"{m.ticker}: ${m.volume_24h:,.0f}")

# --- Polymarket ---
poly_client = ClobClient(host="https://clob.polymarket.com")
response = poly_client.get_markets()
poly_markets = response.get("data", [])
top_poly = sorted(poly_markets, key=lambda m: float(m.get("volume_24hr", 0)), reverse=True)[:5]
print("\n=== Top 5 Polymarket Markets by 24h Volume ===")
for m in top_poly:
    print(f"{m['question']}: ${float(m.get('volume_24hr', 0)):,.0f}")

| SDK Dimension | kalshi-python | py-clob-client | |---|---|---| | Typed models | ✓ (Pydantic) | Partial (dataclasses) | | Async support | ✓ (AsyncApiClient) | ✗ (sync only) | | Error handling | HTTP exception classes | Basic raise on 4xx | | Last commit | Active (monthly) | Active (bi-monthly) |


Regulatory Compliance Considerations for Developers

CFTC oversight of Kalshi and what it means for API consumers

Kalshi is a CFTC-designated Designated Contract Market (DCM) — the same regulatory tier as CME Group. This matters because displaying Kalshi data in a US-facing financial app doesn't require you to layer on additional regulatory disclaimers about the underlying exchange's legitimacy. The CFTC won a pivotal court case in 2024 allowing Kalshi to offer election contracts, solidifying its standing.

Geo-blocking, IP filtering, and state-level ban detection

Minnesota passed a prediction market ban in 2025, joining a small but growing list of states with restrictions. From a developer standpoint, API key provisioning is unaffected — you can still call both APIs from anywhere. The compliance burden is on your application layer: you must gate end-user access to prediction market functionality based on jurisdiction.

Use an IP geolocation API (MaxMind GeoIP2, ipinfo.io) to detect the user's state and block trading UI elements accordingly:

import requests

RESTRICTED_STATES = {"MN"}  # expand as regulations evolve

def get_user_state(ip: str) -> str | None:
    """Returns two-letter state code or None if outside US."""
    r = requests.get(f"https://ipinfo.io/{ip}/json", timeout=5)
    data = r.json()
    if data.get("country") != "US":
        return None
    return data.get("region", "")[:2].upper()

def can_access_prediction_markets(ip: str) -> bool:
    state = get_user_state(ip)
    return state is not None and state not in RESTRICTED_STATES

Compliance checklist for developers

  • Terms of Service review: Kalshi prohibits data redistribution without a data license. Polymarket's CLOB data is more permissive for read access.
  • Data redistribution rights: If you're building a data product (not just an app), contact Kalshi's BD team for a data agreement.
  • User location gating: Implement server-side jurisdiction checks — client-side JS is trivially bypassed.
  • Logging for audit trails: Log which markets a user viewed and when, keyed to their session. If a regulator asks, you want evidence of good-faith geo-blocking.
  • OFAC screening: For any trading functionality using Kalshi, screen wallet addresses and user names against OFAC lists.

When to Choose Kalshi API

Kalshi is the right call in these specific scenarios:

  • Regulated fintech dashboards: Your app is distributed to US retail or institutional users and needs to cite a CFTC-regulated data source in your compliance documentation.
  • Election forecast widgets for news publishers: CNN, Bloomberg-style widgets displaying probability estimates. Kalshi's CFTC status means you're not implicitly endorsing an unregulated gambling product.
  • Fiat-native data pipelines: Your backend has no crypto infrastructure and you want USD-denominated volume and settlement data without dealing with stablecoins or on-chain indexers.
  • Enterprise SLA requirements: Kalshi provides documented uptime SLAs and has a dedicated support channel for institutional API users — Polymarket does not.
  • Sentiment signals for US political/economic events: Kalshi's Federal Reserve rate decision markets and election markets have sufficient depth and regulatory clarity for inclusion in quant research.

Watch out for: Onboarding takes days to weeks (KYC, entity verification, sometimes manual review). Market selection is narrower — roughly 200–400 active markets vs Polymarket's thousands. No crypto composability means you can't plug Kalshi data into a DeFi protocol.


When to Choose Polymarket API

Polymarket wins in these situations:

  • Research and academic tools: No KYC for read access means you can build and distribute a research tool that students and academics can use without account creation friction.
  • Global prediction market aggregators: Polymarket operates globally (outside the US for trading), giving you liquidity and market breadth that dwarfs Kalshi for international events.
  • DeFi and Web3 integrations: Polymarket outcomes are on-chain ERC-1155 tokens on Polygon. You can integrate them into smart contracts, vaults, or on-chain hedging strategies natively.
  • Blockchain data pipelines: If your team already runs a Polygon node or uses The Graph, historical Polymarket data slots directly into your existing infra at zero marginal cost.
  • High-volume automated market monitoring: Polymarket's WebSocket latency (p50 ~55ms) is meaningfully lower than Kalshi's (~85ms) — relevant for any latency-sensitive use case.

Watch out for: US users are restricted from trading on Polymarket — display of data may still be permissible, but confirm with legal counsel before building a US-facing trading interface. Crypto wallet dependency for order placement adds complexity for mainstream app users. No CFTC designation means no regulatory safe harbor.


Verdict

For the most common developer use case in 2025 — building a US-facing app that displays real-time prediction market probabilities — Kalshi is the safer default. Its CFTC designation, fiat settlement, and enterprise support remove compliance risk that Polymarket cannot currently match. If you're building research tools, DeFi integrations, or global applications where you need breadth of markets and don't want to force users through crypto onboarding, Polymarket's open data model wins clearly. The sharpest teams building data products in 2025 are actually running both: Polymarket for research and price discovery signals, Kalshi for the compliance-ready production layer. Start with the normalization schema from Section 3 and you can swap or combine sources without rewriting your pipeline.