Badminton Odds API: Line-Shop 148 Books on BWF Singles

Badminton Odds API
How To Guides November 2, 2026

A badminton odds API should give you Winner prices on every BWF World Tour match, from as many books as quote it, as JSON. On 28 October 2026 at 08:19 UTC, the men’s and women’s singles boards at the Hylo Open in Saarbrücken held a median 141.5 books each, and the deepest held 148. The doubles boards at the same event held a median 52.

That gap decides how you line-shop. A deep singles board gives you about 110 usable prices, which collapse to about 20 distinct price pairs. This guide shows you which events and disciplines carry the deep boards, which book quoted the tightest price, how to read the second market ID that one bookmaker family uses for the result, and how to settle a played match from the game scores.

Why a scraper loses the badminton board

The BWF World Tour moves every week. The Denmark Open, the French Open and the Hylo Open ran on three consecutive weeks in October, and each event ships five draws: men’s and women’s singles, men’s and women’s doubles, and mixed doubles. A scraper has to find each new event, map five draws per event, and keep up with a schedule that changes daily. On the morning of 28 October, all 74 day-two fixtures for the 29th carried the same 08:00 UTC start time, and one day-one men’s singles match moved from 09:15 to 09:25 between two of our reads.

The books also disagree on the market. Up to 140 books on a Hylo Open singles board ship the result as market 491, Winner. Eight feeds of the 1xBet family ship the same bet as market 493, labelled 1X2, with two legs and no draw. A parser that reads only 491 drops that family without an error.

OddsPapi lists every scheduled match as a fixture and attaches every book that quotes it: 300+ bookmakers in the catalogue, 142 of them on the badminton boards in our census, under one schema. You poll one fixture ID and read the whole board. Price history for any played match comes from /v4/historical-odds, free on every plan, and the game scores come from /v4/scores.

Scrape the sportsbooks OddsPapi
Events and draws Find each event and each draw by hand One tournamentId per draw from /v4/fixtures, sportId 49
Books per match The sites you scrape Up to 148 on one call, deduped to about 20 price pairs in Step 2
Result market One parser per site Two IDs: 491 Winner, and 493 on the 1xBet family
Closing prices Your own archive Last active price before the start, any played match
Results A second data source Points per game and games won on /v4/scores

Which badminton matches draw the deep boards

We read all 118 priced, unstarted badminton boards on 28 October 2026, from 08:19 to 08:25 UTC. Two events were live: the Hylo Open and the Hungarian International. The table groups the boards by event and discipline.

Event Discipline Boards Median books Range
Hylo Open Singles (MS, WS) 30 141.5 64 to 148
Hylo Open Mixed doubles 15 85 59 to 127
Hylo Open Doubles (MD, WD) 17 52 15 to 65
Hungarian International Singles (MS, WS) 24 64.5 25 to 84
Hungarian International Doubles (MD, WD) 21 43 12 to 74
Hungarian International Mixed doubles 11 23 12 to 45

Singles at the World Tour event carry the market. All 36 boards with 100 or more books were Hylo Open singles or mixed doubles matches. The Hungarian International singles boards held about half the Hylo Open count, and its doubles boards fell as low as 12 books. If you build a badminton model, train and bet it on World Tour singles first.

Which book to anchor on: Gamdom

On the 36 deep boards, the median board carried 111 usable Winner quotes, and those quotes collapsed to a median 19.5 distinct price pairs. Skins of one platform repeat the same pair: the eight Betano feeds share one pair, and so do the eight 1xBet feeds. Dedupe before you count books or average prices.

Gamdom quoted the tightest pair on 35 of the 36 deep boards. Its median margin was 3.98%, against a median 8.54% for the whole board. The next tightest group, the Betano feeds, sat at 6.98%. Across the 46 boards Gamdom quoted, its no-vig probability sat a median 1.58 points from the board consensus, so the tight price tracks the market. Pinnacle and the exchanges did not quote any of the 118 boards, so Gamdom and a deduped consensus are your two reference prices on badminton.

