Monitoring pre-match football odds changes across multiple UK bookmakers is a common task for developers building betting tools or comparison sites. The challenge isn't just getting the data, but efficiently identifying what has changed between two snapshots. This is where a robust method to diff two bookmaker snapshots for alerting becomes essential.
You need a reliable way to compare current odds against previous data. This allows you to trigger alerts for significant price movements, new markets, or withdrawn selections. Trying to achieve this by scraping often leads to inconsistent data and IP bans. A dedicated UK bookmaker odds API provides consistent, structured pre-match football odds JSON that makes the diffing process much more manageable, allowing you to build an effective alerting system without the headaches of constant scraping.
Prerequisites
Before diving into the code, ensure you have the following set up:
- UK Odds API Key: You'll need an API key from ukoddsapi.com to access the pre-match football odds data.
- Python 3.x: Our examples will use Python.
requestslibrary: For making HTTP requests to the API. Install it via pip install requests.deepdifflibrary: A powerful library for diffing complex Python objects (dictionaries, lists). Install it via pip install deepdiff.- An
event_id: You'll need a specific football event ID to fetch odds for. You can get this from the/v1/football/eventsendpoint.
This tutorial will walk you through fetching pre-match odds, structuring them for comparison, implementing the diffing logic, and setting up basic alerts.
Step 1: Fetching Initial Pre-Match Odds Snapshots
The first step in any diffing process is to get the data you want to compare. We'll use the UK Odds API to fetch the full pre-match odds for a specific football event. This gives us a baseline snapshot.
Here's how to fetch the odds for a given event_id using Python:
import os
import requests
import json # For pretty printing
# Set your API key from environment variables for security
API_KEY = os.environ.get("UKODDSAPI_KEY", "YOUR_API_KEY")
BASE_URL = "https://api.ukoddsapi.com"
HEADERS = {"X-Api-Key": API_KEY}
def fetch_odds_snapshot(event_id: str) -> dict:
"""Fetches a pre-match odds snapshot for a given event ID."""
endpoint = f"{BASE_URL}/v1/football/events/{event_id}/odds"
params = {"package": "core", "odds_format": "decimal"}
try:
response = requests.get(endpoint, 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 odds for event {event_id}: {e}")
return {}
# Example usage: Replace with a real event_id
# You can get event_ids from the /v1/football/events endpoint
# For demonstration, let's use a placeholder event_id
example_event_id = "EVT000000000001" # Replace with a real event ID from the API
initial_snapshot = fetch_odds_snapshot(example_event_id)
if initial_snapshot:
print(f"Fetched initial snapshot for {initial_snapshot.get('event_title')}:")
# print(json.dumps(initial_snapshot, indent=2)) # Uncomment to see full JSON
else:
print("Failed to fetch initial snapshot.")
This Python code defines a function fetch_odds_snapshot that takes an event_id and returns the parsed JSON response from the /v1/football/events/{event_id}/odds endpoint. It uses the core package and decimal odds format, which is suitable for most alerting needs. Error handling is included to catch network issues or API errors.
The initial_snapshot variable will hold the first set of pre-match football odds data. This will serve as our reference point for comparison.

Step 2: Storing and Normalising Odds Data
Before you can effectively diff two bookmaker snapshots for alerting, you need to ensure your data is consistently structured. The raw JSON from the API is great, but for robust diffing, especially with lists of markets and selections, you often need to normalise it. This means ensuring that lists are sorted by a consistent key, and irrelevant fields are removed or ignored.
Here's a simplified example of what a relevant part of the markets array might look like, and how we might process it for consistent diffing:
{
"event_id": "EVT000000000001",
"event_title": "Man Utd vs Liverpool",
"kickoff_utc": "2026-04-25T15:00:00Z",
"markets": [
{
"market_id": "MKT001",
"market_name": "Match Winner",
"selections": [
{ "selection_name": "Man Utd", "odds": 2.50, "bookmaker_code": "UO001", "status": "active" },
{ "selection_name": "Draw", "odds": 3.20, "bookmaker_code": "UO001", "status": "active" },
{ "selection_name": "Liverpool", "odds": 2.80, "bookmaker_code": "UO001", "status": "active" },
{ "selection_name": "Man Utd", "odds": 2.45, "bookmaker_code": "UO002", "status": "active" }
]
},
{
"market_id": "MKT002",
"market_name": "Over/Under 2.5 Goals",
"selections": [
{ "selection_name": "Over 2.5", "odds": 1.80, "bookmaker_code": "UO001", "status": "active" },
{ "selection_name": "Under 2.5", "odds": 2.00, "bookmaker_code": "UO001", "status": "active" }
]
}
]
}
For reliable diffing, we should:
- Sort lists: The
marketsandselectionsarrays can have their order change without a meaningful odds change. Sort them by a unique identifier (e.g.,market_idfor markets, and a combination ofselection_nameandbookmaker_codefor selections). - Filter irrelevant fields: Fields like
last_updated_utcmight change constantly without reflecting a betting change. Exclude them from the diff. - Create a unique key for each selection: Since multiple bookmakers can offer odds for the same selection, a unique key for each selection within a market is crucial.
Here's a Python function to normalise the odds data:
def normalize_odds_data(odds_data: dict) -> dict:
"""
Normalises odds data for consistent diffing.
Sorts markets by ID and selections by name + bookmaker.
"""
if not odds_data or not odds_data.get("markets"):
return odds_data
normalized = odds_data.copy()
normalized_markets = []
for market in normalized["markets"]:
if "selections" in market:
# Create a stable key for each selection for sorting
for selection in market["selections"]:
selection["_sort_key"] = f"{selection['selection_name']}-{selection['bookmaker_code']}"
market["selections"] = sorted(
market["selections"], key=lambda s: s["_sort_key"]
)
# Remove the temporary sort key after sorting
for selection in market["selections"]:
del selection["_sort_key"]
normalized_markets.append(market)
normalized["markets"] = sorted(normalized_markets, key=lambda m: m["market_id"])
# Optionally remove fields that are not relevant for odds changes
# For example, 'schema_version', 'note', 'generated_at_utc'
normalized.pop("schema_version", None)
normalized.pop("note", None)
normalized.pop("generated_at_utc", None) # Timestamp changes constantly
return normalized
# Normalize the initial snapshot
normalized_initial_snapshot = normalize_odds_data(initial_snapshot)
if normalized_initial_snapshot:
print("\nNormalized initial snapshot (truncated for brevity):")
# print(json.dumps(normalized_initial_snapshot, indent=2)) # Uncomment to see full JSON
else:
print("Failed to normalize initial snapshot.")
This normalize_odds_data function ensures that the order of markets and selections doesn't cause false positives when comparing snapshots. It's a critical step for reliable how to diff two bookmaker snapshots for alerting integration.
Step 3: Implementing the Diffing Logic
With normalised snapshots, we can now implement the core diffing logic. The deepdiff library is excellent for this. It can compare two complex Python objects (like nested dictionaries and lists) and report detailed changes.
First, let's simulate a second snapshot with some changes. We'll modify a few odds and add a new selection.
import copy
from deepdiff import DeepDiff
# Simulate a second snapshot with some changes
# Start with a deep copy of the normalized initial snapshot
second_snapshot = copy.deepcopy(normalized_initial_snapshot)
# Introduce some changes
if second_snapshot and second_snapshot.get("markets"):
# Change an existing odd for Man Utd
for market in second_snapshot["markets"]:
if market["market_name"] == "Match Winner":
for selection in market["selections"]:
if selection["selection_name"] == "Man Utd" and selection["bookmaker_code"] == "UO001":
selection["odds"] = 2.40 # Odds changed from 2.50 to 2.40
if selection["selection_name"] == "Liverpool" and selection["bookmaker_code"] == "UO001":
selection["odds"] = 2.90 # Odds changed from 2.80 to 2.90
# Add a new selection from another bookmaker
market["selections"].append({
"selection_name": "Man Utd",
"odds": 2.42,
"bookmaker_code": "UO003", # New bookmaker
"status": "active"
})
# Re-normalize after adding a selection to maintain sort order
market["selections"] = sorted(
market["selections"], key=lambda s: f"{s['selection_name']}-{s['bookmaker_code']}"
)
# Now, normalize the second snapshot
normalized_second_snapshot = normalize_odds_data(second_snapshot)
print("\nSimulated changes in second snapshot.")
# Perform the diff
if normalized_initial_snapshot and normalized_second_snapshot:
diff = DeepDiff(normalized_initial_snapshot, normalized_second_snapshot, ignore_order=True, verbose_level=2)
if diff:
print("\nChanges detected:")
print(json.dumps(diff, indent=2))
else:
print("\nNo changes detected between snapshots.")
else:
print("Cannot perform diff: one or both snapshots are missing.")
The DeepDiff call compares normalized_initial_snapshot with normalized_second_snapshot. The ignore_order=True argument is crucial for lists where item order might change but the items themselves are the same (though our normalisation step already handles this for more precise control). verbose_level=2 provides more detail on the changes.
The diff object will contain a detailed breakdown of all differences:
values_changed: For changes in odds values.iterable_item_added: For new selections or markets.iterable_item_removed: For removed selections or markets.
This output is the core of how to diff two bookmaker snapshots for alerting explained. It tells you exactly what shifted.
Step 4: Triggering Alerts on Significant Changes
The raw DeepDiff output is comprehensive, but for alerting, you need to parse it and decide what constitutes a "significant" change. Not every minor odds fluctuation needs an alert. You might only care about:
- Odds moving beyond a certain threshold (e.g., >5% change).
- A specific selection becoming unavailable.
- A new bookmaker offering odds for a key selection.
- Significant price shifts for a particular team.
Here's an example of how to process the DeepDiff output to generate actionable alerts:
def generate_alerts(diff_result: DeepDiff, event_title: str):
"""Parses DeepDiff output and generates human-readable alerts."""
alerts = []
if not diff_result:
return alerts
# Handle odds value changes
if "values_changed" in diff_result:
for path, change in diff_result["values_changed"].items():
if ".odds" in path:
# Extract selection name and bookmaker code from the path
# Example path: root['markets'][0]['selections'][0]['odds']
# This parsing can be complex, a simpler approach is to re-fetch context
# For this example, we'll simplify based on path structure
# A more robust way would involve re-mapping the path to original data structure
# For simplicity here, let's assume a direct path to odds
try:
# Attempt to extract context from the path string
parts = path.split("']['")
market_name_index = -1
selection_name_index = -1
bookmaker_code_index = -1
for i, part in enumerate(parts):
if "market_name" in part: market_name_index = i
if "selection_name" in part: selection_name_index = i
if "bookmaker_code" in part: bookmaker_code_index = i
market_name = "Unknown Market"
selection_name = "Unknown Selection"
bookmaker_code = "Unknown Bookmaker"
if market_name_index != -1 and market_name_index < len(parts) - 1:
market_name = parts[market_name_index + 1].strip("'")
if selection_name_index != -1 and selection_name_index < len(parts) - 1:
selection_name = parts[selection_name_index + 1].strip("'")
if bookmaker_code_index != -1 and bookmaker_code_index < len(parts) - 1:
bookmaker_code = parts[bookmaker_code_index + 1].strip("'")
alerts.append(
f"Odds change for {event_title}: '{selection_name}' ({bookmaker_code}) "
f"in '{market_name}' market changed from {change['old_value']} to {change['new_value']}"
)
except Exception as e:
alerts.append(f"Odds change detected (path: {path}): {change['old_value']} -> {change['new_value']} (Error parsing context: {e})")
# Handle new selections/bookmakers
if "iterable_item_added" in diff_result:
for path, added_item in diff_result["iterable_item_added"].items():
if "selections" in path:
# Again, context extraction can be tricky. Assume added_item is a dict.
selection_name = added_item.get("selection_name", "Unknown Selection")
bookmaker_code = added_item.get("bookmaker_code", "Unknown Bookmaker")
odds = added_item.get("odds", "N/A")
alerts.append(
f"New selection added for {event_title}: '{selection_name}' ({bookmaker_code}) "
f"with odds {odds}"
)
elif "markets" in path:
market_name = added_item.get("market_name", "Unknown Market")
alerts.append(f"New market added for {event_title}: '{market_name}'")
# Handle removed selections/bookmakers
if "iterable_item_removed" in diff_result:
for path, removed_item in diff_result["iterable_item_removed"].items():
if "selections" in path:
selection_name = removed_item.get("selection_name", "Unknown Selection")
bookmaker_code = removed_item.get("bookmaker_code", "Unknown Bookmaker")
alerts.append(
f"Selection removed for {event_title}: '{selection_name}' ({bookmaker_code})"
)
elif "markets" in path:
market_name = removed_item.get("market_name", "Unknown Market")
alerts.append(f"Market removed for {event_title}: '{market_name}'")
return alerts
# Generate and print alerts
event_title_for_alerts = normalized_initial_snapshot.get("event_title", example_event_id)
alerts = generate_alerts(diff, event_title_for_alerts)
if alerts:
print("\n--- ALERTS ---")
for alert in alerts:
print(alert)
print("--------------")
else:
print("\nNo significant alerts to report.")
This generate_alerts function is a basic example. In a real-world application, you would:
- Define thresholds: Only alert if an odds change is greater than, say, 0.10 (10 pence) or 5%.
- Integrate with notification services: Instead of
print(), send alerts to Slack, email, PagerDuty, or a custom dashboard. - Store historical changes: Keep a log of all detected changes for analysis.
- Handle event lifecycle: Odds for a market might disappear if it's suspended or the event goes in-play (though UK Odds API is pre-match only, bookmakers can still withdraw pre-match markets).
This completes the full cycle of how to diff two bookmaker snapshots for alerting integration. You've fetched the data, prepared it, compared it, and extracted actionable insights.

Common Mistakes
When implementing a system to diff two bookmaker snapshots for alerting, developers often run into similar issues. Avoiding these pitfalls will save you significant debugging time.
- Inconsistent Data Structures: Trying to diff JSON objects where the order of items in arrays is not stable, or where keys are sometimes missing, leads to false positives. Always normalise your data as shown in Step 2.
- Ignoring Market/Selection Status: An odds value might not change, but a market or selection could become
suspendedorinactive. Your diffing logic should account for thestatusfield, as this is a critical betting change. - Overly Aggressive Polling: Fetching data too frequently will quickly exhaust your API rate limits. Plan your polling strategy based on your subscription tier (e.g., 1,000 requests/hour on Starter, 5,000 on Pro). For pre-match football odds JSON, updates every few minutes are usually sufficient.
- Not Handling Missing Data: If a bookmaker or market disappears from a snapshot, your diffing logic needs to gracefully handle
Noneor missing keys, rather than crashing. - Alerting on Insignificant Changes: Not all odds changes are worth an alert. A minor fluctuation of 0.01 might be noise. Define thresholds for what constitutes a "significant" change to avoid alert fatigue.
- Lack of Context in Alerts: A raw diff output is hard to read. Ensure your alerts provide context: which event, which market, which selection, which bookmaker, and the old vs. new value.
- Relying on Scraping for UK Bookmaker Odds: Scraping is inherently brittle. Bookmakers frequently change their website layouts, implement anti-bot measures, and can block your IP. This makes reliable, consistent diffing impossible. Using a dedicated odds API without scraping provides a stable and consistent data source.
Options and Alternatives
When you need to diff two bookmaker snapshots for alerting, you have several approaches. Each comes with its own trade-offs in terms of reliability, effort, and cost.
| Feature / Approach | Manual Scraping | Generic Global Odds APIs | UK Odds API (ukoddsapi.com) |
|---|---|---|---|
| Data Source | Direct websites | Aggregated global feeds | Direct UK bookmakers |
| Reliability | Low (breaks often) | Moderate (coverage varies) | High (stable, normalised) |
| Effort to Build | High (parsers, anti-bot) | Low (API integration) | Low (API integration) |
| UK Bookmaker Coverage | Varies (manual effort) | Often limited for UK-specific | Extensive (27+ UK bookmakers) |
| Data Normalisation | Manual, error-prone | Varies by provider | Built-in, consistent |
| Rate Limits | IP bans, captchas | Varies by plan | Clear, tiered limits |
| Cost | High (dev time, infrastructure) | Varies (often expensive for UK) | Transparent, UK-focused pricing |
| Pre-match Focus | Manual filtering | Often mixed with in-play | Exclusively pre-match |
| Ideal For | Small, personal projects | Broad international data | UK-centric football projects |
Manual scraping is a tempting option for small, ad-hoc tasks, but it quickly becomes a maintenance nightmare. Generic global odds APIs might offer broad coverage but often lack depth for specific UK bookmakers or have inconsistent data structures. A specialised odds API without scraping like ukoddsapi.com is designed specifically for the UK market, providing reliable, normalised pre-match football odds JSON that simplifies the diffing and alerting process considerably.
FAQ
How frequently should I poll for new odds snapshots?
Polling frequency depends on your plan's rate limits and how quickly you need to detect changes. For pre-match football odds JSON, updates every 1-5 minutes are generally sufficient. Rapid sub-minute polling is usually unnecessary for pre-match data and can quickly hit rate limits.
What if a market or selection disappears from the API response?
If a market or selection is present in your initial_snapshot but missing from second_snapshot, DeepDiff will report it as iterable_item_removed. Your alerting logic should explicitly handle these removals, as they represent a significant change (e.g., a bookmaker withdrawing a market).
How can I make my diffing logic more robust against floating-point inaccuracies?
When comparing odds values (which are floats), direct equality checks can sometimes fail due to floating-point representation. Instead of if old_odds == new_odds, consider checking if abs(old_odds - new_odds) < epsilon, where epsilon is a small tolerance (e.g., 0.0001). DeepDiff handles numerical comparisons intelligently, but it's good practice to be aware of this for custom logic.
Is DeepDiff the only way to diff JSON data in Python?
No, DeepDiff is a powerful option, but you could also implement custom recursive diffing functions or use other libraries like jsondiff. DeepDiff is often preferred for its detailed output and handling of complex nested structures, making it ideal for how to diff two bookmaker snapshots for alerting explained scenarios.
Can I use this approach for historical odds data?
Yes, the same diffing principles apply to historical odds data. If you have two historical snapshots, you can compare them to analyse past odds movements. UK Odds API's Pro and Business plans include access to historical odds, allowing you to fetch past data for backtesting and analysis.
Conclusion
Building an effective alerting system for pre-match football odds JSON changes doesn't have to be a battle against inconsistent data or IP bans. By leveraging a reliable UK bookmaker odds API, you can consistently fetch, normalise, and diff two bookmaker snapshots for alerting with precision. This approach allows you to focus on what matters: identifying significant market movements and triggering timely notifications.
For developers looking for a stable and comprehensive source of odds API without scraping, ukoddsapi.com provides the structured data you need to build robust applications.