South America Sports Betting API: Rank 247 Books per Fixture

South America Sports Betting API
How To Guides October 26, 2026

If you need a South America sports betting API, you want the Argentine, Colombian and Peruvian boards as JSON, with the local books next to the global ones. OddsPapi carries 68 South American soccer tournaments in the same schema as the Premier League, and one key reads all of them. On 19 October 2026, between 13:11 and 13:20 UTC, we read 26 open boards across nine tournaments: the median fixture carried 247 bookmakers, and the deepest Argentine board carried 276.

You also need a reference close for each tournament, the book that settles tightest. We pulled the closing 1X2 on 62 played fixtures from 10 to 19 October. Of the three books we tracked, Pinnacle closed tightest in Argentina, Brazil, Chile, Ecuador, Uruguay and Paraguay. In Peru and Bolivia, the Peruvian book Apuesta Total closed under Pinnacle on all 12 fixtures. This guide pulls the tournaments, ranks every book on each board, and checks the close.

Why a scraper per country gives you a partial board

If you scrape South American books, you maintain one parser per site and one per country, and you still miss most of the board. On the boards we read, the median fixture drew 247 books. Many of them are crypto and offshore sportsbooks, US books and European exchanges that no regional scraper touches. Some of the tightest prices on the board come from those books.

OddsPapi returns every book in one /v4/odds call, with a bookmakerFixtureId and a fixturePath deep link to the event on each book’s own site. The local books ship under their own slugs: apuestatotal (Peru), betplay and rushbet.co (Colombia), betano.pe, and the Brazilian licensees that our Brasileirão odds API guide covers.

Scrape local sites OddsPapi
Access No public odds API, one parser per site REST and WebSocket, one apiKey query parameter
Books per fixture The sites you scrape 247 on the median open board we read, 350+ in the catalogue
Tournaments One site per country 68 South American soccer tournaments with fixtures loaded
Shared platforms Invisible A shared bookmakerFixtureId groups the books on one platform
Played fixtures Only what your recorder saved /v4/historical-odds on the free tier

Step 1: find the South American tournaments

Authentication is one query parameter, apiKey. The helper waits out a 429 for as long as the error body says, backs off on a 5xx, and returns None on a 404. The script reads the soccer catalogue, keeps the ten South American countries and the two CONMEBOL club cups, and picks the biggest competition per country.

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(8):
        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}")

COUNTRIES = ["Argentina", "Bolivia", "Brazil", "Chile", "Colombia", "Ecuador",
             "Paraguay", "Peru", "Uruguay", "Venezuela"]

tournaments = get("tournaments", sportId=10)          # soccer
live = [t for t in tournaments
        if (t["categoryName"] in COUNTRIES or t["tournamentName"].startswith("CONMEBOL"))
        and t["futureFixtures"] > 0]
print(f"{len(live)} South American soccer tournaments with fixtures loaded\n")

# the biggest competition per country, plus the two CONMEBOL club cups
top = {}
for t in sorted(live, key=lambda t: -t["futureFixtures"]):
    key = t["tournamentName"] if t["categoryName"] == "International Clubs" else t["categoryName"]
    if "Women" in t["tournamentName"] or "Reserve" in t["tournamentName"]:
        continue
    top.setdefault(key, t)
for key, t in sorted(top.items()):
    print(f'{t["tournamentId"]:>6}  {t["categoryName"]:<19} {t["tournamentName"]:<26} '
          f'{t["futureFixtures"]:>3} future fixtures')
68 South American soccer tournaments with fixtures loaded

   155  Argentina           Primera LPF                 45 future fixtures
 33980  Bolivia             Division Profesional        48 future fixtures
   325  Brazil              Brasileiro Serie A          70 future fixtures
   384  International Clubs CONMEBOL Libertadores        2 future fixtures
   480  International Clubs CONMEBOL Sudamericana        2 future fixtures
 27665  Chile               Primera Division            50 future fixtures
 27070  Colombia            Liga DIMAYOR                54 future fixtures
   240  Ecuador             LigaPro Primera A           48 future fixtures
 27098  Paraguay            Division de Honor           42 future fixtures
   406  Peru                Liga 1                      45 future fixtures
   278  Uruguay             Primera Division             8 future fixtures
 19282  Venezuela           Copa Venezuela               8 future fixtures

Each tournamentId is the filter you pass to /v4/fixtures. The endpoint docs leave it out, but it works and lets you pull a whole month in one call. Venezuela shows its cup because the catalogue had no Venezuelan league fixtures loaded on the day we ran this.