The closing prices show the same order. We took each book’s last active result price before the scheduled start on 58 played singles matches from the Denmark Open (15 to 18 October) and the French Open (22 to 25 October), from the round of 16 to the finals:

Book Matches with a close Median closing margin Brier score
Gamdom (gamdom) 56 4.05% 0.170
Cloudbet (cloudbet) 58 6.98% 0.169
Betano (betano) 55 7.12% 0.176
1xBet (1xbet, market 493) 57 7.56% 0.168
Betway (betway) 54 7.79% 0.172
bet365 (bet365) 40 8.52% 0.170

Gamdom closed tightest of the six on 52 of the 58 graded matches. A tighter price did not make it a sharper forecaster: all six books scored between 0.168 and 0.176 on the Brier score of their no-vig closing probability, and the closing favourite won 44 of Gamdom’s 56 matches. Use Gamdom for the cheapest price and the deduped consensus for the probability. The sample is 58 matches from two events, so measure your own event before you size a bet.

Badminton markets in the API

Badminton is sportId 49. /v4/markets?sportId=49 returned two market definitions on 28 October, both settled on the whole match (period: "result"):

  • Winner: market 491, outcomes 491 (participant 1) and 492 (participant 2). It follows the sportId * 10 + 1 pattern from the moneyline guide across 59 sports.
  • 1X2: market 493, outcomes 493 (1), 494 (X) and 495 (2). On the boards in our census, only the eight 1xBet-family feeds quoted it: 1xbet, 1xbit, 22bet, betwinner, fairpari, megapari, melbet and spelet.lv. Every one of their 344 quotes carried the two legs 493 and 495 and no X leg, and none of those feeds quoted 491 on the same board.

Treat 493 as the 1xBet family’s Winner: map outcome 493 to participant 1 and 495 to participant 2. The 1xBet API guide covers how those eight feeds relate on soccer. The history endpoint stores the same split, so Step 3 reads both IDs too.

Step 1: Find the events and count the books per draw

Get a free key from the sign-up page and export it as ODDSPAPI_API_KEY. The key goes in the apiKey query parameter, not a header. This script lists the priced badminton draws for the next two days and counts the books on three boards per draw.

import os, time, requests
from collections import Counter

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"

def get(path, **params):
    params["apiKey"] = API_KEY
    for _ in range(5):
        r = requests.get(f"{BASE}/{path}", params=params, timeout=120)
        if r.status_code == 429:
            time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
            continue
        if r.status_code >= 500:          # transient gateway error: wait and retry
            time.sleep(3)
            continue
        if r.status_code == 404:          # empty window, not an error
            return []
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"{path} kept returning 429")

# Badminton is sportId 49. Each BWF event ships one tournament per discipline.
fixtures = get("fixtures", sportId=49, **{"from": "2026-10-28", "to": "2026-10-30"})
priced = [f for f in fixtures if f["hasOdds"] and f["statusName"] == "Pre-Game"]
print(f"{len(fixtures)} fixtures, {len(priced)} priced and not started\n")

# Sample up to three fixtures per tournament and count the books on each board.
by_tournament = {}
for f in priced:
    by_tournament.setdefault((f["tournamentId"], f["tournamentName"]), []).append(f)

for (tid, name), group in sorted(by_tournament.items(), key=lambda kv: kv[0][1]):
    counts = []
    for f in group[:3]:
        board = get("odds", fixtureId=f["fixtureId"]) or {}   # 404 = no board right now
        counts.append(len(board.get("bookmakerOdds", {})))
        time.sleep(1.0)
    print(f"{tid:>6}  {name:<32} {len(group):>3} fixtures  books on 3 boards: {counts}")

Output, 28 October 2026, 08:26 UTC:

