South America Sports Betting API: Rank 247 Books per Fixture
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.