Valorant Odds API: Real-Time VCT Odds from Pinnacle, Polymarket & 28+ Bookmakers
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:
- 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
polymarketslug works under the hood — including theexchangeMeta.layside.) - 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
/oddsevery 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.