Superbet API: Route 5 Tournaments to Their Tightest Home Feed

Superbet API featured image
How To Guides October 12, 2026

Looking for a Superbet API? You want Superbet’s prices as JSON, per fixture, without scraping superbet.ro, superbet.pl and superbet.rs one by one. OddsPapi returns them under six slugs: superbet, superbet.ro, superbet.pl, superbet.rs, superbet.bet.br and napoleonsports.be. All six carry liveOdds: true, and one API key reads all six.

The six feeds share one Superbet event ID, but they do not share one price. We pulled 27 boards on 30 September 2026 and 15 played fixtures from 20-21 September. In the Romanian Superliga, the Ekstraklasa, the Serbian Superliga, the Brasileirao and the Belgian Pro League, the feed from the tournament’s own country closed the 1X2 tightest on 15 of 15 fixtures, between 0.99 and 3.48 points under its closest sibling. This guide shows how to pull the six feeds, route each tournament to its home feed, and check the closing line yourself.

Why one Superbet scraper gets you one country’s price

Superbet runs one platform with a site per country. The payload shows it: every feed’s fixturePath ends in the same /offer-event/e-<id> path, and bookmakerFixtureId carries the same number on all six. Napoleon Sports in Belgium uses the same path and the same event ID, so it runs on the Superbet platform too. If you want to group a whole board by platform this way, our Unibet API guide does it for the Kambi family.

The event ID tells you the six feeds price the same fixture. It does not tell you what each one charges. A scraper on superbet.com gets you the Romanian price. On the Cluj derby that price is 8.11%. The Polish site charges 11.85% on the same fixture, and the Serbian site 11.06%. Point the scraper at Serbia instead and the numbers invert on Serbian football. So you need every country feed, and you need to know which one to trust for which tournament.

Scraping Superbet sites OddsPapi
Superbet feeds One scraper per country site 6 slugs, one key
Which feed prices a league tightest Unknown until you scrape all of them Compare the six on one call, route per tournament
Closing lines Only what you stored yourself /v4/historical-odds, free tier
Parked boards Look like live prices Book-level suspended flag on every row
Rest of the board None 350+ bookmakers in the catalogue, 169 to 271 on the Nations League and Brasileirao boards we pulled
Push updates Poll and diff the page WebSocket feed on the same fixture IDs

Step 1: find the Superbet slugs

Authentication is one query parameter, apiKey. The helper below waits out a 429 for as long as the error body says, backs off on a 5xx, and returns None on a 404. The first call lists the catalogue and keeps the Superbet platform feeds.

import os, time, requests
from datetime import datetime, timedelta, timezone
from statistics import median

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=180)
        if r.status_code == 429:                      # rate limited: wait as told
            time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
            continue
        if r.status_code >= 500:                      # transient: back off and retry
            time.sleep(2)
            continue
        if r.status_code == 404:                      # empty window
            return None
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"gave up on {path}")

books = get("bookmakers")
superbet = [b for b in books if b["slug"].startswith(("superbet", "napoleonsports"))]
for b in superbet:
    print(f"{b['slug']:<18} {b['bookmakerName']:<20} liveOdds={b['liveOdds']}  cloneOf={b['cloneOf']}")
print(f"\n{len(books)} bookmakers in the catalogue")
napoleonsports.be  Napoleon Sports BE   liveOdds=True  cloneOf=None
superbet           Superbet             liveOdds=True  cloneOf=superbet.ro
superbet.bet.br    Superbet BR          liveOdds=True  cloneOf=None
superbet.pl        Superbet PL          liveOdds=True  cloneOf=None
superbet.ro        Superbet RO          liveOdds=True  cloneOf=None
superbet.rs        Superbet RS          liveOdds=True  cloneOf=None

361 bookmakers in the catalogue

The catalogue flags one relationship: superbet is a clone of superbet.ro. That flag is correct. Across the 25 boards where both appeared, the two feeds shared 12,720 prices and matched on all 12,720. Use superbet.ro and skip superbet. The other five carry cloneOf: None, and the catalogue says nothing about how their prices relate. The board does.

Step 2: read the six feeds on one fixture

