Finding arbitrage opportunities in sports betting means identifying discrepancies in pre-match football odds across different bookmakers. This allows you to place bets on all possible outcomes of an event and guarantee a profit, regardless of the result. Manually tracking these odds is impossible, but with a UK bookmaker odds API, you can programmatically identify these situations.
This tutorial will show you how to calculate arbitrage opportunities using Python and the UK Odds API. We'll cover fetching pre-match football odds, parsing the JSON response, applying the arbitrage formula, and integrating this into a functional script. You'll learn how to build a system that can quickly flag these profitable scenarios, giving you a significant edge over traditional methods that rely on slow, error-prone scraping.
Prerequisites
To follow this tutorial and calculate arbitrage opportunities, you'll need a few things set up:
- UK Odds API Key: You'll need an API key from ukoddsapi.com. The arbitrage API endpoint is available on the Business tier.
- Python 3.8+: Our code examples use Python.
requestslibrary: For making HTTP requests to the API. Install it via pip install requests.- Basic understanding of JSON: The API responses are in JSON format.
This setup ensures you can reliably access and process the necessary pre-match football odds JSON data.
Step 1: Fetching Pre-Match Football Events
The first step in how to calculate arbitrage opportunities is to get a list of upcoming football events. We'll use the /v1/football/events endpoint to retrieve scheduled fixtures that have odds available. This gives us the event_id needed for fetching detailed odds.
Here's how to make the request in Python:
import os
import requests
from datetime import date, timedelta
API_KEY = os.environ.get("UKODDSAPI_KEY")
if not API_KEY:
raise ValueError("UKODDSAPI_KEY environment variable not set.")
BASE_URL = "https://api.ukoddsapi.com"
HEADERS = {"X-Api-Key": API_KEY}
def fetch_football_events(schedule_date: date):
"""Fetches football events for a given date."""
params = {
"schedule_date": schedule_date.isoformat(),
"has_odds": "true",
"per_page": "50" # Adjust as needed for more events
}
try:
response = requests.get(
f"{BASE_URL}/v1/football/events",
headers=HEADERS,
params=params,
timeout=30
)
response.raise_for_status() # Raise an exception for HTTP errors
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching events: {e}")
return None
# Example usage: Fetch events for tomorrow
tomorrow = date.today() + timedelta(days=1)
events_data = fetch_football_events(tomorrow)
if events_data and events_data.get("events"):
print(f"Found {len(events_data['events'])} events for {tomorrow}:")
for event in events_data["events"][:3]: # Print first 3 for brevity
print(f" ID: {event['event_id']}, Match: {event['home_team']} vs {event['away_team']}")
else:
print(f"No events with odds found for {tomorrow}.")
This Python code fetches football events for a specified date. We set has_odds=true to ensure we only get events where pre-match odds are already available. The per_page parameter helps control pagination, letting you retrieve more events if needed.
Here's a sample of the JSON response you might receive from the /v1/football/events endpoint:
{
"schema_version": "1.0",
"count": 2,
"events": [
{
"event_id": "EV0000000001",
"league_name": "Premier League",
"home_team": "Arsenal",
"away_team": "Chelsea",
"kickoff_utc": "2026-04-26T15:00:00Z",
"markets_with_odds": ["match_betting"],
"unique_bookmaker_codes": ["UO001", "UO002", "UO003"]
},
{
"event_id": "EV0000000002",
"league_name": "Championship",
"home_team": "Leeds",
"away_team": "Leicester",
"kickoff_utc": "2026-04-26T12:30:00Z",
"markets_with_odds": ["match_betting"],
"unique_bookmaker_codes": ["UO001", "UO004", "UO005"]
}
],
"note": "Example only — response is truncated."
}
The key field here is event_id. We'll use this unique identifier in the next step to fetch the detailed odds for a specific match.

