GuidePrediction MarketsPython

Polymarket and Kalshi Odds API: Prediction Markets Data

Get Polymarket and Kalshi sports prices through one API: odds, last price, bid/ask, volume and open interest in the same schema as sportsbook odds.

By the PulseScore teamPublished 7 min read

PulseScore serves Polymarket and Kalshi sports markets through the same REST and WebSocket API as its sportsbooks. Every selection carries decimal odds plus the exchange's own lastPrice, bestBid and bestAsk, so a prediction-market probability lines up with a bookmaker's price for the same match without a second integration.

This guide covers what the two exchanges return, how their prices turn into odds and probabilities, which fields tell you how much to trust a price, and a Python script that compares an exchange's view of a match with a sportsbook's no-vig line.

How do prediction markets differ from sportsbooks?

A sportsbook sets its own prices and builds a margin into them. On a prediction market, traders buy and sell contracts that pay 1 if an outcome happens and 0 if it does not, so the price of a contract is the market's probability for that outcome. Kalshi is a CFTC-regulated US exchange, and both it and Polymarket run order books that match buyers with sellers. For a data consumer that means three numbers per outcome instead of one: the last traded price, the best bid and the best ask.

What do the Polymarket and Kalshi endpoints return?

Both exchanges use the same paths as every other bookmaker in the API, under https://api.pulsescore.net/api/polymarket and https://api.pulsescore.net/api/kalshi:

PathReturns
/{sport}/eventsUpcoming pre-match events, paginated
/{sport}/leaguesLeagues with their event counts
/{sport}/leagues/:league/eventsEvery event in one league
/{sport}/events/:eventIdOne event with every market
/live-eventsIn-play events, filter with ?sport=
/live-events/sportsSports with in-play events now
/live-events/events/:eventIdOne in-play event

Neither exchange publishes a separate league feed, so /leagues is built from the events listed at that moment. Authentication is the same X-Secret header as everywhere else, list endpoints take page and limit (up to 30), and sport keys are the usual kebab-case ones such as soccer, tennis, table-tennis and american-football. What each exchange lists right now is on the sport availability page.

Kalshi also keeps finished events for 30 days at /api/kalshi/results, most recent first and up to 20 per page, with optional sport, league and team filters (exact names, case-insensitive). /results/sports lists the sports with stored results, /results/leagues?sport=soccer lists the exact league names the filter expects, and /results/events/:eventId returns one event. Kalshi settles outcomes on the exchange, so a result carries the event metadata and final open interest rather than a score.

What does a prediction-market selection look like?

Here is a trimmed Polymarket soccer event from /api/polymarket/soccer/events:

json
{
  "eventId": "688669",
  "league": "Russian Premier League",
  "home": "PFK CSKA Moskva",
  "away": "FK Baltika Kaliningrad",
  "startTime": "2026-07-24T17:00:00.000Z",
  "volume": 3173.35,
  "volume24hr": 3000.36,
  "liquidity": 105933.49,
  "markets": [
    {
      "canonicalMarket": "MATCH_RESULT",
      "period": "FULL_TIME",
      "marketId": "ml_rus-csk-bal-2026-07-24",
      "isActive": true,
      "volume": 2138.68,
      "liquidity": 21425.63,
      "selections": [
        { "canonicalOutcome": "HOME", "odds": 2.1277, "lastPrice": 0.455, "bestBid": 0.44, "bestAsk": 0.47, "isActive": true },
        { "canonicalOutcome": "DRAW", "odds": 3.3333, "lastPrice": 0.295, "bestBid": 0.29, "bestAsk": 0.3, "isActive": true },
        { "canonicalOutcome": "AWAY", "odds": 3.8462, "lastPrice": 0.245, "bestBid": 0.23, "bestAsk": 0.26, "isActive": true }
      ]
    }
  ]
}
FieldWhereMeaning
oddsBothDecimal price to buy now
lastPriceBothLast traded price, 0 to 1
bestBid, bestAskBoth, when quotedTop of the order book
volume, volume24hrPolymarketTraded volume, total and 24 hours
liquidityBothDepth resting in the book
openInterestKalshiOpen interest per market and event
marketTickerKalshiThe exchange's market ticker