This step finds the next Romanian Superliga fixture that has not kicked off and pulls it with bookmakers= set to the six slugs. The filter keeps the payload small. Without it, a full board on a Nations League fixture came back at up to 67 MB.

# Reuses get() and the imports from Step 1.
FEEDS = "superbet,superbet.ro,superbet.pl,superbet.rs,superbet.bet.br,napoleonsports.be"

def margin_1x2(book):
    """Overround on market 101 (1X2), or None if any leg is missing or suspended."""
    legs = book.get("markets", {}).get("101", {}).get("outcomes", {})
    prices = [p["price"] for o in legs.values() for p in o["players"].values()
              if p.get("active") and p.get("price", 0) > 1.0]
    return sum(1 / p for p in prices) - 1 if len(prices) == 3 else None

now = datetime.now(timezone.utc)
fixtures = get("fixtures", tournamentId=152,                 # Romanian Superliga
               **{"from": now.strftime("%Y-%m-%d"), "to": (now + timedelta(days=10)).strftime("%Y-%m-%d")})
fx = next(f for f in sorted(fixtures, key=lambda f: f["startTime"])
          if f["statusName"] == "Pre-Game" and f["startTime"] > now.isoformat())
print(f"{fx['participant1Name']} v {fx['participant2Name']}  {fx['startTime'][:16]}Z\n")

board = get("odds", fixtureId=fx["fixtureId"], bookmakers=FEEDS)["bookmakerOdds"]
for slug in FEEDS.split(","):
    b = board.get(slug)
    if not b:
        print(f"{slug:<18} not on this board")
        continue
    m = margin_1x2(b) if not b.get("suspended") else None
    print(f"{slug:<18} event {b['bookmakerFixtureId']}  {len(b['markets']):>3} markets  "
          f"suspended={b.get('suspended')!s:<5}  1X2 margin {f'{m:.2%}' if m is not None else '-'}")
FC CFR 1907 Cluj v FC Universitatea Cluj  2026-10-08T17:30Z

superbet           event 14364939  120 markets  suspended=False  1X2 margin 8.11%
superbet.ro        event 14364939  120 markets  suspended=False  1X2 margin 8.11%
superbet.pl        event 14364939  112 markets  suspended=False  1X2 margin 11.85%
superbet.rs        event 14364939  124 markets  suspended=False  1X2 margin 11.06%
superbet.bet.br    event 14364939  135 markets  suspended=True   1X2 margin -
napoleonsports.be  event 14364939   63 markets  suspended=False  1X2 margin 11.06%

Three things to read in that output.

  • One event ID, three prices. All six rows carry 14364939. The Romanian feed charges 8.11% on a Romanian fixture. The Serbian feed and Napoleon Sports charge 11.06%, and the Polish feed 11.85%.
  • superbet.bet.br parks European domestic boards. It shipped suspended: true with zero active prices on all 16 Romanian, Polish, Serbian and Belgian domestic boards we pulled, 8 to 11 days before kick-off. Filter on the book-level suspended flag before you average anything. The price-level active flag alone is not enough on every book; our Bwin API guide shows a feed that parks its board and keeps every price marked active.
  • Menu width follows the country. On this fixture superbet.bet.br lists 135 markets and Napoleon Sports 63. On the Poland v Romania Nations League board, superbet.rs listed 278 markets against 203 on superbet.ro. Pick the feed for the price you need, then check that it lists the market.

Step 3: route each tournament to its home feed

This step runs Step 2 across five tournaments. For each one it takes the next three fixtures, reads the 1X2 margin from every Superbet feed, and prints the median. The star marks the feed from the tournament’s own country. It reuses FEEDS and margin_1x2() from Step 2.

# Reuses FEEDS and margin_1x2() from Step 2.
LEAGUES = {                      # tournamentId: (name, home feed)
    152: ("Romania Superliga", "superbet.ro"),
    202: ("Poland Ekstraklasa", "superbet.pl"),
    210: ("Serbia Superliga",  "superbet.rs"),
    325: ("Brasileirao A",     "superbet.bet.br"),
    38:  ("Belgium Pro League", "napoleonsports.be"),
}
COLS = ["superbet.ro", "superbet.pl", "superbet.rs", "superbet.bet.br", "napoleonsports.be"]