Step 2: Retrieving Detailed Odds for an Event
Once you have an event_id, you can retrieve all available pre-match football odds JSON for that specific fixture across various bookmakers and markets. The /v1/football/events/{event_id}/odds endpoint provides this granular data.
This is where the power of a UK bookmaker odds API becomes clear. Instead of scraping individual sites, you get a normalised feed.
def fetch_event_odds(event_id: str):
"""Fetches detailed odds for a specific event."""
params = {
"package": "full", # Use 'full' for broader market coverage on higher tiers
"odds_format": "decimal"
}
try:
response = requests.get(
f"{BASE_URL}/v1/football/events/{event_id}/odds",
headers=HEADERS,
params=params,
timeout=60
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching odds for {event_id}: {e}")
return None
# Assuming we got an event_id from the previous step
if events_data and events_data.get("events"):
first_event_id = events_data["events"][0]["event_id"]
odds_data = fetch_event_odds(first_event_id)
if odds_data:
print(f"\nOdds for {odds_data.get('event_title')} ({first_event_id}):")
# Process odds_data here
else:
print(f"Could not fetch odds for event {first_event_id}.")
This function fetches the full odds for a given event_id. We specify package="full" to access a wider range of markets (available on Pro and Business tiers) and odds_format="decimal" for easier calculations.
A simplified JSON response for detailed odds looks like this:
{
"schema_version": "1.0",
"event_id": "EV0000000001",
"event_title": "Arsenal vs Chelsea",
"kickoff_utc": "2026-04-26T15:00:00Z",
"markets": [
{
"market_id": "MA0000000001",
"market_name": "Match Betting",
"market_group": "main",
"selections": [
{
"selection_name": "Home",
"line": null,
"odds": 1.80,
"bookmaker_code": "UO001",
"status": "active"
},
{
"selection_name": "Draw",
"line": null,
"odds": 3.50,
"bookmaker_code": "UO001",
"status": "active"
},
{
"selection_name": "Away",
"line": null,
"odds": 4.20,
"bookmaker_code": "UO001",
"status": "active"
},
{
"selection_name": "Home",
"line": null,
"odds": 1.90,
"bookmaker_code": "UO002",
"status": "active"
},
{
"selection_name": "Draw",
"line": null,
"odds": 3.40,
"bookmaker_code": "UO002",
"status": "active"
},
{
"selection_name": "Away",
"line": null,
"odds": 4.00,
"bookmaker_code": "UO002",
"status": "active"
}
]
}
],
"note": "Example only — response is truncated."
}
Notice the markets array, which contains selections. Each selection has an odds value and a bookmaker_code. This structure is crucial for comparing odds across different bookmakers.
Step 3: Calculating Arbitrage for a Two-Way Market
To calculate arbitrage opportunities, you need to find the best odds for each possible outcome of a market across all bookmakers. For a two-way market (like "Over/Under 2.5 Goals"), you'd look for the best "Over" odds from one bookmaker and the best "Under" odds from another. For a three-way market (like "Match Betting" - Home/Draw/Away), you need the best odds for all three outcomes.
The core of the arbitrage calculation involves implied probability. For each selection, the implied probability is 1 / odds. If the sum of the implied probabilities for all outcomes in a market is less than 1, an arbitrage opportunity exists.
Let's define a function to find the best odds for each outcome and then calculate the arbitrage percentage.
def calculate_arbitrage(odds_data: dict, market_name: str = "Match Betting"):
"""
Calculates arbitrage opportunities for a specified market.
Returns (profit_percentage, best_odds_per_outcome) if arb exists, else None.
"""
best_odds = {}
# Find the specified market
target_market = None
for market in odds_data.get("markets", []):
if market.get("market_name") == market_name:
target_market = market
break
if not target_market:
return None, "Market not found"
# Find the best odds for each selection across all bookmakers
for selection in target_market.get("selections", []):
name = selection["selection_name"]
current_odds = selection["odds"]
bookmaker = selection["bookmaker_code"]
if name not in best_odds or current_odds > best_odds[name]["odds"]:
best_odds[name] = {"odds": current_odds, "bookmaker": bookmaker}
# Ensure we have odds for all expected outcomes (e.g., Home, Draw, Away for Match Betting)
# This example assumes a 3-way market, adjust for 2-way if needed
if len(best_odds) < target_market.get("selection_count", 0):
return None, "Not all outcomes have best odds found"
# Calculate implied probabilities
implied_probabilities_sum = 0
for outcome, data in best_odds.items():
if data["odds"] > 0: # Avoid division by zero
implied_probabilities_sum += (1 / data["odds"])
else:
return None, "Zero odds found, cannot calculate"
# If sum < 1, an arbitrage opportunity exists
if implied_probabilities_sum < 1:
profit_percentage = (1 / implied_probabilities_sum - 1) * 100
return profit_percentage, best_odds
else:
return None, "No arbitrage opportunity"
# Example usage:
if odds_data:
profit, details = calculate_arbitrage(odds_data, "Match Betting")
if profit is not None and isinstance(profit, float):
print(f"\nArbitrage found for {odds_data['event_title']} (Match Betting):")
print(f" Profit: {profit:.2f}%")
for outcome, data in details.items():
print(f" {outcome}: Odds {data['odds']} at {data['bookmaker']}")
else:
print(f"\n{odds_data['event_title']} (Match Betting): {details}")
This function first aggregates the best odds for each outcome (Home, Draw, Away) from all bookmakers for a specific market. Then, it sums their implied probabilities. If this sum is less than 1, an arbitrage exists, and the profit percentage is calculated. This is a fundamental step in how to calculate arbitrage opportunities explained in a practical way.

Step 4: Scaling to Multiple Bookmakers and Markets
Manually checking each event and market is still inefficient. A robust arbitrage finder needs to:
- Iterate through events: Fetch all relevant events for a given period.
- Iterate through markets: For each event, check multiple markets (e.g., Match Betting, Over/Under, Both Teams to Score).
- Aggregate best odds: For each market, find the best odds for each outcome across all available bookmakers.
- Calculate arbitrage: Apply the formula.
The UK Odds API simplifies this by providing a unified data source. For users on the Business tier, there's even a dedicated /v1/football/arbitrage endpoint designed to return pre-calculated arbitrage opportunities directly. This endpoint handles the heavy lifting of comparing odds across bookmakers and identifying qualifying situations.
Here’s a high-level example of how you might integrate the dedicated arbitrage endpoint (if on the Business tier) or build a loop for manual calculation:
def fetch_arbitrage_feed(target_date: date):
"""Fetches pre-calculated arbitrage opportunities (Business tier only)."""
params = {
"date": target_date.isoformat(),
"min_profit": "0.5", # Minimum profit percentage to filter
"total_stake": "100" # Example total stake for rounding calculations
}
try:
response = requests.get(
f"{BASE_URL}/v1/football/arbitrage",
headers=HEADERS,
params=params,
timeout=120
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching arbitrage feed: {e}")
return None
# --- If you have the Business tier ---
# arbitrage_feed = fetch_arbitrage_feed(tomorrow)
# if arbitrage_feed and arbitrage_feed.get("arbitrage_opportunities"):
# print("\n--- Arbitrage Opportunities from dedicated feed ---")
# for arb in arbitrage_feed["arbitrage_opportunities"][:3]:
# print(f" Event: {arb['event_title']}, Market: {arb['market_name']}")
# print(f" Profit: {arb['profit_percentage']:.2f}%")
# for outcome in arb['outcomes']:
# print(f" {outcome['selection_name']}: Odds {outcome['odds']} at {outcome['bookmaker_code']}")
# else:
# print("No arbitrage opportunities found via dedicated feed.")
# --- Manual iteration for other tiers (conceptual) ---
print("\n--- Manually searching for arbitrage opportunities ---")
if events_data and events_data.get("events"):
for event_summary in events_data["events"]:
event_id = event_summary["event_id"]
odds_data_full = fetch_event_odds(event_id)
if odds_data_full:
# Check multiple markets if available and relevant
for market_to_check in ["Match Betting", "Over/Under 2.5 Goals"]: # Example markets
profit, details = calculate_arbitrage(odds_data_full, market_to_check)
if profit is not None and isinstance(profit, float):
print(f"\n Arbitrage found for {event_summary['home_team']} vs {event_summary['away_team']} ({market_to_check}):")
print(f" Profit: {profit:.2f}%")
for outcome, data in details.items():
print(f" {outcome}: Odds {data['odds']} at {data['bookmaker']}")
This conceptual loop demonstrates how to calculate arbitrage opportunities integration into a broader system. For non-Business tier users, you'd iterate through events and markets, calling calculate_arbitrage for each. For Business tier users, the /v1/football/arbitrage endpoint provides a streamlined solution, delivering ready-to-use arbitrage data. This dramatically simplifies building an arbitrage finder, moving you away from complex, fragile odds API without scraping solutions.
Common Mistakes When Calculating Arbitrage
Building an arbitrage finder comes with its own set of challenges. Here are some common mistakes developers make:
- Confusing pre-match with in-play odds: Arbitrage opportunities are fleeting, but pre-match odds are relatively stable until kickoff. Do not use "live" or "in-play" data for arbitrage calculations, as UK Odds API focuses on pre-match football odds JSON.
- Stale odds: Odds change. If your data isn't fresh, your calculations will be wrong. Always fetch the latest odds before calculating.
- Ignoring market correlation: Ensure you're comparing truly independent outcomes within the same market (e.g., Home, Draw, Away for a match result). Don't mix markets.
- Incorrect odds format conversion: Always use decimal odds for calculation (
1 / odds). If using fractional, convert it first. - Overlooking bookmaker terms: Some bookmakers have rules that might void one leg of an arbitrage bet (e.g., maximum payouts, rule 4 deductions). Factor these into your risk model.
- Rate limit issues: Aggressively polling too many events or markets will quickly hit API rate limits. Design your system to be efficient, fetching data only when necessary or in batches.
- Transaction costs: Remember to factor in any fees or commissions from betting exchanges or payment processors, as these can eat into small arbitrage profits.
- Incomplete bookmaker coverage: If your UK bookmaker odds API doesn't cover enough bookmakers, you might miss opportunities. Ensure your API provides comprehensive coverage.
Options and Alternatives for Odds Data
When looking for how to calculate arbitrage opportunities, developers often consider several approaches for obtaining odds data. Each has its pros and cons.
| Method | Data Freshness | Bookmaker Coverage | Ease of Integration | Reliability | Cost |
|---|---|---|---|---|---|
| UK Odds API | High (updated snapshots) | Excellent (27+ UK) | High (normalised JSON) | High | Tiered |
| Manual Scraping | Variable | Limited (per site) | Low (complex, breaks) | Low | High (dev time) |
| Generic Sports Data API | Variable | Often global, less UK-specific | Medium | Medium | Variable |
The UK Odds API provides a robust solution for developers needing pre-match football odds JSON specifically from UK bookmakers. It eliminates the need for complex, fragile scraping setups, offering a reliable and normalised data feed. While manual scraping might seem "free" initially, the development time, maintenance, and constant debugging of broken scrapers quickly make it the most expensive option. Generic sports data APIs might offer broad coverage but often lack the depth or specific UK bookmaker focus needed for effective arbitrage detection.
FAQ
How fresh are the odds from the UK Odds API for arbitrage calculations?
The UK Odds API provides updated snapshots of pre-match football odds. While not an in-play feed, these snapshots are refreshed frequently enough to identify arbitrage opportunities before kickoff.
Can I use the Free tier of UK Odds API to calculate arbitrage?
The Free tier offers limited bookmaker coverage (2 UK bookmakers) and request volume (300 requests/month). While you can experiment with the core concepts, comprehensive arbitrage detection requires broader bookmaker coverage and higher request limits, typically found in the Pro or Business tiers.
What if I only want to find arbitrage for a specific league, like the Premier League?
You can filter events by league_name after fetching them using the /v1/football/events endpoint. Then, proceed with fetching odds and calculating arbitrage only for the events in your desired league.
How do I handle different market types (e.g., Over/Under, Both Teams to Score) in my arbitrage calculations?
The calculate_arbitrage function can be adapted to different market types by changing the market_name parameter. The key is to ensure you identify all possible outcomes for that specific market and find the best odds for each.
Does the UK Odds API provide historical odds data for backtesting arbitrage strategies?
Yes, historical odds data is available on the Pro and Business tiers. This allows you to backtest your arbitrage detection algorithms against past events to refine your strategy.
Conclusion
Calculating arbitrage opportunities programmatically is a powerful application of sports betting data. By leveraging a reliable UK bookmaker odds API, you can move beyond the limitations of manual scraping and build sophisticated tools to identify profitable pre-match football odds JSON discrepancies. The UK Odds API provides the structured, normalised data you need to implement these calculations efficiently and reliably.
Ready to start building your arbitrage finder? Explore the API documentation and get your API key at ukoddsapi.com.