Valorant Odds API: Real-Time VCT Odds from Pinnacle, Polymarket & 28+ Bookmakers

Valorant Odds API - OddsPapi API Blog
How To Guides May 25, 2026

Looking for a Valorant odds API? PandaScore wants $200/month for live VCT odds. The Odds API doesn’t cover Valorant at all. Bookmakers like Pinnacle don’t have a public API. So if you want to build a model, an arb scanner, or just track Champions Tour line movement, your options are: pay enterprise pricing, or scrape twelve different sites.

There’s a third option. OddsPapi aggregates Valorant odds from 350+ bookmakers — including Pinnacle (the sharp benchmark), Polymarket (exchange pricing), DraftKings, BetMGM, and seven crypto books — into one JSON response. Free tier. No scraping. Free historical data for backtesting.

This post shows you how to pull real Valorant Champions Tour odds in Python. Every code example is tested against the live API and uses real data from a Heretics vs BBL EMEA fixture (May 9, 2026, 28 books pricing the moneyline).

The Valorant Odds API Coverage Reality (Honest Numbers)

Before we get to code, let’s set expectations. We pulled every Valorant fixture across the next 10 days from /v4/fixtures?sportId=61 and counted bookmakers per match.

Tournament Match Books Has Pinnacle?
Champions Tour: EMEA Liquid vs Gentle Mates 29
Champions Tour: EMEA Heretics vs BBL 28
Champions Tour: Americas LOUD vs Leviatan 27
Champions Tour: Americas Sentinels vs NRG 26
Challengers DACH Eintracht Frankfurt vs FOKUS 21
Champions Tour: Americas G2 vs Cloud9 5
Challengers Spain Otakar vs Barca 3
Challengers France Joblife vs WIP 0

The pattern: VCT International Leagues (EMEA / Americas / Pacific) get 25–29 books per match. Challengers tier gets 0–5. Game Changers and tier-2 regional leagues are thin. If you’re building anything that depends on book diversity (line shopping, arb, value scanning), stick to the international leagues.

One more honest note: Valorant on OddsPapi is moneyline-only right now. Match Winner (market ID 611) is the only market shipped across the catalog. No map handicaps, no map totals, no round handicaps. If you need that depth, you’re stuck with sportsbook scraping or PandaScore. For 80% of use cases — model building, line shopping, arb scanning, CLV tracking — moneyline from 28+ books is exactly what you want.

Old Way vs OddsPapi

Approach Books Covered Pinnacle? Crypto Books? Cost Historical Data
PandaScore ~30 Limited From $199/mo Paid add-on
The Odds API 0 (no Valorant) n/a n/a n/a n/a
Scraping individual books Whatever you build No (closed) You’ll get IP-banned Free + your time Build your own
OddsPapi 29 distinct, 28+ per VCT match ✓ (7 books) Free tier ✓ Free

Step 1: Authentication

Grab a free API key. Auth is a query parameter, not a header — this trips up people coming from RapidAPI-style services.

import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

# Smoke test
r = requests.get(f"{BASE_URL}/sports", params={"apiKey": API_KEY})
print(r.status_code)  # 200

Step 2: Find Valorant Fixtures

Valorant’s sportId is 61. The full sport name in the API is ESport Valorant, slug esport-valorant. Pull fixtures over a date window — max 10 days per call.

from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc)
to = now + timedelta(days=7)

r = requests.get(f"{BASE_URL}/fixtures", params={
    "apiKey": API_KEY,
    "sportId": 61,
    "from": now.strftime("%Y-%m-%dT%H:%M:%S"),
    "to":   to.strftime("%Y-%m-%dT%H:%M:%S"),
})
fixtures = [f for f in r.json() if f.get("hasOdds")]

for f in fixtures:
    print(f"{f['tournamentName']:35s}  "
          f"{f['participant1Name']} vs {f['participant2Name']}  "
          f"start={f['startTime']}")

Real output snippet from May 9, 2026:

Champions Tour: EMEA                 Heretics vs BBL              start=2026-05-09T15:15:00.000Z
Champions Tour: EMEA                 Liquid vs Gentle Mates       start=2026-05-09T...
Champions Tour: Americas             LOUD vs Leviatan             start=2026-05-09T...
Champions Tour: Americas             Sentinels vs NRG             start=2026-05-09T...
Challengers DACH                     Eintracht Frankfurt vs FOKUS start=2026-05-09T...