now = datetime.now(timezone.utc)
window = {"from": now.strftime("%Y-%m-%d"), "to": (now + timedelta(days=14)).strftime("%Y-%m-%d")}
print(f"{'league':<20}" + "".join(f"{c:>18}" for c in COLS))
for tid, (name, home) in LEAGUES.items():
    upcoming = [f for f in sorted(get("fixtures", tournamentId=tid, **window) or [], key=lambda f: f["startTime"])
                if f["statusName"] == "Pre-Game" and f["startTime"] > now.isoformat()][:3]
    margins = {c: [] for c in COLS}
    for fx in upcoming:
        board = get("odds", fixtureId=fx["fixtureId"], bookmakers=FEEDS)["bookmakerOdds"]
        for c in COLS:
            b = board.get(c)
            m = margin_1x2(b) if b and not b.get("suspended") else None
            if m is not None:
                margins[c].append(m)
        time.sleep(1.0)
    cells = [(f"{median(v):.2%}" if v else "parked") + ("*" if c == home else " ") for c, v in margins.items()]
    print(f"{name:<20}" + "".join(f"{x:>18}" for x in cells) + f"   ({len(upcoming)} fixtures)")
print("\n* = home feed; median 1X2 margin; 'parked' = board shipped suspended")
league                     superbet.ro       superbet.pl       superbet.rs   superbet.bet.br napoleonsports.be
Romania Superliga               8.11%*           11.85%            11.04%            parked            11.04%    (3 fixtures)
Poland Ekstraklasa             11.04%             8.75%*           11.04%            parked            11.04%    (3 fixtures)
Serbia Superliga               11.26%            12.53%             8.64%*           parked            11.26%    (3 fixtures)
Brasileirao A                   5.57%             9.38%             5.57%             7.10%*            5.57%    (3 fixtures)
Belgium Pro League              9.07%             9.88%             9.07%            parked             8.07%*   (3 fixtures)

* = home feed; median 1X2 margin; 'parked' = board shipped suspended

On the four European leagues, the home feed sits 1.0 to 2.9 points under its closest sibling. The Romanian feed takes 8.11% on Romanian football and 11.04% on Polish football. The Polish feed flips that: 8.75% at home, 11.85% on Romanian fixtures. Napoleon Sports is the Belgian case, at 8.07% against 9.07% for the Romanian and Serbian feeds.

Brazil breaks the pattern on this pull. With the next three Brasileirao fixtures still days away, superbet.bet.br charged 7.10% and superbet.ro charged 5.57%. Step 4 shows the order flips by kick-off.

Two more cuts from the same session, outside the table:

  • Neutral fixtures collapse to one price. On five Nations League boards, superbet.ro and superbet.rs quoted the same 1X2 at a median 5.52%. Napoleon Sports ran 5.02%, superbet.pl 6.46% and superbet.bet.br 7.72%. Poland v Romania shows the routing at work: superbet.ro charged 5.36% against 6.38% on superbet.pl, and Pinnacle charged 5.63% on the same board.
  • Tennis follows a different map. On two ATP Tokyo fixtures, superbet.rs and Napoleon Sports ran 5.55% against 6.67% on superbet.ro. The home-feed rule is a domestic-football finding. Measure it again before you apply it to another sport.

Step 4: check the closing line on played fixtures

A live board 8 to 12 days out is not the price you bet into. /v4/historical-odds returns every snapshot a book posted on a played fixture, free, up to three bookmakers a call. This step takes three played fixtures per tournament from 20-21 September 2026, reads the last active 1X2 snapshot before kick-off from the home feed and two siblings, and compares.