Step 2: rank every book on the next board in each tournament

This step reads the next open board in each tournament and scores every book on the 1X2. A book counts only when the book itself is open and all three legs are active. The script prints four numbers per board: books in the payload, books with a full 1X2, distinct price sets, and the median margin. It then prints the margin and rank of three books that you can use as a reference.

# Reuses get(), top and the imports from Step 1.
WATCH = ["pinnacle", "betplay", "apuestatotal"]

def margin_1x2(book):
    """1X2 margin in %, or None unless the book is open and all three legs are active."""
    if book.get("suspended") or book.get("bookmakerIsActive") is False:
        return None
    m = book["markets"].get("101")                     # Full Time Result
    if not m:
        return None
    legs = [m["outcomes"].get(o, {}).get("players", {}).get("0") for o in ("101", "102", "103")]
    if not all(l and l.get("active") and l.get("price") for l in legs):
        return None
    return (sum(1 / l["price"] for l in legs) - 1) * 100, tuple(l["price"] for l in legs)

now = datetime.now(timezone.utc)
window = {"from": now.strftime("%Y-%m-%d"),
          "to": (now + timedelta(days=5)).strftime("%Y-%m-%d")}
for key, t in sorted(top.items()):
    fixtures = get("fixtures", tournamentId=t["tournamentId"], **window) or []
    time.sleep(1)
    for f in sorted(fixtures, key=lambda f: f["startTime"]):
        if (f["statusName"] != "Pre-Game" or not f["hasOdds"]
                or f["startTime"] <= now.isoformat()):
            continue
        board = (get("odds", fixtureId=f["fixtureId"]) or {}).get("bookmakerOdds", {})
        time.sleep(1)
        board = {s: b for s, b in board.items() if "+" not in s and s != "demo"}
        quotes = {s: q for s, b in board.items() if (q := margin_1x2(b)) and q[0] > 0}
        if len(quotes) >= 20:                          # first board with an open 1X2
            break
    else:
        print(f"{key:<22} no open board in the next 5 days")
        continue
    order = sorted(quotes, key=lambda s: quotes[s][0])
    rank = lambda s: f"{quotes[s][0]:.2f}% #{order.index(s) + 1}" if s in quotes else "-"
    print(f'{key:<22} {len(board):>3} books  {len(quotes):>3} full 1X2  '
          f'{len({q[1] for q in quotes.values()}):>3} distinct  '
          f'median {median(q[0] for q in quotes.values()):.2f}%')
    print("    " + "   ".join(f"{s} {rank(s)}" for s in WATCH))
Argentina              274 books  240 full 1X2   91 distinct  median 7.52%
    pinnacle 3.50% #18   betplay 3.67% #26   apuestatotal 5.86% #64
Bolivia                no open board in the next 5 days
Brazil                 275 books  251 full 1X2   99 distinct  median 6.99%
    pinnacle 3.12% #20   betplay 5.81% #62   apuestatotal 4.90% #42
CONMEBOL Libertadores  258 books  244 full 1X2   90 distinct  median 7.59%
    pinnacle 5.82% #55   betplay 2.54% #8   apuestatotal 4.89% #18
CONMEBOL Sudamericana  260 books  238 full 1X2   82 distinct  median 7.72%
    pinnacle 5.44% #43   betplay 3.96% #15   apuestatotal 4.99% #20
Chile                  240 books  223 full 1X2   64 distinct  median 8.96%
    pinnacle 5.53% #8   betplay 6.84% #27   apuestatotal 6.83% #22
Colombia               253 books  235 full 1X2   86 distinct  median 8.80%
    pinnacle 4.89% #21   betplay 3.77% #13   apuestatotal 6.55% #31
Ecuador                204 books  188 full 1X2   65 distinct  median 8.97%
    pinnacle 5.33% #16   betplay -   apuestatotal 6.51% #20
Paraguay               no open board in the next 5 days
Peru                   207 books  203 full 1X2   60 distinct  median 9.31%
    pinnacle 7.82% #43   betplay -   apuestatotal 7.23% #28
Uruguay                252 books  231 full 1X2   77 distinct  median 9.84%
    pinnacle 4.61% #16   betplay 10.07% #157   apuestatotal 5.60% #22
Venezuela              no open board in the next 5 days

We ran this on 19 October 2026 at 13:35 UTC. Two numbers matter. First, the board is wide: 204 to 275 books per fixture. Second, the price sets collapse. Argentina’s 240 full quotes came from 91 distinct price sets, because many books run on one platform and copy its prices. Dedupe before you average, as our consensus odds guide shows.

