Guide
Fetch betting odds with Python
A complete Python script that lists upcoming football events, pages through them, reads the odds of each event and retries correctly on 429 and 503. It uses only requests.
Before you start
You need an API key from the dashboard; see Authentication. The script sends it as Authorization: Bearer; the X-Api-Key header works as well.
Which bookmakers your key can read depends on your plan and bookmaker entitlements; see Bookmakers for the list your key returns. No bookmaker is in verified customer coverage right now. The examples use bookmaker=tipico and football. Examples use Tipico for illustration only. Tipico ingestion is offline for a source review and is not part of current customer coverage.
Install and set the key
Install requests and keep the key in an environment variable instead of the source file:
python -m pip install requests
export BETSPREAD_API_KEY="YOUR_API_KEY"A client that handles errors and rate limits
Every error response has the shape {"error":{"code","message"}}. Branch on error.code, not on the message text. The get helper below retries only what can succeed later:
429 rate_limit_exceeded: wait forRetry-Afterseconds, or untilX-RateLimit-Reset(Unix seconds) whenRetry-Afteris missing.503 service_unavailable: the API fails closed while an auth, quota or rate-limit backend is down. Retry with exponential backoff.400 validation_failed,401 missing_api_key/invalid_api_key,403 forbidden/live_not_enabled/api_key_scope_deniedand404 not_found: raise, because the same request fails again.
When the monthly REST quota is used up, X-RateLimit-Reset points at the start of the next month. The helper stops instead of sleeping when the wait exceeds MAX_WAIT_SECONDS. See Errors and Rate limits for the full contract.
import os
import time
from datetime import datetime, timezone
import requests
API_BASE = "https://api.betspread.io/api/v1"
MAX_RETRIES = 5
MAX_WAIT_SECONDS = 120
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['BETSPREAD_API_KEY']}"
class BetspreadError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status = status
self.code = code
def error_of(response):
try:
error = response.json()["error"]
return error["code"], error["message"]
except (ValueError, KeyError, TypeError):
return "unknown", response.text
def wait_seconds(response, attempt):
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
return int(retry_after)
reset = response.headers.get("X-RateLimit-Reset")
if reset is not None:
return max(0, int(reset) - int(time.time()))
return 2 ** attempt
def get(path, params=None):
for attempt in range(MAX_RETRIES):
response = session.get(f"{API_BASE}{path}", params=params, timeout=30)
if response.ok:
return response.json()
code, message = error_of(response)
if response.status_code == 429:
delay = wait_seconds(response, attempt)
elif response.status_code == 503:
delay = 2 ** attempt
else:
# 400, 401, 403 and 404 do not get better by retrying.
raise BetspreadError(response.status_code, code, message)
if delay > MAX_WAIT_SECONDS:
# A monthly quota resets at the next month start: stop here.
raise BetspreadError(response.status_code, code, message)
time.sleep(delay)
raise BetspreadError(response.status_code, code, "retries exhausted")Paginate events
GET /api/v1/events is paginated with limit (1–500, default 50) and offset. The response carries meta.total, meta.limit and meta.offset. Default ordering does not guarantee the next upcoming event first, so pass dateFrom (ISO-8601) when you want future games only.
def list_events(sport="football", date_from=None, page_size=100):
params = {"sport": sport, "limit": page_size, "offset": 0}
if date_from is not None:
params["dateFrom"] = date_from
while True:
page = get("/events", params)
yield from page["data"]
params["offset"] += page["meta"]["limit"]
if not page["data"] or params["offset"] >= page["meta"]["total"]:
returnRead the odds of an event
GET /api/v1/events/{eventId}/odds returns the event and its markets. Each market has a type, an optional line (null for 1X2) and one entry per bookmaker with its outcomes. Odds are decimal; every outcome carries status and lastUpdatedAt. REST returns only open quotes, so status is always open here.
def event_odds(event_id, bookmaker="tipico"):
params = {"bookmaker": bookmaker}
return get(f"/events/{event_id}/odds", params)["data"]
def odds_rows(odds):
event = odds["event"]
for market in odds["markets"]:
for bookmaker in market["bookmakers"]:
for outcome in bookmaker["outcomes"]:
yield (
event["kickoffAt"],
f"{event['homeTeam']} vs {event['awayTeam']}",
market["type"],
market.get("line"),
bookmaker["slug"],
outcome["outcome"],
outcome["odds"],
outcome["lastUpdatedAt"],
)For decisions on current prices, drop quotes whose lastUpdatedAt is older than your own cutoff; see Quote freshness.
Run it
Every request, including the odds request per event, counts against your monthly REST quota. The example therefore reads odds for the first five events only.
if __name__ == "__main__":
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
events = list(list_events(date_from=now))
print(f"{len(events)} upcoming events")
# One odds request per event: start small, every call counts.
for event in events[:5]:
for row in odds_rows(event_odds(event["id"])):
print(*row)python betspread_odds.pyComplete script
import os
import time
from datetime import datetime, timezone
import requests
API_BASE = "https://api.betspread.io/api/v1"
MAX_RETRIES = 5
MAX_WAIT_SECONDS = 120
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['BETSPREAD_API_KEY']}"
class BetspreadError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status = status
self.code = code
def error_of(response):
try:
error = response.json()["error"]
return error["code"], error["message"]
except (ValueError, KeyError, TypeError):
return "unknown", response.text
def wait_seconds(response, attempt):
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
return int(retry_after)
reset = response.headers.get("X-RateLimit-Reset")
if reset is not None:
return max(0, int(reset) - int(time.time()))
return 2 ** attempt
def get(path, params=None):
for attempt in range(MAX_RETRIES):
response = session.get(f"{API_BASE}{path}", params=params, timeout=30)
if response.ok:
return response.json()
code, message = error_of(response)
if response.status_code == 429:
delay = wait_seconds(response, attempt)
elif response.status_code == 503:
delay = 2 ** attempt
else:
# 400, 401, 403 and 404 do not get better by retrying.
raise BetspreadError(response.status_code, code, message)
if delay > MAX_WAIT_SECONDS:
# A monthly quota resets at the next month start: stop here.
raise BetspreadError(response.status_code, code, message)
time.sleep(delay)
raise BetspreadError(response.status_code, code, "retries exhausted")
def list_events(sport="football", date_from=None, page_size=100):
params = {"sport": sport, "limit": page_size, "offset": 0}
if date_from is not None:
params["dateFrom"] = date_from
while True:
page = get("/events", params)
yield from page["data"]
params["offset"] += page["meta"]["limit"]
if not page["data"] or params["offset"] >= page["meta"]["total"]:
return
def event_odds(event_id, bookmaker="tipico"):
params = {"bookmaker": bookmaker}
return get(f"/events/{event_id}/odds", params)["data"]
def odds_rows(odds):
event = odds["event"]
for market in odds["markets"]:
for bookmaker in market["bookmakers"]:
for outcome in bookmaker["outcomes"]:
yield (
event["kickoffAt"],
f"{event['homeTeam']} vs {event['awayTeam']}",
market["type"],
market.get("line"),
bookmaker["slug"],
outcome["outcome"],
outcome["odds"],
outcome["lastUpdatedAt"],
)
if __name__ == "__main__":
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
events = list(list_events(date_from=now))
print(f"{len(events)} upcoming events")
# One odds request per event: start small, every call counts.
for event in events[:5]:
for row in odds_rows(event_odds(event["id"])):
print(*row)Next steps
- Stream live odds over WebSocket instead of polling.
- Pull odds into Google Sheets on a schedule.
- API reference for every parameter and response code.