# Reuses get() from Step 1.
PLAYED = {   # home feed: played fixtures, 20-21 Sep 2026
    "superbet.ro":       ["id1000015272388004", "id1000015272388006", "id1000015272388002"],
    "superbet.pl":       ["id1000020272043176", "id1000020272043174", "id1000020272043178"],
    "superbet.rs":       ["id1000021072083054", "id1000021072083058", "id1000021072083066"],
    "superbet.bet.br":   ["id1000032566887034", "id1000032566887018", "id1000032566887032"],
    "napoleonsports.be": ["id1000003872203068", "id1000003872203070", "id1000003872203072"],
}
KICKOFF = {"id1000015272388004": "2026-09-20T17:30", "id1000015272388006": "2026-09-21T15:00",
           "id1000015272388002": "2026-09-21T18:00", "id1000020272043176": "2026-09-20T12:45",
           "id1000020272043174": "2026-09-20T15:30", "id1000020272043178": "2026-09-20T18:15",
           "id1000021072083054": "2026-09-20T15:00", "id1000021072083058": "2026-09-20T17:00",
           "id1000021072083066": "2026-09-20T18:00", "id1000032566887034": "2026-09-20T19:00",
           "id1000032566887018": "2026-09-20T21:30", "id1000032566887032": "2026-09-20T22:30",
           "id1000003872203068": "2026-09-20T14:00", "id1000003872203070": "2026-09-20T16:30",
           "id1000003872203072": "2026-09-20T17:15"}

def closing_margin(book, kickoff):
    """1X2 overround on the last active pre-kick-off snapshot of each leg."""
    legs = book["markets"]["101"]["outcomes"]            # 101 home, 102 draw, 103 away
    close = []
    for oid in ("101", "102", "103"):
        snaps = [s for s in legs[oid]["players"]["0"]
                 if s["active"] and s["price"] > 1.0 and s["createdAt"] < kickoff]
        close.append(snaps[-1]["price"])
    return sum(1 / p for p in close) - 1

wins = 0
for home, fixture_ids in PLAYED.items():
    rivals = [s for s in ("superbet.ro", "superbet.pl", "superbet.rs") if s != home][:2]
    for fid in fixture_ids:
        hist = get("historical-odds", fixtureId=fid, bookmakers=",".join([home] + rivals))["bookmakers"]
        closes = {slug: closing_margin(book, KICKOFF[fid]) for slug, book in hist.items()}
        best_rival = min(closes[r] for r in rivals)
        wins += closes[home] < best_rival
        print(f"{fid}  {home:<18} {closes[home]:6.2%}   best rival {best_rival:6.2%}   "
              f"gap {100 * (best_rival - closes[home]):4.2f} pts")
        time.sleep(4.6)                                      # /historical-odds is rate limited
print(f"\nhome feed closed tightest on {wins} of {sum(map(len, PLAYED.values()))} fixtures")
id1000015272388004  superbet.ro         4.52%   best rival  7.23%   gap 2.71 pts
id1000015272388006  superbet.ro         4.42%   best rival  6.64%   gap 2.22 pts
id1000015272388002  superbet.ro         4.33%   best rival  7.28%   gap 2.95 pts
id1000020272043176  superbet.pl         4.94%   best rival  7.05%   gap 2.11 pts
id1000020272043174  superbet.pl         5.24%   best rival  6.72%   gap 1.48 pts
id1000020272043178  superbet.pl         5.44%   best rival  7.43%   gap 1.99 pts
id1000021072083054  superbet.rs         5.08%   best rival  8.16%   gap 3.08 pts
id1000021072083058  superbet.rs         5.53%   best rival  9.01%   gap 3.48 pts
id1000021072083066  superbet.rs         5.11%   best rival  8.36%   gap 3.25 pts
id1000032566887034  superbet.bet.br     4.06%   best rival  5.42%   gap 1.36 pts
id1000032566887018  superbet.bet.br     4.06%   best rival  5.41%   gap 1.36 pts
id1000032566887032  superbet.bet.br     3.87%   best rival  4.85%   gap 0.99 pts
id1000003872203068  napoleonsports.be   3.57%   best rival  5.25%   gap 1.68 pts
id1000003872203070  napoleonsports.be   3.74%   best rival  5.36%   gap 1.62 pts
id1000003872203072  napoleonsports.be   3.47%   best rival  4.98%   gap 1.51 pts

home feed closed tightest on 15 of 15 fixtures