192 fixtures, 118 priced and not started

 27032  Hungarian International, MD        8 fixtures  books on 3 boards: [48, 74, 43]
 27028  Hungarian International, MS       14 fixtures  books on 3 boards: [69, 65, 35]
 27034  Hungarian International, WD       13 fixtures  books on 3 boards: [50, 18, 24]
 27030  Hungarian International, WS       10 fixtures  books on 3 boards: [69, 73, 76]
 27036  Hungarian International, XD       11 fixtures  books on 3 boards: [27, 18, 49]
 27022  Hylo Open, MD                      8 fixtures  books on 3 boards: [43, 31, 52]
 27018  Hylo Open, MS                     16 fixtures  books on 3 boards: [142, 148, 148]
 27024  Hylo Open, WD                      9 fixtures  books on 3 boards: [65, 53, 27]
 27020  Hylo Open, WS                     14 fixtures  books on 3 boards: [147, 146, 145]
 27026  Hylo Open, XD                     15 fixtures  books on 3 boards: [59, 73, 60]

Keep the tournament IDs. Each draw has its own, and the next steps filter /v4/fixtures with tournamentId, which cuts the payload and lets you query more than ten days at a time.

Step 2: Rank the Winner prices on a singles board

This script takes the next Hylo Open men’s singles match, merges markets 491 and 493, applies the guards, dedupes on the price pair and ranks the pairs by margin. Three guards matter on badminton:

  • Drop a book with suspended: true or bookmakerIsActive: false, and any price with active: false.
  • Drop a book whose newest changedAt on the fixture is more than 36 hours old. Badminton books post a line and leave it: on a Hylo Open singles board an hour before the start, 78 of 148 books had not moved a price in more than 12 hours. A 60-minute cut that suits a soccer board would leave you 19 of them.
  • Reject a pair whose inverse prices sum to 1.0 or less. One book never offers an arb against itself.
import os, time, statistics, requests
from collections import defaultdict
from datetime import datetime, timezone

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
HYLO_OPEN_MS = 27018

def get(path, **params):
    params["apiKey"] = API_KEY
    for _ in range(5):
        r = requests.get(f"{BASE}/{path}", params=params, timeout=120)
        if r.status_code == 429:
            time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
            continue
        if r.status_code >= 500:
            time.sleep(3)
            continue
        if r.status_code == 404:
            return {}
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"{path} kept returning an error")

def ts(s):
    return datetime.fromisoformat(s.replace("Z", "+00:00"))

# The result ships under two IDs. 491 Winner: outcomes 491 / 492.
# 493 "1X2": the 1xBet family quotes it with legs 493 / 495 and no X leg.
RESULT = {"491": ("491", "492"), "493": ("493", "495")}

now = datetime.now(timezone.utc)
fixtures = get("fixtures", tournamentId=HYLO_OPEN_MS, **{"from": "2026-10-28", "to": "2026-10-30"})
ahead = sorted((f for f in fixtures if f["statusName"] == "Pre-Game" and ts(f["startTime"]) > now),
               key=lambda f: f["startTime"])
f = ahead[0]
board = get("odds", fixtureId=f["fixtureId"])

quotes, stale = {}, 0
for slug, book in board["bookmakerOdds"].items():
    if "+" in slug or slug in ("demo", "singbet-b"):           # internal test feeds
        continue
    if book.get("suspended") or book.get("bookmakerIsActive") is False:
        continue
    markets = book.get("markets") or {}
    stamps = [ts(p["changedAt"]) for m in markets.values() for o in m["outcomes"].values()
              for p in o["players"].values() if p.get("changedAt")]
    # Badminton lines sit for hours. Cut at 36 h, not the 60 minutes you use on soccer.
    if not stamps or (now - max(stamps)).total_seconds() > 36 * 3600:
        stale += 1
        continue
    for mid, (o1, o2) in RESULT.items():
        legs = (markets.get(mid) or {}).get("outcomes", {})
        if o1 in legs and o2 in legs:
            p1, p2 = legs[o1]["players"]["0"], legs[o2]["players"]["0"]
            if p1.get("active") is not False and p2.get("active") is not False \
                    and 1 / p1["price"] + 1 / p2["price"] > 1.0:    # one book never arbs itself
                quotes[slug] = (p1["price"], p2["price"], mid)
            break