Polymarket markets carry a marketId and Kalshi markets a marketTicker. The Polymarket API and Kalshi API pages show complete sample responses.

How do prices become odds and probabilities?

A price of 0.47 means paying 0.47 for a contract that returns 1, so the outcome is priced at a 47% chance and the decimal odds are 1 / 0.47 = 2.13. PulseScore sets odds to 1 / bestAsk, the price of buying that outcome right now, and falls back to 1 / lastPrice when no ask is quoted. That keeps odds comparable with a bookmaker's price, which is also the price you can take now.

For an estimate of the probability itself, the mid-point of bid and ask is usually better than either side. In the example above the asks add up to 1.03, the bids to 0.96 and the mids to 0.995, close to the 1.00 a complete market should sum to. The odds math guide explains booksums, overround and no-vig odds in detail.

How much should you trust a price?

  • Spread: bestAsk - bestBid. A spread of 0.01 means buyers and sellers agree closely; a wide spread means the mid is a rough guess and the last trade may sit anywhere inside it.
  • Liquidity and volume: on Polymarket, liquidity is the depth resting in the book and volume and volume24hr are the amounts traded, on the event and per market. A price nobody trades is a weak signal.
  • Open interest: on Kalshi, openInterest on each market and summed on the event shows how much is still held in its contracts, and liquidity on the event shows the depth behind the quotes.
  • Staleness: lastPrice is the most recent trade, which can be hours old on a quiet market. Prefer the mid when both sides are quoted.

How do you compare an exchange with a sportsbook?

Both exchanges use the same canonical markets as every sportsbook in the API, so a Polymarket MATCH_RESULT lines up with MATCH_RESULT on Betfair Sportsbook, Bet365 or any other bookmaker, and one API key covers all 60+ of them. The script below pulls one league from each source, matches fixtures by kick-off time and team names, removes the bookmaker's margin and prints the gap per outcome:

python
import difflib
from datetime import datetime, timedelta
from urllib.parse import quote

import requests

API = "https://api.pulsescore.net/api"
HEADERS = {"X-Secret": "YOUR_API_KEY", "Accept-Encoding": "gzip"}

def league_events(source, sport, league):
    """Every pre-match event in one league, markets included."""
    url = f"{API}/{source}/{sport}/leagues/{quote(league, safe='')}/events"
    response = requests.get(url, headers=HEADERS, timeout=15)
    response.raise_for_status()
    return response.json()["events"]

def kickoff(event):
    return datetime.fromisoformat(event["startTime"].replace("Z", "+00:00"))

def normalize(name):
    return " ".join(name.lower().replace("-", " ").replace(".", " ").split())

def similar(a, b):
    return difflib.SequenceMatcher(None, normalize(a), normalize(b)).ratio()

def same_fixture(a, b, window=timedelta(minutes=30), threshold=0.6):
    return (abs(kickoff(a) - kickoff(b)) <= window
            and similar(a["home"], b["home"]) >= threshold
            and similar(a["away"], b["away"]) >= threshold)

def match_result(event):
    """{outcome: selection} of the active full-time 1X2 market, or None."""
    for market in event.get("markets", []):
        if (market.get("canonicalMarket") == "MATCH_RESULT"
                and market.get("period") == "FULL_TIME" and market.get("isActive")):
            sels = {s.get("canonicalOutcome"): s for s in market.get("selections", [])
                    if s.get("isActive") and s.get("odds", 0) > 1}
            if set(sels) == {"HOME", "DRAW", "AWAY"}:
                return sels
    return None

def exchange_probs(sels):
    """Bid/ask mid (or last trade) per outcome, scaled to sum to 1."""
    raw = {}
    for outcome, s in sels.items():
        bid, ask = s.get("bestBid"), s.get("bestAsk")
        if bid is not None and ask is not None and 0 < bid <= ask < 1:
            raw[outcome] = (bid + ask) / 2
        else:
            raw[outcome] = s.get("lastPrice") or 1 / s["odds"]
    total = sum(raw.values())
    return {k: v / total for k, v in raw.items()}

def no_vig_probs(sels):
    """Sportsbook implied probabilities with the margin removed."""
    raw = {k: 1 / s["odds"] for k, s in sels.items()}
    total = sum(raw.values())
    return {k: v / total for k, v in raw.items()}