Note that each fixture object also exposes externalProviders.pinnacleId, which is useful if you’re cross-referencing with Pinnacle’s internal IDs from another data source.

Step 3: Pull Live Odds for a Match

One /v4/odds call returns every bookmaker’s pricing for a fixture. The response is nested — bookmakerOdds → {slug} → markets → {marketId} → outcomes → {outcomeId} → players → "0" → price.

fixture_id = "pn619022571630262973"  # Heretics vs BBL

r = requests.get(f"{BASE_URL}/odds", params={
    "apiKey": API_KEY,
    "fixtureId": fixture_id,
})
data = r.json()
books = data["bookmakerOdds"]
print(f"Books pricing this match: {len(books)}")
print(f"Bookmaker slugs: {sorted(books.keys())}")

Real output:

Books pricing this match: 28
Bookmaker slugs: ['atg.se', 'bcgame', 'betmgm.co.uk', 'betplay', 'betrivers',
                  'bingoal.be', 'blaze', 'casumo', 'draftkings', 'duel',
                  'leovegas', 'paf', 'paf.es', 'pinnacle', 'polymarket',
                  'rollbit', 'roobet', 'rushbet.co', 'scooore.be',
                  'stake.bet.br', 'svenskaspel', 'unibet', 'unibet.be',
                  'unibet.com.au', 'unibet.dk', 'unibet.ie', 'unibet.se', 'vertex']

That’s Pinnacle (sharp), Polymarket (exchange), DraftKings + BetRivers + BetMGM UK (regulated), seven crypto books (BCGame, Blaze, Stake, Rollbit, Roobet, Duel, Vertex), and a Unibet/Kindred network across seven jurisdictions. From a single API call.

Step 4: Parse the Moneyline (Market 611)

Valorant’s only market is the Match Winner, ID 611. Outcomes are 611 (participant1 / Home) and 612 (participant2 / Away). Here’s the safe parsing pattern:

def get_winner_prices(books, market_id="611"):
    """Return {bookmaker: (home_price, away_price, active, changed_at)}."""
    out = {}
    for slug, bk in books.items():
        market = bk.get("markets", {}).get(market_id)
        if not market:
            continue
        outcomes = market.get("outcomes", {})
        home = outcomes.get("611", {}).get("players", {}).get("0", {})
        away = outcomes.get("612", {}).get("players", {}).get("0", {})
        out[slug] = (
            home.get("price"),
            away.get("price"),
            home.get("active") and away.get("active"),
            home.get("changedAt"),
        )
    return out

prices = get_winner_prices(books)
for slug, (h, a, active, ts) in sorted(prices.items()):
    if active:
        print(f"{slug:25s}  home={h:>6}  away={a:>6}  updated={ts}")

Sample output (Heretics vs BBL, captured 2026-05-09 at 13:33 UTC):

pinnacle                  home= 1.555  away=  2.47  updated=2026-05-09T09:44:47.301Z
polymarket                home= 1.613  away= 2.564  updated=2026-05-09T13:33:52.353Z
draftkings                home= 1.570  away=  2.30  updated=2026-05-08T07:26:47.100Z
betmgm.co.uk              home= 1.530  away=  2.35  updated=2026-05-09T10:21:10.634Z
betrivers                 home= 1.530  away=  2.35  updated=2026-05-09T10:17:53.078Z
unibet                    home= 1.530  away=  2.35  updated=2026-05-09T10:20:51.621Z
vertex                    home= 1.563  away= 2.497  updated=2026-05-09T10:38:58.788Z
betplay                   home= 1.490  away=  2.35  updated=2026-05-09T10:21:19.368Z
leovegas                  home= 1.490  away=  2.35  updated=2026-05-09T10:20:41.939Z

Step 5: Calculate Vig and Find the Best Price

The whole point of pulling 28 books in one call is to price-shop. Here’s the vig calc and best-price scan in seven lines.

def vig(home, away):
    return (1/home + 1/away - 1) * 100

active = {s: (h, a) for s, (h, a, act, _) in prices.items() if act and h and a}
print(f"\nVig per book (sorted, lowest = sharpest):")
for slug, (h, a) in sorted(active.items(), key=lambda x: vig(*x[1])):
    print(f"  {slug:25s}  vig={vig(h,a):5.2f}%")