groups = defaultdict(list)                                       # dedupe on the price pair
for slug, (a, b, mid) in quotes.items():
    groups[(a, b)].append(slug)

def margin(q):
    return 100 * (1 / q[0] + 1 / q[1] - 1)

fair_p1 = statistics.median((1 / a) / (1 / a + 1 / b) for a, b in groups)
print(f"{f['participant1Name']} v {f['participant2Name']}, start {f['startTime'][:16]}Z")
print(f"{len(board['bookmakerOdds'])} books on the board, {len(quotes)} usable result quotes "
      f"({sum(q[2] == '493' for q in quotes.values())} of them on market 493), "
      f"{len(groups)} distinct price pairs, {stale} books cut as stale")
print(f"median margin {statistics.median(margin(q) for q in quotes.values()):.2f}%, "
      f"deduped consensus P1 {fair_p1:.1%}")
print("tightest pairs:")
for q in sorted(groups, key=margin)[:5]:
    print(f"  {q[0]:>5} / {q[1]:<5} {margin(q):5.2f}%  {len(groups[q]):>2} books  {', '.join(sorted(groups[q])[:3])}")
for slug in ("gamdom", "1xbet", "bet365"):
    if slug in quotes:
        a, b, mid = quotes[slug]
        print(f"  {slug:<7} {a} / {b}  {margin((a, b)):.2f}%  market {mid}")

Output, 28 October 2026, 08:27 UTC:

Lee, Zii Jia v Teh, Jia Heng Jason, start 2026-10-28T09:25Z
148 books on the board, 121 usable result quotes (8 of them on market 493), 22 distinct price pairs, 1 books cut as stale
median margin 8.14%, deduped consensus P1 59.8%
tightest pairs:
   1.59 / 2.44   3.88%   1 books  gamdom
   1.55 / 2.35   7.07%   8 books  betano, betano.bg, betano.ca
   1.55 / 2.327  7.49%   8 books  1xbet, 1xbit, 22bet
   1.55 / 2.32   7.62%   1 books  winbet.ro
   1.58 / 2.25   7.74%   1 books  maxbet.rs
  gamdom  1.59 / 2.44  3.88%  market 491
  1xbet   1.55 / 2.327  7.49%  market 493
  bet365  1.54 / 2.25  9.38%  market 491

The 1xBet family’s pair at 7.49% is the third-tightest group on the board, and it exists only on market 493. Gamdom sits alone at the top. If you need a fair price, take the deduped consensus or de-vig the Gamdom pair; our consensus odds guide compares the methods.

Step 3: Settle a played match from the history and the scores

/v4/scores returns one key per game, p1 to p3, each with the points of both players, plus result with the games won. Winner settles on result. This script reads the closing result price for three books from /v4/historical-odds on the French Open men’s final and grades the favourite. Cut the close at the scheduled startTime: the recorded trueStartTime on this final came 12 minutes later, after the first serve.

import os, time, requests
from datetime import datetime

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
FIXTURE = "id4902064275994734"           # French Open MS final, 25 Oct 2026
FRENCH_OPEN_MS = 20642
BOOKS = "gamdom,1xbet,bet365"            # /historical-odds takes at most 3

def get(path, **params):
    params["apiKey"] = API_KEY
    for _ in range(6):
        r = requests.get(f"{BASE}/{path}", params=params, timeout=180)
        if r.status_code == 429:
            time.sleep(r.json().get("error", {}).get("retryMs", 5000) / 1000 + 1)
            continue
        if r.status_code >= 500:
            time.sleep(4)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"{path} kept returning an error")

def ts(s):
    return datetime.fromisoformat(s.replace("Z", "+00:00"))

# Same two result IDs as Step 3: 491 Winner, and 493 on the 1xBet family.
RESULT = {"491": ("491", "492"), "493": ("493", "495")}

fx = get("fixtures", tournamentId=FRENCH_OPEN_MS, **{"from": "2026-10-25", "to": "2026-10-26"})
f = next(f for f in fx if f["fixtureId"] == FIXTURE)
start = ts(f["startTime"])               # scheduled start; trueStartTime is already in play
print(f"{f['participant1Name']} v {f['participant2Name']}, start {f['startTime'][:16]}Z")