# Copy each league name exactly as that source's /soccer/leagues lists it.
exchange = league_events("polymarket", "soccer", "English Premier League")
book = league_events("betfair-sb", "soccer", "English Premier League")

for event in exchange:
    twin = next((b for b in book if same_fixture(event, b)), None)
    ex = match_result(event)
    bk = match_result(twin) if twin else None
    if not ex or not bk:
        continue
    p, f = exchange_probs(ex), no_vig_probs(bk)
    print(f"{event['home']} v {event['away']}")
    for outcome in ("HOME", "DRAW", "AWAY"):
        gap = (p[outcome] - f[outcome]) * 100
        print(f"  {outcome:<5} exchange {p[outcome]:6.1%}  book {f[outcome]:6.1%}  {gap:+.1f} pts")

With an exchange quoting 0.40/0.42, 0.26/0.28 and 0.31/0.33 and a bookmaker at 2.30, 3.50 and 3.10, the output looks like this:

text
Fulham v Chelsea
  HOME  exchange  41.0%  book  41.7%  -0.7 pts
  DRAW  exchange  27.0%  book  27.4%  -0.4 pts
  AWAY  exchange  32.0%  book  30.9%  +1.1 pts

Two requests cover a whole league, one per source, and rate limits apply per bookmaker, so this fits every plan including the free one at 500 requests a month. Swap polymarket for kalshi, or betfair-sb for any other bookmaker id, and nothing else changes. Event ids differ per source, which is why the script matches on names and kick-off: the 0.6 similarity threshold handles Fulham against Fulham FC, while pairs like Man Utd and Manchester United need an alias table.

Live prices: REST, WebSocket or Stream API

In-play markets come from /api/polymarket/live-events and /api/kalshi/live-events, or as a push feed on the PRO, MAX and ULTRA plans: wss://api.pulsescore.net/api/polymarket/ws/live?key=YOUR_API_KEY&sport=soccer (and the same path under kalshi) sends every in-play event of that sport about once a second, in the same schema as the REST responses. Both exchanges are also on the Stream API, which sends each board once and then only what changed.

Caveats before you call it an edge

  • Thin markets: a small order can move a shallow book, so weigh comparisons by liquidity, volume24hr or openInterest.
  • Stale trades: a last price can be old; use the mid when both sides are quoted and check the spread.
  • Fees: the prices are order-book quotes and trades, with any exchange fees charged separately, so subtract them before comparing with a bookmaker's price.
  • Resolution rules: check what a contract settles on, for example regular time or including extra time, before treating it as the same outcome as a bookmaker's market.
  • Timing: compare prices fetched close together, and live prices only with live prices.

Frequently asked questions

Does PulseScore have a Polymarket API?

Yes. PulseScore serves Polymarket sports markets under /api/polymarket with the same paths and schema as its sportsbooks: pre-match events, leagues, single events, live events and a WebSocket feed on the PRO, MAX and ULTRA plans. Each selection carries decimal odds, lastPrice, bestBid and bestAsk, and events include volume, 24-hour volume and liquidity.

Does PulseScore have a Kalshi API?

Yes. Kalshi markets are served under /api/kalshi with decimal odds, lastPrice, bestBid and bestAsk per selection, open interest per market and event, live events, a WebSocket feed on the PRO, MAX and ULTRA plans, and a 30-day results archive at /api/kalshi/results.

How are prediction-market prices converted to decimal odds?

A contract price between 0 and 1 is the market's implied probability. PulseScore sets odds to 1 divided by the best ask, the price of buying the outcome now, and falls back to 1 divided by the last traded price when no ask is quoted. A best ask of 0.47 gives odds of about 2.13.

Can I compare Polymarket or Kalshi with sportsbook odds?

Yes. Both use the same canonical markets as every sportsbook in the API, so a MATCH_RESULT market on Polymarket lines up with MATCH_RESULT on Bet365 or Betfair Sportsbook. Remove the bookmaker's margin first, then compare its fair probabilities with the exchange's bid/ask mid. Event ids differ per source, so match fixtures by kick-off time and team names.

Do Polymarket and Kalshi prices include fees?

No. The prices are the exchanges' order-book quotes and last trades, and any trading fees are charged separately, so account for them before treating a price difference as an edge.