best_home = max(active.items(), key=lambda x: x[1][0])
best_away = max(active.items(), key=lambda x: x[1][1])
print(f"\nBest Heretics: {best_home[1][0]} @ {best_home[0]}")
print(f"Best BBL:      {best_away[1][1]} @ {best_away[0]}")

Real output:

Vig per book (sorted, lowest = sharpest):
  polymarket                vig= 1.00%
  vertex                    vig= 4.03%
  pinnacle                  vig= 4.79%
  draftkings                vig= 7.17%
  unibet                    vig= 7.91%
  betmgm.co.uk              vig= 7.91%
  ...
  paf.es                    vig=10.58%
  rushbet.co                vig=10.58%

Best Heretics (across sportsbooks): 1.613 @ polymarket
Best BBL (across sportsbooks):      2.564 @ polymarket

Two things to notice:

  1. Polymarket has 1.00% vig — by far the tightest pricing in the field. That’s because Polymarket is a peer-to-peer prediction market, not a sportsbook taking the other side. If you’re modeling Valorant, Polymarket’s implied probability is the cleanest signal we expose. (See our Polymarket Deep Dive for how the polymarket slug works under the hood — including the exchangeMeta.lay side.)
  2. Pinnacle de-vigs to ~61.4% / 38.6%. Soft books like betplay, leovegas, paf.es are pricing the favorite around 67–68% implied — meaning they’re charging an extra 6 percentage points of overround on top of Pinnacle’s number. That’s where +EV opportunities hide.

For the line-shopping pattern in detail (with the same defensive filters across all sports), see our Line Shopping in Python tutorial — the best_price() helper there works on Valorant unchanged.

Step 6: Scan All Live VCT Matches at Once

Build a slate scanner: for every Valorant fixture with odds, pull the moneyline from every book, and surface mispricings vs Pinnacle.

import time

def scan_valorant_slate(api_key, days=7):
    now = datetime.now(timezone.utc)
    to = now + timedelta(days=days)
    r = requests.get(f"{BASE_URL}/fixtures", params={
        "apiKey": api_key, "sportId": 61,
        "from": now.strftime("%Y-%m-%dT%H:%M:%S"),
        "to":   to.strftime("%Y-%m-%dT%H:%M:%S"),
    })
    fixtures = [f for f in r.json() if f.get("hasOdds")]

    for f in fixtures:
        time.sleep(0.2)  # respect free-tier cooldown
        rr = requests.get(f"{BASE_URL}/odds", params={
            "apiKey": api_key, "fixtureId": f["fixtureId"]
        })
        books = rr.json().get("bookmakerOdds", {})
        prices = get_winner_prices(books)

        pn = prices.get("pinnacle")
        if not (pn and pn[2] and pn[0] and pn[1]):
            continue  # skip fixtures with no Pinnacle benchmark

        # Pinnacle de-vig fair probability
        s = 1/pn[0] + 1/pn[1]
        fair_home, fair_away = (1/pn[0])/s, (1/pn[1])/s

        match = f"{f['participant1Name']} vs {f['participant2Name']}"
        print(f"\n{f['tournamentName']:30s} | {match}")
        print(f"  Pinnacle fair: {1/fair_home:.3f} / {1/fair_away:.3f}  ({len(books)} books)")

        # Find +EV vs Pinnacle de-vig (skip crypto books — see note below)
        SKIP = {"bcgame", "blaze", "rollbit", "roobet", "duel", "vertex", "stake"}
        for slug, (h, a, act, _) in prices.items():
            if slug in SKIP or slug == "pinnacle" or not act:
                continue
            ev_h = (h * fair_home - 1) * 100
            ev_a = (a * fair_away - 1) * 100
            if ev_h > 1 or ev_a > 1:
                print(f"    +EV {slug:20s}  home {ev_h:+5.2f}%  away {ev_a:+5.2f}%")

scan_valorant_slate(API_KEY, days=7)