def close(snaps):
    """Last active price recorded before the scheduled start."""
    pre = [s for s in snaps if ts(s["createdAt"]) < start and s.get("active") is not False]
    return max(pre, key=lambda s: s["createdAt"])["price"] if pre else None

periods = get("scores", fixtureId=FIXTURE)["scores"]["periods"]
games = [f"{periods[k]['participant1Score']}-{periods[k]['participant2Score']}"
         for k in ("p1", "p2", "p3") if k in periods]
res = periods["result"]
p1_won = res["participant1Score"] > res["participant2Score"]
print(f"games {', '.join(games)}; result {res['participant1Score']}-{res['participant2Score']}\n")

time.sleep(1.0)
hist = get("historical-odds", fixtureId=FIXTURE, bookmakers=BOOKS)
for slug, book in hist["bookmakers"].items():
    for mid, (o1, o2) in RESULT.items():
        legs = book.get("markets", {}).get(mid, {}).get("outcomes", {})
        if o1 in legs and o2 in legs:
            a = close(legs[o1]["players"]["0"])
            b = close(legs[o2]["players"]["0"])
            margin = 100 * (1 / a + 1 / b - 1)
            fav_p1 = a < b
            print(f"{slug:<7} market {mid}  close {a} / {b}  {margin:.2f}%  "
                  f"favourite {'P1' if fav_p1 else 'P2'} {'WON' if fav_p1 == p1_won else 'LOST'}")
            break

Output:

Ubaidillah, Moh. Zaki v Popov, Christo, start 2026-10-25T13:35Z
games 11-21, 21-14, 13-21; result 1-2

1xbet   market 493  close 3.02 / 1.346  7.41%  favourite P2 WON
bet365  market 491  close 3.0 / 1.33  8.52%  favourite P2 WON
gamdom  market 491  close 3.2 / 1.37  4.24%  favourite P2 WON

1xBet’s history sits under market 493, the same as its live board. Gamdom closed the final at 4.24%, bet365 at 8.52%. For the full history workflow, see the historical odds API guide. The table tennis odds API guide runs the same scores pattern on best-of-five matches.

Polling works on a pre-match board that moves every few hours. Once a match is live, the WebSocket feed pushes each new price as the book posts it. If you came here for lawn tennis, the tennis moneyline guide covers the ATP, WTA and ITF boards.

Badminton odds API FAQ

Is there a free badminton odds API?

Yes. OddsPapi’s free plan returns Winner prices on badminton fixtures (sportId 49) from 300+ bookmakers, plus the price history and the game scores of played matches. BWF World Tour events and smaller international events such as the Hungarian International are in the feed, with one tournament ID per draw.

How many bookmakers quote badminton?

On 28 October 2026, 142 distinct books quoted the badminton boards in our census. Hylo Open singles matches held a median 141.5 books each, mixed doubles 85 and doubles 52.

Which bookmaker has the tightest badminton odds?

Gamdom. It quoted the tightest Winner pair on 35 of 36 deep Hylo Open boards, at a 3.98% median margin against 8.54% for the whole board, and it closed tightest of six books on 52 of 58 played singles matches at the Denmark Open and French Open.

Which market ID is the badminton Winner?

Market 491, with outcomes 491 and 492. The eight 1xBet-family feeds quote the same bet as market 493 (1X2) with legs 493 and 495, so read both IDs and map them to one Winner.

How do I settle badminton bets from the API?

Call /v4/scores. The p1 to p3 keys carry the points of each game and result carries the games won. Winner settles on result. Take the closing price from /v4/historical-odds as the last active snapshot before the scheduled startTime.

Read every BWF draw from one key

Every call above runs on the free key: 300+ bookmakers under one schema, free historical odds on every played match, and a WebSocket feed for live play. Get a free API key and run Step 1 on this week’s World Tour draw.