Polymarket API & Kalshi API: Python Guide to Prediction Market Data
The sports betting data landscape has fractured into two distinct parallel universes.
On one side, you have the Traditional Sportsbooks (DraftKings, Pinnacle, Bet365). They operate on a “Fixed Odds” model, managed by risk teams and algorithms. They are the giants of volume and liquidity.
On the other side, you have the rising Prediction Markets: Polymarket (Crypto/Polygon) and Kalshi (CFTC-Regulated). These operate on an “Order Book” model, where prices are set purely by supply and demand from users.
For developers and quantitative bettors, this split is an opportunity. When two markets try to price the same event using different mechanisms, inefficiencies are guaranteed.
In this guide, we’ll show you the new way to access Polymarket and Kalshi data: through OddsPapi’s unified API. One API key. One request. Polymarket, Kalshi, Pinnacle, and 300+ sportsbooks—all normalized and ready to compare.
Quick Comparison: The Old Way vs. OddsPapi
Before diving into the code, here’s the landscape:
| Feature | Polymarket Direct | Kalshi Direct | OddsPapi (Unified) |
|---|---|---|---|
| Data Sources | Polymarket only | Kalshi only | Polymarket + Kalshi + 300+ Sportsbooks |
| Price Format | $0.00–$1.00 shares | 1¢–99¢ contracts | Decimal odds (normalized) |
| Auth Required | No (read-only) | No (public endpoints) | 1 API Key (covers everything) |
| Data Normalization | DIY conversion | DIY conversion | Already done for you |
| Order Book Depth | Yes (via CLOB) | Yes (bid/ask) | Yes (via exchangeMeta) |
| Arbitrage Detection | Must combine with sportsbook API | Must combine with sportsbook API | Compare all sources in one response |
| Python Libraries | py-clob-client, gamma-api | requests + JWT handling | Just requests |
The bottom line: Why juggle 3 different APIs with 3 different data formats when you can get everything in one call?
Part 1: Understanding the Market Types
Before diving into code, you need to understand the fundamental difference between these market types.
Prediction Markets (The Order Book Model)
Both Kalshi and Polymarket function like the Nasdaq. They use a CLOB (Central Limit Order Book).
- Pricing: Prices range from $0.00 to $1.00. This represents the Implied Probability of the event happening.
- Liquidity: Peer-to-peer. If you want to bet $10,000, there must be a counterparty willing to take the other side.
- The Data: You get a list of Bids (offers to buy) and Asks (offers to sell) at various price levels.
Traditional Sportsbooks (The Dealer Model)
Sportsbooks function like a casino dealer.
- Pricing: Presented in American (-110) or Decimal (1.91) odds.
- Liquidity: The “House” takes the risk. They set limits based on their confidence in the line.
- The Data: A single set of odds per market.
The opportunity: When these two systems price the same event differently, you can exploit the gap.
Part 2: Accessing Polymarket via OddsPapi
Here’s the old way to get Polymarket data:
# The OLD way - juggling multiple libraries
pip install py-clob-client
from py_clob_client.client import ClobClient
client = ClobClient("https://clob.polymarket.com")
markets = client.get_simplified_markets()
# Then manually normalize prices...
# Then separately query sportsbooks...
# Then try to match events across APIs...
The new way? One API call:
import requests
API_KEY = "YOUR_ODDSPAPI_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
# Get Polymarket + Pinnacle + Bet365 in ONE request
response = requests.get(
f"{BASE_URL}/odds",
params={
"apiKey": API_KEY,
"fixtureId": "id1400003167969464", # Any NFL/NBA fixture
"bookmakers": "polymarket,pinnacle,bet365"
}
)
data = response.json()
# All odds already normalized to decimal format
polymarket = data['bookmakerOdds'].get('polymarket', {})
pinnacle = data['bookmakerOdds'].get('pinnacle', {})
bet365 = data['bookmakerOdds'].get('bet365', {})
The exchangeMeta: Full Order Book Depth
OddsPapi doesn’t just give you the midpoint. For prediction markets, you get the full order book via exchangeMeta:
# Polymarket response includes order book depth
{
"price": 1.786, # Already in decimal odds
"exchangeMeta": {
"back": [
{"cents": 0.56, "price": 1.786, "size": 7144.62, "limit": 4000.99},
{"cents": 0.57, "price": 1.754, "size": 271737.16, "limit": 154890.18},
{"cents": 0.58, "price": 1.724, "size": 460004.85, "limit": 266802.81}
],
"lay": [
# Opposing side of the book
]
}
}
This gives you:
- price: Current best price in decimal odds (already normalized)
- cents: Original Polymarket share price ($0.56 = 56% implied probability)
- size: Total liquidity available at that price level
- limit: Maximum bet size you can place
Part 3: Accessing Kalshi via OddsPapi
Kalshi is CFTC-regulated and available to US residents. OddsPapi includes Kalshi as a bookmaker with liveOdds: true.
# Include Kalshi in your query
response = requests.get(
f"{BASE_URL}/odds",
params={
"apiKey": API_KEY,
"fixtureId": "YOUR_FIXTURE_ID",
"bookmakers": "kalshi,polymarket,pinnacle"
}
)
data = response.json()
kalshi = data['bookmakerOdds'].get('kalshi', {})
Coverage Note: Kalshi’s sports coverage focuses on major US events (NFL spreads, totals, player props). For Kalshi-only markets like elections or economic indicators, you’ll still need their direct API. But for sports betting arbitrage? OddsPapi has you covered.
When to Use Kalshi Directly
Kalshi excels at non-sports prediction markets. If you need elections, weather, or economic data, here’s their public endpoint:
import requests
# For non-sports Kalshi markets (elections, etc.)
url = "https://api.elections.kalshi.com/trade-api/v2/markets"
params = {"limit": 10, "status": "open"}
response = requests.get(url, params=params)
markets = response.json().get('markets', [])
Part 4: Data Normalization (Already Done)
This is where OddsPapi saves you the most time. The old approach required manual conversion:
# The OLD way - manual conversion
def share_to_decimal(share_price):
"""Convert $0.56 share price to 1.786 decimal odds"""
return 1 / share_price
def cents_to_decimal(cents):
"""Convert 56¢ Kalshi price to 1.786 decimal odds"""
return 1 / (cents / 100)
def american_to_decimal(american):
"""Convert -150 to 1.667 decimal odds"""
if american > 0:
return (american / 100) + 1
else:
return (100 / abs(american)) + 1
With OddsPapi, all prices come pre-normalized to decimal odds. Polymarket, Kalshi, Pinnacle, Bet365—they’re all in the same format, ready to compare.
Reference: The Math Behind the Conversions
Understanding the math helps you interpret the data:
| Source Format | Example | Implied Prob | Decimal Odds |
|---|---|---|---|
| Polymarket shares | $0.56 | 56% | 1.786 |
| Kalshi cents | 56¢ | 56% | 1.786 |
| American odds | -127 | 56% | 1.787 |
| Decimal odds | 1.786 | 56% | 1.786 |
The formula is simple: Decimal Odds = 1 / Implied Probability
Part 5: Arbitrage Detection (The Real Power)
Now for the money. With unified data, arbitrage detection becomes trivial.
Strategy A: Prediction Market vs. Sharp Book Scanner
Polymarket prices are driven by retail sentiment. Pinnacle prices are set by professional bettors. When they diverge, one is wrong.
import requests
API_KEY = "YOUR_ODDSPAPI_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
def find_prediction_market_edges(fixture_id):
"""
Compare Polymarket to Pinnacle.
If Polymarket is offering better odds than the sharp book,
that's a potential edge.
"""
response = requests.get(
f"{BASE_URL}/odds",
params={
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmakers": "polymarket,pinnacle"
}
)
data = response.json()
poly = data['bookmakerOdds'].get('polymarket', {})
pinnacle = data['bookmakerOdds'].get('pinnacle', {})
if not poly or not pinnacle:
return None
# Compare Full Time Result (Market 101)
poly_markets = poly.get('markets', {})
pinn_markets = pinnacle.get('markets', {})
for market_id in poly_markets:
if market_id not in pinn_markets:
continue
poly_outcomes = poly_markets[market_id].get('outcomes', {})
pinn_outcomes = pinn_markets[market_id].get('outcomes', {})
for outcome_id in poly_outcomes:
if outcome_id not in pinn_outcomes:
continue
# Get prices (already in decimal odds)
poly_price = poly_outcomes[outcome_id]['players']['0']['price']
pinn_price = pinn_outcomes[outcome_id]['players']['0']['price']
# If Polymarket offers better odds than Pinnacle, flag it
edge = ((poly_price - pinn_price) / pinn_price) * 100
if edge > 2: # 2% edge threshold
print(f"EDGE DETECTED: Market {market_id}, Outcome {outcome_id}")
print(f" Polymarket: {poly_price:.3f}")
print(f" Pinnacle: {pinn_price:.3f}")
print(f" Edge: {edge:.1f}%")
# Run on a specific fixture
find_prediction_market_edges("id1400003167969464")
Strategy B: Multi-Source Arbitrage
True arbitrage means guaranteed profit regardless of outcome. With 300+ bookmakers, you can find situations where:
def check_arbitrage(fixture_id):
"""
Check if backing all outcomes across different books
guarantees profit.
"""
response = requests.get(
f"{BASE_URL}/odds",
params={
"apiKey": API_KEY,
"fixtureId": fixture_id,
"marketId": "101" # Full Time Result (1X2)
}
)
data = response.json()
best_prices = {} # outcome_id -> (best_price, bookmaker)
for bookie, bookie_data in data.get('bookmakerOdds', {}).items():
markets = bookie_data.get('markets', {})
if '101' not in markets:
continue
for outcome_id, outcome_data in markets['101'].get('outcomes', {}).items():
price = outcome_data['players']['0']['price']
if outcome_id not in best_prices or price > best_prices[outcome_id][0]:
best_prices[outcome_id] = (price, bookie)
# Calculate arbitrage margin
# If sum of (1/price) < 1, arbitrage exists
if best_prices:
margin = sum(1/p[0] for p in best_prices.values())
profit_pct = (1 - margin) * 100
print(f"Best prices across all books:")
for outcome, (price, bookie) in best_prices.items():
print(f" Outcome {outcome}: {price:.3f} @ {bookie}")
print(f"\nMargin: {margin:.4f}")
if profit_pct > 0:
print(f"ARBITRAGE: {profit_pct:.2f}% guaranteed profit")
else:
print(f"No arbitrage (margin: {abs(profit_pct):.2f}%)")
check_arbitrage("id1400003167969464")
Strategy C: News Latency Arbitrage
Prediction markets react fast to news. Sportsbooks are slower. Here’s the workflow:
- Monitor Polymarket: Watch for velocity spikes (price drops 15% in 30 seconds)
- Query OddsPapi: Check if Bet365, DraftKings, or offshore books still have stale lines
- Execute: Bet the “correct” side on the slow book before they update
import time
def monitor_velocity(fixture_id, interval=30):
"""
Track Polymarket price velocity.
Alert when sharp movement detected.
"""
previous_prices = {}
while True:
response = requests.get(
f"{BASE_URL}/odds",
params={
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmakers": "polymarket"
}
)
data = response.json()
poly = data['bookmakerOdds'].get('polymarket', {})
for market_id, market_data in poly.get('markets', {}).items():
for outcome_id, outcome_data in market_data.get('outcomes', {}).items():
price = outcome_data['players']['0']['price']
key = f"{market_id}_{outcome_id}"
if key in previous_prices:
change = (price - previous_prices[key]) / previous_prices[key] * 100
if abs(change) > 5: # 5% move threshold
print(f"VELOCITY ALERT: {key}")
print(f" Previous: {previous_prices[key]:.3f}")
print(f" Current: {price:.3f}")
print(f" Change: {change:+.1f}%")
previous_prices[key] = price
time.sleep(interval)
Why OddsPapi is the Hub
You can write scripts for Polymarket and Kalshi all day, but a signal is useless if you don’t know the “True” price.
Prediction markets are often wrong. They have lower limits and less liquidity than major sportsbooks. To know if a Polymarket price is signal or noise, you need a benchmark.
OddsPapi provides that benchmark.
- Unified Data: Polymarket, Kalshi, and 300+ sportsbooks in one response. No more juggling APIs.
- Pre-Normalized: Everything in decimal odds. No conversion code needed.
- Order Book Depth: Full
exchangeMetafor prediction markets—not just midpoints. - Real-Time WebSockets: See lines move instantly. Critical for latency arbitrage.
- Historical Data: backtest your strategies with OddsPapi against years of odds data—free on the starter tier.
- Sharp Books Included: Pinnacle, OddsPapi Singbet guide, and other sharp references to validate signals.
Frequently Asked Questions
Do I need a Polymarket wallet to get data?
No. With OddsPapi, you just need an API key. We handle the connection to Polymarket’s CLOB and normalize the data for you. No crypto wallet, no py-clob-client, no Gamma API—just one REST endpoint.
Is Polymarket data real-time via OddsPapi?
Yes. OddsPapi provides live odds with liveOdds: true for Polymarket. For sub-second latency, use our WebSocket API to stream updates as they happen.
Does OddsPapi have Kalshi coverage for all markets?
OddsPapi includes Kalshi for sports markets where coverage overlaps (NFL, NBA). For Kalshi-only categories like elections, economic indicators, or weather, you’ll need Kalshi’s direct API. But for sports arbitrage? We’ve got both prediction markets and 300+ sportsbooks unified.
How do prediction market odds compare to sportsbook odds?
Prediction markets often have higher variance due to lower liquidity and retail-driven pricing. Sharp sportsbooks like Pinnacle and Singbet have more efficient pricing. This inefficiency creates arbitrage opportunities—use OddsPapi to compare them all in one request.
Can I automate trading on Polymarket?
Polymarket supports automated trading via their CLOB API (requires wallet auth). OddsPapi is for data. The workflow: detect opportunities via OddsPapi, then execute on Polymarket or sportsbooks. For exchange-style execution, see our OddsPapi Betfair guide.
Conclusion
The era of juggling multiple APIs is over. Polymarket, Kalshi, Pinnacle, Bet365—they’re all in one place now.
Stop writing conversion functions. Stop matching events across different data formats. Stop maintaining three different API integrations.
One API key. One request. All the data you need to find edges.