Pinnacle ranks 8th to 55th on these boards. Exchanges, prediction markets and a few low-margin sportsbooks sit above it, and the median book charges 7% to 10%. Paraguay printed no board because its next round was four days out and the books had not opened it yet. Bolivia and Venezuela had no fixture loaded in the window. Step 3 reads Paraguay and Bolivia on played fixtures instead.

The local books split three ways. apuestatotal shares its event ID with 23 other slugs (among them lottoland, wazamba, estrelabet and winpot.mx) on all 26 boards of our census, but its 1X2 matched another book’s prices on one board only. Treat it as its own price. betplay and rushbet.co carry the Kambi event ID with 36 other slugs, which our Unibet API guide maps. On five of eight Colombian boards, rushbet.co quoted betplay‘s exact 1X2. On the Argentine boards it charged about 13%. betano.pe quoted the same prices as the global betano book on every board, so it adds no independent price. Our Betano API guide covers that family.

Step 3: check the closing line on played fixtures

A live board shows you the price days before kickoff. The close shows where each book settled after the full market cycle. This step pulls the played Colombian round of 17 to 19 October from /v4/historical-odds and takes the last active price before kickoff on each leg. One call per outcome keeps the payload small. Use startTime as the cutoff, because trueStartTime can sit a few minutes after kickoff and let in-play prices in.

# Reuses get() and the imports from Step 1.
BOOKS = "pinnacle,betplay,apuestatotal"
COLOMBIA, RESULT = 27070, ["101", "102", "103"]   # Full Time Result: home, draw, away

def closing_prices(fixture_id, kickoff, outcome_id):
    """Last active price before kickoff, per book, for one outcome."""
    hist = get("historical-odds", fixtureId=fixture_id,
               bookmakers=BOOKS, outcomeId=outcome_id)
    time.sleep(4.6)                    # /historical-odds needs a longer gap
    close = {}
    for slug, book in (hist or {}).get("bookmakers", {}).items():
        for market in book["markets"].values():
            for o in market["outcomes"].values():
                snaps = [s for s in o["players"].get("0", [])
                         if s["active"] and s["createdAt"] < kickoff]
                if snaps:
                    close[slug] = snaps[-1]["price"]
    return close

played = get("fixtures", tournamentId=COLOMBIA,
             **{"from": "2026-10-17", "to": "2026-10-20"}) or []
played = [f for f in played if f["statusName"] == "Finished"]

margins = {s: [] for s in BOOKS.split(",")}
for f in sorted(played, key=lambda f: f["startTime"]):
    kickoff = f["startTime"]           # scheduled kickoff, not trueStartTime
    legs = [closing_prices(f["fixtureId"], kickoff, oid) for oid in RESULT]
    row = {}
    for slug in margins:
        if all(slug in leg for leg in legs):
            row[slug] = (sum(1 / leg[slug] for leg in legs) - 1) * 100
            margins[slug].append(row[slug])
    name = f'{f["participant1Name"]} v {f["participant2Name"]}'[:34]
    print(f"{name:<34}", "  ".join(f"{s} {v:.2f}%" for s, v in row.items()))

print(f"\n{len(played)} played Liga DIMAYOR fixtures, closing 1X2:")
for slug, vals in margins.items():
    print(f"  {slug:<12} median {median(vals):.2f}%  ({len(vals)} fixtures)")
Boyaca Chico FC v Atletico Naciona pinnacle 4.90%  betplay 6.86%  apuestatotal 6.48%
Millonarios FC v Jaguares de Cordo pinnacle 5.27%  betplay 2.94%
Independiente Medellin v CD Once C pinnacle 4.56%  betplay 5.89%  apuestatotal 6.55%
Aguilas Doradas Rionegro v CD Toli pinnacle 7.38%  betplay 7.30%  apuestatotal 6.46%
CA Bucaramanga v America de Cali   pinnacle 4.47%  betplay 4.71%  apuestatotal 6.46%
Llaneros FC v Independiente Santa  pinnacle 7.41%  betplay 10.45%  apuestatotal 6.42%
Deportivo Cali v CD Junior FC      pinnacle 7.33%  betplay 3.48%  apuestatotal 6.53%

7 played Liga DIMAYOR fixtures, closing 1X2:
  pinnacle     median 5.27%  (7 fixtures)
  betplay      median 5.89%  (7 fixtures)
  apuestatotal median 6.47%  (6 fixtures)

