{"id":4383,"date":"2026-10-01T10:00:00","date_gmt":"2026-10-01T10:00:00","guid":{"rendered":"https:\/\/oddspapi.io\/blog\/?p=4383"},"modified":"2026-09-22T13:47:07","modified_gmt":"2026-09-22T13:47:07","slug":"football-data-api-betting-predictions","status":"publish","type":"post","link":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/","title":{"rendered":"Football Data API: Grade Your Model on 250 Labelled Fixtures"},"content":{"rendered":"<p>A football model needs two things: prices to learn from and results to learn against. Most free feeds hand you one half. The stats feeds carry results and refresh their odds once a day. The odds feeds carry prices and drop the fixture at the final whistle.<\/p>\n<p>OddsPapi carries both on the free tier. <code>\/v4\/historical-odds<\/code> replays every price snapshot a bookmaker posted, and <code>\/v4\/scores<\/code> returns the finished score that labels it. This study runs them together over 250 finished fixtures from the top five European tournaments, played between 1 August and 21 September 2026 and measured on 22 September.<\/p>\n<p>The headline: <strong>all 250 fixtures returned a full-time score<\/strong>, and the de-vigged Pinnacle closing line named the winner on <strong>52.2% of the 249 with a complete closing three-way<\/strong>. Its Brier score of 0.5778 is the number your model has to beat. If you have never run a backtest against price history, <a href=\"https:\/\/oddspapi.io\/blog\/backtest-betting-model-free-historical-odds\/\">start with the walkthrough<\/a> and come back when your pipeline runs.<\/p>\n<h2>Why &#8220;free football data API&#8221; usually means half a dataset<\/h2>\n<p>Search that phrase and you meet two kinds of product. One sells fixtures, lineups and results, then bolts on a pre-match odds field that updates daily. The other sells live prices from a handful of books and forgets the fixture once it finishes. Train on the first and your features are stale. Train on the second and you have nothing to train against.<\/p>\n<p>The label is the expensive half. You can approximate a price from a competitor. You cannot approximate whether Arsenal won.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>What a model needs<\/th>\n<th>Where it comes from<\/th>\n<th>Measured on this sample<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Full-time result label<\/td>\n<td><code>\/v4\/scores<\/code>, <code>periods.result<\/code><\/td>\n<td>250 of 250 fixtures<\/td>\n<\/tr>\n<tr>\n<td>Half-time result label<\/td>\n<td><code>\/v4\/scores<\/code>, <code>periods.p1<\/code><\/td>\n<td>250 of 250 fixtures<\/td>\n<\/tr>\n<tr>\n<td>Price feature<\/td>\n<td><code>\/v4\/historical-odds<\/code>, market 101<\/td>\n<td>249 complete closing three-ways<\/td>\n<\/tr>\n<tr>\n<td>Closing price<\/td>\n<td>Last active snapshot before kick-off<\/td>\n<td>Median 32 seconds before kick-off<\/td>\n<\/tr>\n<tr>\n<td>Margin to strip<\/td>\n<td>Sum of the implied probabilities<\/td>\n<td>Median 2.89% on Pinnacle<\/td>\n<\/tr>\n<tr>\n<td>Stake size behind the price<\/td>\n<td><code>limit<\/code> on each snapshot<\/td>\n<td>Median 10,000 at the close<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>Step 1: pull the finished fixtures<\/h2>\n<p>Filter <code>\/v4\/fixtures<\/code> on <code>tournamentId<\/code>. The filter is undocumented, it works, and it lifts the ten-day range cap that <code>sportId<\/code> alone runs into. Premier League is 17, LaLiga 8, Serie A 23, Bundesliga 35 and Ligue 1 34. Resolve those IDs on name plus category if you build the list yourself, because 35 tournaments in the catalogue are called &#8220;Premier League&#8221;.<\/p>\n<pre class=\"wp-block-code\"><code>import os, time, requests\n\nAPI_KEY = os.environ[\"ODDSPAPI_API_KEY\"]\nBASE = \"https:\/\/api.oddspapi.io\/v4\"\n\nTOURNAMENTS = {17: \"Premier League\", 8: \"LaLiga\", 23: \"Serie A\",\n               35: \"Bundesliga\", 34: \"Ligue 1\"}\n\ndef get(path, **params):\n    \"\"\"One call, with the 429 backoff the API asks for.\"\"\"\n    params[\"apiKey\"] = API_KEY\n    for attempt in range(5):\n        r = requests.get(f\"{BASE}\/{path}\", params=params, timeout=180)\n        if r.status_code == 429:\n            time.sleep(float(r.json()[\"error\"].get(\"retryMs\", 3000)) \/ 1000 + 1)\n            continue\n        r.raise_for_status()\n        return r.json()\n    raise RuntimeError(f\"rate limited on \/{path}\")\n\ndef finished_fixtures(tournament_id, start, end):\n    rows = get(\"fixtures\", sportId=10, tournamentId=tournament_id,\n               **{\"from\": start, \"to\": end})\n    return [f for f in rows if f[\"statusName\"] == \"Finished\"]\n\nfixtures = []\nfor tid, name in TOURNAMENTS.items():\n    rows = finished_fixtures(tid, \"2026-08-01\", \"2026-09-22\")\n    print(f\"{name}: {len(rows)} finished\")\n    fixtures += rows\n    time.sleep(1.3)\n<\/code><\/pre>\n<pre class=\"wp-block-code\"><code>Premier League: 50 finished\nLaLiga: 69 finished\nSerie A: 50 finished\nBundesliga: 36 finished\nLigue 1: 45 finished\n<\/code><\/pre>\n<p>Set <code>to<\/code> to the day after the last day you want. It is a midnight-UTC instant, so a same-day query returns only the fixtures that start at 00:00Z. Finished fixtures come back with <code>hasOdds: false<\/code>, because the live board is gone. Their price history stays queryable, which is the reason this study runs on a free key at all. For the live side of the same endpoint, see the <a href=\"https:\/\/oddspapi.io\/blog\/soccer-fixtures-api\/\">soccer fixtures guide<\/a>.<\/p>\n<p>The <code>get<\/code> helper earns its place on the first run. A 1.1-second gap between <code>\/fixtures<\/code> calls still drew a 429 on the second tournament, and the error body carries a <code>retryMs<\/code> that tells you exactly how long to wait.<\/p>\n<h2>Step 2: label every fixture from \/v4\/scores<\/h2>\n<p>The scores endpoint nests under <code>scores.periods<\/code>, not a top-level <code>periods<\/code> key. Soccer returns three keys: <code>p1<\/code> holds the half-time score, <code>fulltime<\/code> the ninety-minute score, and <code>result<\/code> the final score. On league football the last two agree. On a tie that goes to extra time they diverge, so read <code>result<\/code> for the settled outcome and <code>fulltime<\/code> for the ninety-minute one.<\/p>\n<pre class=\"wp-block-code\"><code>def label(fixture_id):\n    periods = get(\"scores\", fixtureId=fixture_id)[\"scores\"][\"periods\"]\n    full = periods[\"result\"]\n    home, away = full[\"participant1Score\"], full[\"participant2Score\"]\n    half = periods[\"p1\"]\n    return {\n        \"full\": \"H\" if home &gt; away else (\"A\" if away &gt; home else \"D\"),\n        \"half\": \"H\" if half[\"participant1Score\"] &gt; half[\"participant2Score\"]\n                else (\"A\" if half[\"participant2Score\"] &gt; half[\"participant1Score\"] else \"D\"),\n        \"score\": (home, away),\n    }\n\nprint(label(fixtures[0][\"fixtureId\"]))\n<\/code><\/pre>\n<pre class=\"wp-block-code\"><code>{'full': 'H', 'half': 'H', 'score': (3, 0)}\n<\/code><\/pre>\n<p>All 250 fixtures returned a score, with no retries and no errors. The outcome split came out <strong>43.2% home, 24.8% draw, 32.0% away<\/strong>. That split is your first baseline. A model that predicts the season base rate every week scores a Brier of 0.6486, and anything worse than that carries no information.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Tournament<\/th>\n<th>Fixtures<\/th>\n<th>Home<\/th>\n<th>Draw<\/th>\n<th>Away<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Premier League<\/td>\n<td>50<\/td>\n<td>36.0%<\/td>\n<td>32.0%<\/td>\n<td>32.0%<\/td>\n<\/tr>\n<tr>\n<td>LaLiga<\/td>\n<td>69<\/td>\n<td>44.9%<\/td>\n<td>24.6%<\/td>\n<td>30.4%<\/td>\n<\/tr>\n<tr>\n<td>Serie A<\/td>\n<td>50<\/td>\n<td>44.0%<\/td>\n<td>18.0%<\/td>\n<td>38.0%<\/td>\n<\/tr>\n<tr>\n<td>Bundesliga<\/td>\n<td>36<\/td>\n<td>52.8%<\/td>\n<td>19.4%<\/td>\n<td>27.8%<\/td>\n<\/tr>\n<tr>\n<td>Ligue 1<\/td>\n<td>45<\/td>\n<td>40.0%<\/td>\n<td>28.9%<\/td>\n<td>31.1%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Six weeks is a small window, and the spread across tournaments shows it. Bundesliga home sides won 52.8% and Premier League home sides won 36.0%. Pull a full season before you treat any of those five rows as a prior.<\/p>\n<p>The half-time key earns its place too. Someone led at the break in 149 of the 250 fixtures, and that side <strong>failed to win 40 of them, 26.8%<\/strong>. Train a half-time model on <code>p1<\/code> and grade it on <code>result<\/code>, and the same two calls give you a second dataset.<\/p>\n<h2>Step 3: pull the closing price as your feature<\/h2>\n<p><code>\/v4\/historical-odds<\/code> takes three bookmaker slugs per call and returns every snapshot each one posted. This study reads Pinnacle alone, because Pinnacle is the sharpest anchor in the top tier and its closing line is the standard benchmark. A wider consensus shifts the number: a 3-book closing line sits 0.67 percentage points from a 176-book one, measured in <a href=\"https:\/\/oddspapi.io\/blog\/how-many-bookmakers-backtest\/\">the book-count study<\/a>.<\/p>\n<p>Two traps live in this call. The historical endpoint&#8217;s top-level key is <code>bookmakers<\/code>, where the live endpoint uses <code>bookmakerOdds<\/code>. And <code>players[\"0\"]<\/code> holds a list of snapshots here, where the live endpoint makes it a dict.<\/p>\n<pre class=\"wp-block-code\"><code>LEGS = {\"101\": \"H\", \"102\": \"D\", \"103\": \"A\"}   # market 101 = Full Time Result\n\ndef closing_1x2(fixture_id, start_time, book=\"pinnacle\"):\n    data = get(\"historical-odds\", fixtureId=fixture_id, bookmakers=book)\n    market = data[\"bookmakers\"][book][\"markets\"].get(\"101\")\n    if not market:\n        return None\n    prices = {}\n    for outcome_id, leg in LEGS.items():\n        snaps = market[\"outcomes\"][outcome_id][\"players\"][\"0\"]\n        pre = [s for s in snaps if s[\"createdAt\"] &lt; start_time and s[\"active\"]]\n        if not pre:\n            return None\n        prices[leg] = max(pre, key=lambda s: s[\"createdAt\"])[\"price\"]\n    return prices\n\nf = fixtures[0]\nprint(f[\"participant1Name\"], \"v\", f[\"participant2Name\"], f[\"startTime\"])\nprint(closing_1x2(f[\"fixtureId\"], f[\"startTime\"]))\n<\/code><\/pre>\n<pre class=\"wp-block-code\"><code>Arsenal FC v Coventry City 2026-08-21T19:00:00.000Z\n{'H': 1.21, 'D': 7.15, 'A': 16.0}\n<\/code><\/pre>\n<p>Both filters matter. Snapshots keep arriving after kick-off, and a suspended price sits in the list with its last number and an <code>active<\/code> flag of false. Skip the filters and you grade your model against an in-play price. Across the 249 fixtures with a complete closing three-way, the last active snapshot landed a median <strong>32 seconds before kick-off<\/strong>, with the tenth percentile at 7 seconds and the ninetieth at 195.<\/p>\n<p>One fixture of the 250 returned 404 from <code>\/historical-odds<\/code>. Catch the error and move on rather than letting it kill the loop.<\/p>\n<h2>Step 4: strip the margin<\/h2>\n<p>The three closing prices imply more than 100% probability. The excess is the bookmaker&#8217;s margin, and it has to come out before the number means anything. Proportional de-vig divides each implied probability by the total. The power method raises each one to a common exponent, which takes more weight off the longshot.<\/p>\n<pre class=\"wp-block-code\"><code>def fair_proportional(prices):\n    implied = {leg: 1 \/ price for leg, price in prices.items()}\n    total = sum(implied.values())\n    return {leg: p \/ total for leg, p in implied.items()}, total - 1\n\ndef brier(probs, actual):\n    return sum((probs[leg] - (1 if actual == leg else 0)) ** 2 for leg in \"HDA\")\n\nprices = closing_1x2(f[\"fixtureId\"], f[\"startTime\"])\nfair, overround = fair_proportional(prices)\nprint({k: round(v, 4) for k, v in fair.items()}, f\"overround {overround:.2%}\")\n<\/code><\/pre>\n<pre class=\"wp-block-code\"><code>{'H': 0.8033, 'D': 0.1359, 'A': 0.0608} overround 2.88%\n<\/code><\/pre>\n<p>Pinnacle&#8217;s median closing overround across the sample was <strong>2.89%<\/strong>, inside a range of 2.26% to 5.14%. Pinnacle also publishes a stake <code>limit<\/code> on every snapshot, and the median closing limit was 10,000. Most books leave that field null, so check it before you do arithmetic on it.<\/p>\n<h2>What the closing line scores<\/h2>\n<p>Run the loop over all 249 fixtures and grade the de-vigged closing probabilities against the labels. Lower is better on both scores.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Predictor<\/th>\n<th>Brier score<\/th>\n<th>Log loss<\/th>\n<th>Favourite correct<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Season base rate (43.2 \/ 24.8 \/ 32.0)<\/td>\n<td>0.6486<\/td>\n<td>1.0717<\/td>\n<td>43.4%<\/td>\n<\/tr>\n<tr>\n<td>Pinnacle opening line<\/td>\n<td>0.5875<\/td>\n<td>0.9867<\/td>\n<td>50.2%<\/td>\n<\/tr>\n<tr>\n<td><strong>Pinnacle closing line<\/strong><\/td>\n<td><strong>0.5778<\/strong><\/td>\n<td><strong>0.9710<\/strong><\/td>\n<td><strong>52.2%<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Closing line, power de-vig<\/td>\n<td>0.5776<\/td>\n<td>0.9701<\/td>\n<td>52.2%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>The closing line beat the opening line by <strong>1.7% on Brier and 1.6% on log loss<\/strong>, and it converted five more favourites over the same 249 fixtures. Weeks of money moved the price that far. The gap between the base rate and the close is larger: <strong>10.9% on Brier<\/strong>. That gap is the whole value of knowing anything about the fixture, and your model has to clear it before it clears the margin.<\/p>\n<p>The two de-vig methods finished within 0.0002 of each other on Brier. Pick either one and stay consistent. The choice matters more on three-way markets with a wide margin, where the tie leg moves by more than a point between methods.<\/p>\n<h3>The closing line is well calibrated<\/h3>\n<p>Bucket every de-vigged probability and compare it against how often that outcome happened. Each fixture contributes three legs, so 249 fixtures give 747 rows.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Predicted band<\/th>\n<th>Legs<\/th>\n<th>Mean predicted<\/th>\n<th>Realised<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>0.0 to 0.1<\/td>\n<td>28<\/td>\n<td>6.5%<\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td>0.1 to 0.2<\/td>\n<td>106<\/td>\n<td>15.6%<\/td>\n<td>17.9%<\/td>\n<\/tr>\n<tr>\n<td>0.2 to 0.3<\/td>\n<td>271<\/td>\n<td>25.7%<\/td>\n<td>26.9%<\/td>\n<\/tr>\n<tr>\n<td>0.3 to 0.4<\/td>\n<td>136<\/td>\n<td>33.8%<\/td>\n<td>30.1%<\/td>\n<\/tr>\n<tr>\n<td>0.4 to 0.5<\/td>\n<td>85<\/td>\n<td>44.7%<\/td>\n<td>48.2%<\/td>\n<\/tr>\n<tr>\n<td>0.5 to 0.6<\/td>\n<td>59<\/td>\n<td>54.6%<\/td>\n<td>44.1%<\/td>\n<\/tr>\n<tr>\n<td>0.6 to 0.7<\/td>\n<td>29<\/td>\n<td>65.1%<\/td>\n<td>69.0%<\/td>\n<\/tr>\n<tr>\n<td>0.7 to 0.8<\/td>\n<td>20<\/td>\n<td>74.9%<\/td>\n<td>80.0%<\/td>\n<\/tr>\n<tr>\n<td>0.8 to 0.9<\/td>\n<td>11<\/td>\n<td>82.7%<\/td>\n<td>100.0%<\/td>\n<\/tr>\n<tr>\n<td>0.9 to 1.0<\/td>\n<td>2<\/td>\n<td>91.6%<\/td>\n<td>100.0%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>The three fullest bands track their predictions within 1.3 percentage points. The 0.5 to 0.6 band came in ten points light on 59 legs, which is the size of gap six weeks of football produces by chance. Run the same table over a full season before you read a bias into any single row.<\/p>\n<h2>The full loop<\/h2>\n<p>Two calls per fixture, one sleep each, and the whole study fits on a free key.<\/p>\n<pre class=\"wp-block-code\"><code>import json\n\nrows = []\nfor f in fixtures:\n    fid, start = f[\"fixtureId\"], f[\"startTime\"]\n    try:\n        prices = closing_1x2(fid, start)\n        time.sleep(4.6)              # \/historical-odds needs the wider gap\n        if not prices:\n            continue\n        y = label(fid)\n        time.sleep(1.3)\n    except (requests.HTTPError, RuntimeError):\n        continue\n    fair, overround = fair_proportional(prices)\n    rows.append({\"fixtureId\": fid, \"tournament\": f[\"tournamentName\"],\n                 \"closing\": prices, \"fair\": fair, \"overround\": overround,\n                 \"result\": y[\"full\"], \"half\": y[\"half\"], \"score\": y[\"score\"]})\n\njson.dump(rows, open(\"labelled.json\", \"w\"), indent=1)\nprint(len(rows), \"labelled fixtures\")\nprint(\"closing-line Brier:\",\n      round(sum(brier(r[\"fair\"], r[\"result\"]) for r in rows) \/ len(rows), 4))\n<\/code><\/pre>\n<p>Sleep 4.6 seconds after a successful <code>\/historical-odds<\/code> call and about 1.3 seconds elsewhere. Concurrency gets almost everything rate limited, whatever the worker count. Retry a failed batch once, one slug at a time. The 250-fixture run above took 25 minutes and returned 249 usable rows.<\/p>\n<h2>Where to take it next<\/h2>\n<p>You now hold features and labels in one file, which used to need two vendors. Three directions from here:<\/p>\n<ul>\n<li><strong>Beat the close.<\/strong> Grade your own probabilities against 0.5778. Anything above that is worse than reading the market.<\/li>\n<li><strong>Add books.<\/strong> Pinnacle is one opinion out of 350+ in the catalogue. Walk it three slugs at a time, deduplicate on the price tuple, and check your <a href=\"https:\/\/oddspapi.io\/blog\/team-ratings-from-closing-odds\/\">team ratings<\/a> against the consensus.<\/li>\n<li><strong>Add markets.<\/strong> Market 101 is the three-way. The same call carries totals, Asian handicaps and correct score, which is how you <a href=\"https:\/\/oddspapi.io\/blog\/poisson-soccer-model-python\/\">reconstruct the score distribution<\/a>.<\/li>\n<\/ul>\n<p>When you want to run the model forward instead of backward, the WebSocket feed pushes the live board under the same market IDs. For the live pre-match board on these five tournaments, see the <a href=\"https:\/\/oddspapi.io\/blog\/football-odds-api-soccer-data\/\">football odds API guide<\/a>.<\/p>\n<h2>FAQ<\/h2>\n<h3>Does the OddsPapi free tier include match results?<\/h3>\n<p>Yes. <code>\/v4\/scores<\/code> returns <code>p1<\/code>, <code>fulltime<\/code> and <code>result<\/code> for a finished fixture. All 250 top-five-league fixtures tested on 22 September 2026 returned a full-time score.<\/p>\n<h3>How far back does the free price history go?<\/h3>\n<p>About eight months on Premier League fixtures. A January 2026 fixture still replays, and earlier ones return 404. Retention reaches deeper than one season on the tournaments tested.<\/p>\n<h3>Which market ID is the 1X2 in soccer?<\/h3>\n<p>Market <code>101<\/code>, Full Time Result, with outcomes 101 home, 102 draw and 103 away. Resolve names from <code>\/v4\/markets<\/code> rather than hardcoding a set, because the same bet ships under more than one ID in several sports.<\/p>\n<h3>How accurate is a bookmaker closing line at predicting football results?<\/h3>\n<p>Across 249 top-five-league fixtures played between 1 August and 21 September 2026, the de-vigged Pinnacle closing line scored a Brier of 0.5778 and named the winner 52.2% of the time. The season base rate scored 0.6486 and 43.4%.<\/p>\n<h3>Can one API supply both features and labels?<\/h3>\n<p>Yes. <code>\/v4\/historical-odds<\/code> supplies the price features and <code>\/v4\/scores<\/code> supplies the labels, on the same fixture ID and the same free key. Two calls per fixture.<\/p>\n<h3>What is the difference between the fulltime and result period keys?<\/h3>\n<p>On soccer, <code>fulltime<\/code> holds the ninety-minute score and <code>result<\/code> holds the settled final score. They agree on league football and diverge on a tie decided in extra time, so grade ninety-minute markets on <code>fulltime<\/code>.<\/p>\n<h2>Get the data<\/h2>\n<p>The free key covers 69 sports, 350+ bookmakers in the catalogue and the full price history used above. <a href=\"https:\/\/oddspapi.io\/en\/sign-up\">Get a free API key<\/a> and run the loop on your own tournament list. If you want the wider board instead of one sharp book, the <a href=\"https:\/\/oddspapi.io\/blog\/free-odds-api-350-bookmakers\/\">free odds API guide<\/a> covers catalogue discovery.<\/p>\n<p><script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Does the OddsPapi free tier include match results?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. \/v4\/scores returns p1, fulltime and result for a finished fixture. All 250 top-five-league fixtures tested on 22 September 2026 returned a full-time score.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How far back does the free price history go?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"About eight months on Premier League fixtures. A January 2026 fixture still replays. Retention reaches deeper than one season on the tournaments tested.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Which market ID is the 1X2 in soccer?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Market 101, Full Time Result, with outcomes 101 home, 102 draw and 103 away. Resolve names from \/v4\/markets rather than hardcoding a set, because the same bet ships under more than one ID in several sports.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How accurate is a bookmaker closing line at predicting football results?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Across 249 top-five-league fixtures played between 1 August and 21 September 2026, the de-vigged Pinnacle closing line scored a Brier of 0.5778 and named the winner 52.2% of the time. The season base rate scored 0.6486 and 43.4%.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can one API supply both features and labels?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. \/v4\/historical-odds supplies the price features and \/v4\/scores supplies the labels, on the same fixture ID and the same free key. Two calls per fixture.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What is the difference between the fulltime and result period keys?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"On soccer, fulltime holds the ninety-minute score and result holds the settled final score. They agree on league football and diverge on a tie decided in extra time, so grade ninety-minute markets on fulltime.\"\n      }\n    }\n  ]\n}\n<\/script><\/p>\n<p><!--\nFocus Keyphrase: football data api\nSEO Title: Football Data API: Grade Your Model on 250 Labelled Fixtures\nMeta Description: The free football data API that returns labels too: \/v4\/scores graded all 250 fixtures, and Pinnacle's de-vigged close called 52.2% of them.\nSlug: football-data-api-betting-predictions\n--><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The free football data API that returns labels too: \/v4\/scores graded all 250 fixtures, and Pinnacle&#8217;s de-vigged close called 52.2% of them.<\/p>\n","protected":false},"author":2,"featured_media":4384,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[30,4,9,11,15],"class_list":["post-4383","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to-guides","tag-backtesting","tag-historical-odds","tag-odds-api","tag-python","tag-soccer"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Football Data API: Grade Your Model on 250 Labelled Fixtures | OddsPapi Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Football Data API: Grade Your Model on 250 Labelled Fixtures | OddsPapi Blog\" \/>\n<meta property=\"og:description\" content=\"The free football data API that returns labels too: \/v4\/scores graded all 250 fixtures, and Pinnacle&#039;s de-vigged close called 52.2% of them.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/\" \/>\n<meta property=\"og:site_name\" content=\"OddsPapi Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-01T10:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"675\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/webp\" \/>\n<meta name=\"author\" content=\"Odds API Writer\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/oddspapi.io\/logo-v2.webp\" \/>\n<meta name=\"twitter:creator\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:site\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Odds API Writer\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/\"},\"author\":{\"name\":\"Odds API Writer\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\"},\"headline\":\"Football Data API: Grade Your Model on 250 Labelled Fixtures\",\"datePublished\":\"2026-10-01T10:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/\"},\"wordCount\":1662,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp\",\"keywords\":[\"Backtesting\",\"historical odds\",\"Odds API\",\"Python\",\"Soccer\"],\"articleSection\":[\"How To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/\",\"name\":\"Football Data API: Grade Your Model on 250 Labelled Fixtures | OddsPapi Blog\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp\",\"datePublished\":\"2026-10-01T10:00:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp\",\"width\":1200,\"height\":675,\"caption\":\"Football Data API featured image\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/oddspapi.io\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Football Data API: Grade Your Model on 250 Labelled Fixtures\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"name\":\"OddsPapi\",\"description\":\"Sports Odds API Tutorials &amp; Guides\",\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"alternateName\":\"Odds Papi\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/oddspapi.io\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\",\"name\":\"OddsPapi\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"width\":135,\"height\":135,\"caption\":\"OddsPapi\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/oddspapiapi\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\",\"name\":\"Odds API Writer\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"caption\":\"Odds API Writer\"},\"url\":\"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Football Data API: Grade Your Model on 250 Labelled Fixtures | OddsPapi Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/","og_locale":"en_US","og_type":"article","og_title":"Football Data API: Grade Your Model on 250 Labelled Fixtures | OddsPapi Blog","og_description":"The free football data API that returns labels too: \/v4\/scores graded all 250 fixtures, and Pinnacle's de-vigged close called 52.2% of them.","og_url":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/","og_site_name":"OddsPapi Blog","article_published_time":"2026-10-01T10:00:00+00:00","og_image":[{"width":1200,"height":675,"url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp","type":"image\/webp"}],"author":"Odds API Writer","twitter_card":"summary_large_image","twitter_image":"https:\/\/oddspapi.io\/logo-v2.webp","twitter_creator":"@oddspapiapi","twitter_site":"@oddspapiapi","twitter_misc":{"Written by":"Odds API Writer","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#article","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/"},"author":{"name":"Odds API Writer","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13"},"headline":"Football Data API: Grade Your Model on 250 Labelled Fixtures","datePublished":"2026-10-01T10:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/"},"wordCount":1662,"commentCount":0,"publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp","keywords":["Backtesting","historical odds","Odds API","Python","Soccer"],"articleSection":["How To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/","url":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/","name":"Football Data API: Grade Your Model on 250 Labelled Fixtures | OddsPapi Blog","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp","datePublished":"2026-10-01T10:00:00+00:00","breadcrumb":{"@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#primaryimage","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/09\/football-data-api-betting-predictions.webp","width":1200,"height":675,"caption":"Football Data API featured image"},{"@type":"BreadcrumbList","@id":"https:\/\/oddspapi.io\/blog\/football-data-api-betting-predictions\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/oddspapi.io\/blog\/"},{"@type":"ListItem","position":2,"name":"Football Data API: Grade Your Model on 250 Labelled Fixtures"}]},{"@type":"WebSite","@id":"https:\/\/oddspapi.io\/blog\/#website","url":"https:\/\/oddspapi.io\/blog\/","name":"OddsPapi","description":"Sports Odds API Tutorials &amp; Guides","publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"alternateName":"Odds Papi","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/oddspapi.io\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/oddspapi.io\/blog\/#organization","name":"OddsPapi","url":"https:\/\/oddspapi.io\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","width":135,"height":135,"caption":"OddsPapi"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/oddspapiapi"]},{"@type":"Person","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13","name":"Odds API Writer","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","caption":"Odds API Writer"},"url":"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/"}]}},"_links":{"self":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/4383","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/comments?post=4383"}],"version-history":[{"count":1,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/4383\/revisions"}],"predecessor-version":[{"id":4385,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/4383\/revisions\/4385"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media\/4384"}],"wp:attachment":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media?parent=4383"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/categories?post=4383"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/tags?post=4383"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}