Fifteen of fifteen. The Serbian feed closed its own tournament at 5.08% to 5.53% while the Romanian and Polish feeds sat at 8.16% to 9.35%, the widest gap in the sample. The Brazilian feed, which ran wide on the live board in Step 3, closed the Brasileirao at 3.87% to 4.06%, 0.99 to 1.36 points under superbet.ro. Napoleon Sports closed three Belgian fixtures at 3.47% to 3.74%.

If you backtest on Superbet, this is the table to rebuild first. A model tested against superbet.pl closes on Romanian football will look better than it is, because it trades against a price 3.5 to 4.0 points worse than the one a Romanian account sees. Our backtesting guide walks through the rest of the loop.

Step 5: give each Superbet price one vote

Six feeds on one platform are not six opinions. On a neutral fixture the Romanian and Serbian feeds quote one price. On a domestic fixture they split. So a static rule such as “count the Superbet family once” is wrong on exactly the boards where the home feed matters. Dedupe on the price tuple, per fixture. This step reuses board from Step 2.

# Reuses FEEDS, margin_1x2() and board from Step 2.
def superbet_votes(board):
    """Collapse the Superbet feeds on one board to one vote per distinct 1X2 price tuple."""
    groups = {}
    for slug in FEEDS.split(","):
        b = board.get(slug)
        if not b or b.get("suspended") or margin_1x2(b) is None:
            continue
        legs = b["markets"]["101"]["outcomes"]
        key = tuple(p["price"] for oid in ("101", "102", "103") for p in legs[oid]["players"].values())
        groups.setdefault(key, []).append(slug)
    return groups

for prices, slugs in superbet_votes(board).items():
    print(f"{'/'.join(map(str, prices)):<18} {', '.join(slugs)}")
2.75/3.15/2.5      superbet, superbet.ro
2.65/3.05/2.42     superbet.pl
2.67/3.05/2.45     superbet.rs, napoleonsports.be

Five priced feeds become three votes on the Cluj derby, and the home feed’s price is the best of the three on every leg. Feed those three into the consensus calculator and Superbet gets three votes, not five. When you shop for the best price, the line-shopping script will find the home feed on its own; the dedupe stops it from counting that price twice.

For a feed-by-feed view of each slug, the sportsbook pages list what OddsPapi carries: Superbet RO, Superbet PL, Superbet RS, Superbet BR and Napoleon Sports BE. For the rest of the Brazilian board around superbet.bet.br, see the Brasileirao odds API guide.

Superbet API FAQ

Does Superbet have a public API?

OddsPapi returns Superbet odds as JSON through /v4/odds, under six slugs: superbet, superbet.ro, superbet.pl, superbet.rs, superbet.bet.br and napoleonsports.be. One API key reads all six, alongside 350+ other bookmakers.

Which Superbet feed should I use?

Use the feed from the tournament’s own country. On 15 played fixtures in Romania, Poland, Serbia, Brazil and Belgium (20-21 September 2026), the home feed closed the 1X2 tightest every time, 0.99 to 3.48 points under its closest sibling.

Is superbet the same feed as superbet.ro?

Yes. The catalogue marks superbet as a clone of superbet.ro, and the two matched on 12,720 of 12,720 shared prices across 25 boards measured on 30 September 2026. Read superbet.ro.

Is Napoleon Sports part of Superbet?

Napoleon Sports runs on the Superbet platform: its rows carry the same Superbet event ID and the same offer-event path. It closed three Belgian Pro League fixtures at 3.47% to 3.74%, under superbet.ro and superbet.pl on every one.

Can I get historical Superbet odds?

Yes. /v4/historical-odds returns every snapshot for any three Superbet feeds per call on a played fixture, on the free tier. On Crvena Zvezda v Radnicki Nis (20 September 2026), superbet.rs closed the 1X2 at 5.53%.

How many bookmakers can I compare against Superbet?

The OddsPapi catalogue lists 361 bookmakers. The Nations League and Brasileirao boards we pulled on 30 September 2026 carried 169 to 271 books each, all in one /v4/odds call.

Pull all six Superbet feeds

Every call in this guide runs on the free key: 350+ bookmakers in the catalogue, free historical odds for the closing-line check in Step 4, and a WebSocket feed when polling gets too slow. Get a free API key and run Step 3 on the next round.