Colombia is the split tournament. betplay closed under Pinnacle on three of these seven fixtures, and Pinnacle took the median by 0.6 points. We ran the same check on 62 played fixtures from 10 to 19 October, across all eleven tournaments:

Tournament (played, 10 to 19 Oct 2026) Fixtures Median close, pinnacle Median close, betplay Median close, apuestatotal Tightest close of the three
Brazil, Brasileiro Serie A 6 2.87% 7.19% 5.03% pinnacle 6 of 6
Argentina, Primera LPF 8 3.54% 5.17% 6.01% pinnacle 6 of 8
Chile, Primera Division 6 4.24% 7.21% 5.52% pinnacle 6 of 6
Uruguay, Primera Division 6 4.48% 10.56% 5.49% pinnacle 6 of 6
Ecuador, LigaPro 6 5.08% 7.08% 6.64% pinnacle 6 of 6
Paraguay, Division de Honor 6 5.14% 8.84% 5.98% pinnacle 6 of 6
Colombia, Liga DIMAYOR 8 5.21% 5.30% 6.46% betplay 3, pinnacle 3, apuestatotal 2
Peru, Liga 1 6 6.48% 6.48% 5.49% apuestatotal 6 of 6
Bolivia, Division Profesional 6 9.24% 13.10% 6.53% apuestatotal 6 of 6
CONMEBOL Libertadores (semi-final first legs) 2 3.82% 2.97% 5.04% betplay 2 of 2
CONMEBOL Sudamericana (semi-final first legs) 2 4.16% 2.76% 5.00% betplay 2 of 2

Pinnacle closed tightest on 39 of the 62 fixtures. Its median close climbs as you move down the table: 2.87% in Brazil, over 5% in Ecuador, Paraguay and Colombia, and 9.24% in Bolivia. Apuesta Total’s median close stays between 5.0% and 6.6% in every tournament. In Peru it closed at 5.49% against Pinnacle’s 6.48%, and in Bolivia at 6.53% against 9.24%. It closed tighter than Pinnacle on 12 of those 12 fixtures. betplay closed the four CONMEBOL semi-final first legs at 2.35% to 3.39%. Four fixtures is a small sample, so treat the cups as a lead and re-run the check after the second legs.

Take the fair price from the deduplicated board, de-vigged, and use the table to pick the reference close per tournament. Pinnacle is the tightest of the three in Brazil, Argentina, Chile, Uruguay, Ecuador and Paraguay. In Peru and Bolivia, compare against apuestatotal. In Colombia, read pinnacle and betplay side by side. Our backtesting guide turns closes like these into a full strategy test, and our African football odds API guide runs the same local-book check on a different region.

Snapshots record polling, so collapse identical consecutive prices before you count line moves. To read these boards live without a polling loop, use the WebSocket feed. For a one-page view of the Brazilian top flight, see the Brasileirão league page.

South America sports betting API FAQ

Is there a sports betting API for South American soccer?

Yes. OddsPapi carries 68 South American soccer tournaments with fixtures loaded, among them Argentina’s Primera LPF, Colombia’s Liga DIMAYOR, Peru’s Liga 1, the Brasileiro Serie A and both CONMEBOL club cups. Call /v4/odds with a fixtureId and your apiKey.

How many bookmakers price South American soccer?

On 26 open top-flight boards read on 19 October 2026, the median fixture carried 247 bookmakers, and the deepest Argentine board carried 276. They include Pinnacle, exchanges, US books, crypto and offshore sportsbooks and local books.

Which South American bookmakers can I read through the API?

The local slugs include apuestatotal (Peru), betplay and rushbet.co (Colombia), betano.pe, and Brazilian licensees such as betano.bet.br, estrelabet.bet.br and kto.bet.br. Check the live /v4/bookmakers catalogue for the current list.

Which bookmaker closes South American soccer tightest?

Across 62 played fixtures from 10 to 19 October 2026, Pinnacle closed the 1X2 tightest of three books on 39. Apuesta Total closed under Pinnacle on 12 of 12 Peru and Bolivia fixtures, and betplay closed the four CONMEBOL semi-final first legs at 2.35% to 3.39%.

Can I get historical odds for Copa Libertadores fixtures?

Yes. /v4/historical-odds returns snapshots on played fixtures, on the free tier. Pass the fixtureId, up to three bookmaker slugs and one outcomeId to keep the payload small, then take the last active snapshot before startTime as the close.

Read 247 books per fixture from one key

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