Soccer Data has two football odds endpoints: GET /matches/{match_id}/odds returns pre-match prices, with an opening and a latest (closing) price for every selection, and GET /matches/{match_id}/odds/live returns in-play prices while a match is being played. Prices come from Bet365, Paddy Power, BetMGM UK, Pinnacle and Betfair Exchange as decimal odds, one entry per bookmaker, and both endpoints are included in the Pro and Business plans.
The two odds endpoints
| Endpoint | What it returns | Bookmakers | When to call it |
|---|---|---|---|
/matches/{match_id}/odds |
Pre-match prices, with opening and last_seen for each selection |
Bet365, Paddy Power, BetMGM UK, Pinnacle, Betfair Exchange | Before kickoff, or after it to read closing prices |
/matches/{match_id}/odds/live |
In-play prices, with one live price for each selection |
Bet365, Paddy Power, BetMGM UK, Betfair Exchange | While the match's live_odds_available is true |
Every price is in decimal odds, sent as a string such as "1.380". If your users expect fractional or American odds, convert them in your app. Send your API key in the X-Api-Key header, from your server.
Which plans include odds
Odds are part of the Pro and Business plans:
| Plan | Requests / day | Per minute | Live data refreshed (normally) | Odds |
|---|---|---|---|---|
| Free | 50 | 10 | 5 min | — |
| Pro | 1,000 | 60 | 15 s | Yes |
| Business | 10,000 | 300 | 15 s | Yes |
A key on a plan without odds gets 403 with the code PLAN_UPGRADE_REQUIRED:
{
"error": {
"code": "PLAN_UPGRADE_REQUIRED",
"message": "This endpoint needs the Pro or Business plan. Upgrade from your dashboard to use it.",
"status_code": 403
}
}
That 403 doesn't count against your daily quota. Plans are monthly and listed on the pricing page.
Pre-match odds with opening and closing prices
This asks for Bet365 and Pinnacle prices on Arsenal v Leeds United in the Premier League:
curl "https://api.soccer-data.co/v1/matches/match_yoeml0e6ae37/odds?bookmaker=bet365,pinnacle" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": {
"match_id": "match_yoeml0e6ae37",
"bookmakers": [
{
"bookmaker": "Bet365",
"markets": {
"match_odds": {
"home": { "opening": "1.380", "last_seen": "1.380" },
"draw": { "opening": "4.500", "last_seen": "5.000" },
"away": { "opening": "7.500", "last_seen": "7.500" }
}
}
},
{
"bookmaker": "Pinnacle",
"markets": {
"match_odds": {
"home": { "opening": "1.377", "last_seen": "1.413" },
"draw": { "opening": "4.910", "last_seen": "4.870" },
"away": { "opening": "7.620", "last_seen": "7.760" }
}
}
}
]
}
}
Every selection carries two prices:
opening: the first price we saw, ornullwhen no opening price was recorded.last_seen: the latest price. Once the match has kicked off, this is the closing price.
The endpoint works for upcoming and finished matches alike, so you can follow a line as it moves before kickoff and look back at closing lines afterwards.
Markets
The common keys are match_odds (home, draw, away), btts (both teams to score, yes or no) and total_goals (one key per line, such as "2.5", each with over and under). Depending on the bookmaker and the match, you'll also find first_half_result, double_chance, draw_no_bet, asian_handicap, handicap_result, correct_score, match_corners, total_cards, team_total_goals, to_qualify for knockout ties, and penalty markets. On bigger fixtures, Bet365 adds markets for shots, throw-ins, goal kicks, tackles and offsides.
A market only appears when that bookmaker prices it, so check that a key exists before you read it.
The bookmaker filter
The pre-match endpoint takes one parameter, bookmaker: one or more slugs, comma-separated.
| Bookmaker | Slug | Pre-match | In-play |
|---|---|---|---|
| Bet365 | bet365 |
Yes | Yes |
| Paddy Power | paddy-power |
Yes | Yes |
| BetMGM UK | betmgm-uk |
Yes | Yes |
| Pinnacle | pinnacle |
Yes | No |
| Betfair Exchange | betfair-exchange |
Yes | Yes |
- Leave
bookmakerout to get every bookmaker that prices the match. - An unknown slug returns
400. - If the match has odds, but none from the bookmakers you asked for, you get
200with an emptybookmakerslist. - The in-play endpoint takes no parameters, so sending
bookmakerto it returns400 UNKNOWN_PARAMETER. Filter itsbookmakerslist in your code instead.
In-play odds
Call the live endpoint only while the match's live_odds_available flag is true:
curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/odds/live" \
-H "X-Api-Key: YOUR_API_KEY"
{
"data": {
"match_id": "match_0i2m68v7oaxa",
"bookmakers": [
{
"bookmaker": "Bet365",
"markets": {
"match_odds": {
"home": { "live": "3.400" },
"draw": { "live": "3.100" },
"away": { "live": "2.250" }
}
}
},
{
"bookmaker": "Betfair Exchange",
"markets": {
"match_odds": {
"home": {
"live": "3.650",
"available_to_back": [{ "price": 3.65, "size": 1209.44 }, { "price": 3.6, "size": 1795.66 }],
"available_to_lay": [{ "price": 3.7, "size": 480.78 }, { "price": 3.75, "size": 2184.81 }]
}
}
}
}
]
}
}
Each selection has a single live price. Betfair Exchange also gives the money waiting in the market: available_to_back and available_to_lay, up to three levels each, best price first, each with a price and a size. Betfair Exchange's pre-match prices carry the same two lists.
Things to plan for when you poll:
- Bookmakers suspend and reopen markets during play, so a market or a whole bookmaker can be missing from one response and back in the next.
- In-play prices are normally refreshed every 15 seconds on Pro and Business. Polling faster returns the same prices and still spends requests.
- If a fresh answer can't be fetched, you may get an older copy marked
X-Cache: STALE, with its age in seconds inX-Cache-Age. - After full time the endpoint returns
404. Stop polling then.
Check odds coverage before you call
Matches and competitions carry flags, so you don't have to ask blindly:
| Flag | On | Means |
|---|---|---|
odds_available |
Match | Pre-match prices exist. Always false for postponed and cancelled matches. |
live_odds_available |
Match | In-play prices exist right now. |
odds_available |
Competition | Its matches have prices. |
live_odds_available |
Competition | At least one of its matches has in-play prices right now. |
The flags come back on /matches, /matches/{match_id} and /competitions/{competition_id}. A match without prices of the kind you ask for returns 404 from the odds endpoints, and that 404 counts as a request, so checking the flag first saves quota.
To see which competitions and seasons have odds, use the coverage endpoints. /coverage/leagues takes data_type=opening_odds, closing_odds or odds (an older name for closing_odds):
curl "https://api.soccer-data.co/v1/coverage/leagues?data_type=opening_odds" \
-H "X-Api-Key: YOUR_API_KEY"
/coverage/leagues/{competition_id} breaks one competition down season by season, with covered_events and coverage_pct for opening_odds and closing_odds:
curl "https://api.soccer-data.co/v1/coverage/leagues/competition_705uv6jm8myn" \
-H "X-Api-Key: YOUR_API_KEY"
Each season in data.seasons looks like this (trimmed to the odds keys):
{
"id": "season_98l95mxugqs9",
"year": "25/26",
"status": "complete",
"data_types": {
"opening_odds": { "available": false, "covered_events": 0, "coverage_pct": 0 },
"closing_odds": { "available": true, "covered_events": 380, "coverage_pct": 100 }
}
}
Odds history is thinner than match history. From the 2026/27 season, matches can have opening and closing prices from all five bookmakers. Before that, most matches only have Bet365 closing prices: opening is null and last_seen holds the closing price. In the Premier League, for example, closing odds go back to 2015/16 and opening odds start in 2026/27.
Example: compare prices across bookmakers
This Python script fetches the pre-match odds for one match and prints the best match_odds price for each outcome, with the bookmaker offering it and how far that price moved from its opening:
import os, requests
API_URL = "https://api.soccer-data.co/v1"
def best_prices(match_id, bookmakers="bet365,paddy-power,betmgm-uk,pinnacle,betfair-exchange"):
res = requests.get(
f"{API_URL}/matches/{match_id}/odds",
params={"bookmaker": bookmakers},
headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]},
)
if res.status_code == 404:
return {} # no pre-match odds for this match
res.raise_for_status()
best = {}
for book in res.json()["data"]["bookmakers"]:
market = book["markets"].get("match_odds")
if not market:
continue # this bookmaker doesn't price the result market
for outcome in ("home", "draw", "away"):
price = market.get(outcome)
if not price:
continue
last = float(price["last_seen"])
if outcome not in best or last > best[outcome]["price"]:
opening = price["opening"]
best[outcome] = {
"price": last,
"bookmaker": book["bookmaker"],
"moved": None if opening is None else round(last - float(opening), 3),
}
return best
for outcome, row in best_prices("match_yoeml0e6ae37", "bet365,pinnacle").items():
print(f'{outcome}: {row["price"]:.3f} at {row["bookmaker"]} (moved {row["moved"]})')
For the Bet365 and Pinnacle prices in the example response above, it prints:
home: 1.413 at Pinnacle (moved 0.036)
draw: 5.000 at Bet365 (moved 0.5)
away: 7.760 at Pinnacle (moved 0.14)
To run this over a round of fixtures, list matches with /matches using competition_id, date_from and date_to, and skip any match whose odds_available is false.
Next steps
- Odds guide: markets, flags and history in full.
- Pre-match odds reference and live odds reference: every field.
- Coverage: which competitions and seasons have odds.
- Still deciding on a provider? The checklist covers how to check odds coverage before you commit.
- Create an account and get an API key.
Frequently asked questions
Which football API includes bookmaker odds?
Soccer Data includes pre-match and in-play football odds from Bet365, Paddy Power, BetMGM UK, Pinnacle and Betfair Exchange. Pre-match prices come from GET /matches/{match_id}/odds and in-play prices from GET /matches/{match_id}/odds/live, on the Pro and Business plans.
How do I get opening and closing odds for a football match?
Call Soccer Data's GET /matches/{match_id}/odds. Every selection has an opening price (the first one seen, or null if none was recorded) and a last_seen price, which becomes the closing price once the match kicks off.
Can I get live in-play football odds from an API?
Yes. Soccer Data's GET /matches/{match_id}/odds/live returns a live price per selection from Bet365, Paddy Power, BetMGM UK and Betfair Exchange while a match is in play. Call it while the match's live_odds_available flag is true; prices are normally refreshed every 15 seconds.
How do I filter football odds by bookmaker?
Add the bookmaker parameter to Soccer Data's pre-match odds endpoint with one or more slugs, comma-separated: bet365, paddy-power, betmgm-uk, pinnacle or betfair-exchange. Leave it out to get every bookmaker; an unknown slug returns 400.
Does the Soccer Data odds API include Betfair Exchange back and lay prices?
Yes. In Soccer Data's pre-match and in-play odds, Betfair Exchange prices include available_to_back and available_to_lay: up to three levels each, best price first, each with a price and the size of money available.
How far back does football odds history go in Soccer Data?
From the 2026/27 season, matches can have opening and closing prices from all five bookmakers. Earlier seasons mostly have Bet365 closing prices only; check each competition season by season with GET /coverage/leagues/{competition_id}.