One gotcha worth flagging: a few crypto books (BCGame, Blaze, Rollbit) occasionally ship Valorant outcome IDs in reversed order — they’ll list the away team’s price under outcome 611. We filter them out of the +EV scanner above as a safety measure. The fix is to cross-reference each book’s bookmakerOutcomeId field (which says "home" or "away") when in doubt, or simply restrict your scanner to regulated books for production use. Pinnacle, Polymarket, DraftKings, BetRivers, BetMGM, Unibet, and the Kindred network all parse correctly.

Step 7: Free Historical Odds for Backtesting

This is the kill shot competitors charge $200+/month for. Pull the full price history of any Valorant fixture’s moneyline from /v4/historical-odds:

r = requests.get(f"{BASE_URL}/historical-odds", params={
    "apiKey": API_KEY,
    "fixtureId": "pn619022571630262973",
    "bookmakers": "pinnacle,polymarket,draftkings",  # max 3 per call
})
data = r.json()

# NOTE: historical endpoint key is `bookmakers`, not `bookmakerOdds`,
# and players["0"] is a LIST of price snapshots, not a single dict.
for slug, bk in data["bookmakers"].items():
    snapshots = bk["markets"]["611"]["outcomes"]["611"]["players"]["0"]
    print(f"\n{slug}: {len(snapshots)} price changes for Heretics")
    for snap in snapshots[-5:]:
        print(f"  {snap['createdAt']}  price={snap['price']}")

Loop the call with different bookmaker groupings and merge the responses if you need more than three books at a time. We’ve covered the full historical-odds export pattern (CSV / Excel) in our Historical Odds Export guide if you want to feed this into pandas for backtesting.

What to Build With This

  • VCT line-movement tracker — poll /odds every 60s during a match and log every price change. Hat tip to our Steam Move Detector if you want a sharp-money alert bot pattern.
  • Polymarket vs sportsbook arb scanner — Polymarket’s 1% vig means it’s frequently the best price on at least one side. When sportsbooks lag the Polymarket line, there’s a real arb window.
  • Backtest a Valorant rating model — pull historical odds for the last 100 VCT matches, compare your model’s predicted probabilities to the Pinnacle closing line, calculate CLV. Free to do; competitors charge $200/mo for this.
  • Real-time dashboard — build a Streamlit page that scans the live VCT slate every minute and surfaces best price + line movement. Same pattern as our Streamlit Odds Dashboard.

FAQ

Does OddsPapi cover the full Valorant Champions Tour?

Yes — both the international leagues (EMEA, Americas, Pacific) and the Challengers tier are in the fixture feed. International league matches typically have 25–29 books pricing the moneyline; Challengers and Game Changers events get 0–5 books and shouldn’t be relied on for line shopping.

Why is Polymarket showing different odds than Pinnacle?

Polymarket is a peer-to-peer prediction market, not a sportsbook. Their pricing represents the market’s implied probability after a near-zero margin, while Pinnacle bakes in roughly 5% vig as a sharp sportsbook. Both are “sharp” signals, but they’re not the same number. OddsPapi ships Polymarket prices already converted to decimal odds — the polymarket slug behaves like any other bookmaker in the response.

Can I get map-by-map handicaps and totals?

Not currently. Valorant on OddsPapi is moneyline-only — Match Winner (market ID 611) is the only market shipped across the full bookmaker network. If your use case strictly needs map handicaps or round totals, you’ll need to scrape directly from Pinnacle / DraftKings, or add a paid esports-specialist provider on top.

What’s the difference between this and the OddsPapi esports guide?

The Esports Odds API guide covers CS2, LoL, and Dota 2 — the older esports cluster. Valorant has its own dedicated sportId (61) with VCT-specific tournament feeds, which is why it gets a separate post. The code pattern is the same; the sport ID and tournament names are what change.

Is the free tier enough for a Valorant scanner?

For most use cases, yes. The free tier limits you to roughly one call per second per endpoint and 250 free requests/day. A VCT slate scanner that polls every 60 seconds across 5 live matches uses well under that. WebSocket push (paid tier) is only worth it if you need sub-second latency — see our WebSocket Odds API post for the latency tradeoffs.

Stop Scraping. Get Your Free API Key.

One requests.get() call. 28+ bookmakers. Pinnacle, Polymarket, DraftKings, BetMGM, BetRivers, Unibet, seven crypto books — all on the free tier. Free historical odds for backtesting. No scraping, no IP-bans, no enterprise sales call.

Get your free OddsPapi API key →