{"id":2824,"date":"2026-04-13T10:00:00","date_gmt":"2026-04-13T10:00:00","guid":{"rendered":"https:\/\/oddspapi.io\/blog\/?p=2824"},"modified":"2026-04-11T14:57:28","modified_gmt":"2026-04-11T14:57:28","slug":"historical-odds-csv-excel-backtesting","status":"publish","type":"post","link":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/","title":{"rendered":"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API)"},"content":{"rendered":"<p>You need historical odds for backtesting. The options look bleak: OddsJam charges <strong>$299\/month<\/strong>, SportsGameOdds walls it behind their Enterprise plan, and The Odds API historical endpoint costs a premium tier just to query one sport at a time. So you give up and try to scrape Bet365&#8217;s archive into an Excel file. That dies within a week.<\/p>\n<p>Here&#8217;s the workaround: OddsPapi&#8217;s <code>\/historical-odds<\/code> endpoint is free, covers every sport, and returns the full price history for each bookmaker \u2014 every line move, timestamped. This tutorial shows you how to pull it, flatten the nested JSON, and dump it straight to CSV or Excel so you can load it into pandas, a notebook, or even a spreadsheet and start backtesting.<\/p>\n<p>All code below was tested against the live API on April 11, 2026.<\/p>\n<h2>Why Historical Odds Data Is So Hard to Get<\/h2>\n<p>Three problems keep killing backtest projects before they start:<\/p>\n<ol>\n<li><strong>Paywalls.<\/strong> The big historical providers treat the data as a premium product. $299\/month to OddsJam or $500+\/month to Sportradar-backed feeds just to answer the question &#8220;what did Pinnacle close at last Tuesday?&#8221;<\/li>\n<li><strong>Per-sport gatekeeping.<\/strong> Even when you pay, you usually pay <em>per sport<\/em>. Want NFL + Premier League + ATP tennis in one dataset? That&#8217;s three subscriptions.<\/li>\n<li><strong>Nested JSON hell.<\/strong> The providers that do give you historical data hand it back as deeply nested objects \u2014 <code>bookmakers \u2192 markets \u2192 outcomes \u2192 players \u2192 price_history<\/code> \u2014 and leave you to unpack it before you can even load a DataFrame.<\/li>\n<\/ol>\n<p>OddsPapi solves all three in one call. Free historical data is on the free tier. One endpoint covers every sport we ingest (soccer, NFL, tennis, esports, cricket, volleyball \u2014 <strong>59 sports<\/strong>). And with ~30 lines of Python below, you&#8217;ll flatten the nested response into a clean tabular CSV or Excel file.<\/p>\n<h2>Historical Odds Providers Compared (2026)<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Provider<\/th>\n<th>Historical Cost<\/th>\n<th>Sports Covered<\/th>\n<th>Export Format<\/th>\n<th>Price History<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>OddsPapi<\/strong><\/td>\n<td><strong>Free<\/strong><\/td>\n<td>59 sports<\/td>\n<td>JSON (flatten to CSV\/Excel with Python)<\/td>\n<td>\u2705 Every line move, timestamped<\/td>\n<\/tr>\n<tr>\n<td>The Odds API<\/td>\n<td>$119\/mo+ (historical endpoint, paid tier)<\/td>\n<td>Per-sport plans<\/td>\n<td>JSON<\/td>\n<td>Single snapshots per timestamp<\/td>\n<\/tr>\n<tr>\n<td>SportsGameOdds<\/td>\n<td>Enterprise plan only<\/td>\n<td>Major US sports<\/td>\n<td>JSON<\/td>\n<td>Limited<\/td>\n<\/tr>\n<tr>\n<td>OddsJam<\/td>\n<td>$299\/mo+<\/td>\n<td>Major sports<\/td>\n<td>CSV export (built-in)<\/td>\n<td>\u2705 Available<\/td>\n<\/tr>\n<tr>\n<td>Bet365 archive scraping<\/td>\n<td>&#8220;Free&#8221; (until they block you)<\/td>\n<td>Bet365 only<\/td>\n<td>HTML (good luck)<\/td>\n<td>\u274c Not exposed<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>If you already have an OddsJam subscription and just want a CSV export, the built-in tool is fine. If you want <em>free<\/em> historical data across every sport with every price tick, read on.<\/p>\n<h2>Step 1: Set Up the API and Discover Sports<\/h2>\n<p>Authentication is a query parameter (<code>apiKey<\/code>), not a header. Grab a free key at <a href=\"https:\/\/oddspapi.io\">oddspapi.io<\/a> and the rest is three imports.<\/p>\n<pre class=\"wp-block-code\"><code>import requests\nimport pandas as pd\n\nAPI_KEY = \"YOUR_API_KEY\"\nBASE_URL = \"https:\/\/api.oddspapi.io\/v4\"\n\n# Discover every sport available\nresp = requests.get(f\"{BASE_URL}\/sports\", params={\"apiKey\": API_KEY})\nsports = resp.json()\n\nfor s in sports[:10]:\n    print(f\"{s['sportId']:&gt;3}  {s['sportName']}\")\n<\/code><\/pre>\n<p>Output (partial):<\/p>\n<pre class=\"wp-block-code\"><code> 10  Soccer\n 11  Basketball\n 12  Tennis\n 13  Baseball\n 14  American Football\n 15  Ice Hockey\n 16  ESport Dota\n 17  ESport Counter-Strike\n 20  MMA\n 23  Volleyball\n<\/code><\/pre>\n<p>Every one of those sports has historical data available. Pick a <code>sportId<\/code> and move on.<\/p>\n<h2>Step 2: Fetch Historical Fixtures for a Date Range<\/h2>\n<p>The <code>\/fixtures<\/code> endpoint takes <code>from<\/code> and <code>to<\/code> parameters (max 10 days apart) and returns every fixture played or scheduled in that window. For a backtest, point it at a past date range and pull the full fixture list.<\/p>\n<pre class=\"wp-block-code\"><code># Fetch all soccer fixtures from April 5\u20136, 2026\nparams = {\n    \"apiKey\": API_KEY,\n    \"sportId\": 10,          # 10 = Soccer\n    \"from\": \"2026-04-05\",\n    \"to\": \"2026-04-06\",\n}\n\nresp = requests.get(f\"{BASE_URL}\/fixtures\", params=params)\nfixtures = resp.json()\n\nprint(f\"Fetched {len(fixtures)} fixtures\")\n<\/code><\/pre>\n<p>Output:<\/p>\n<pre class=\"wp-block-code\"><code>Fetched 985 fixtures\n<\/code><\/pre>\n<p>985 soccer fixtures in a single 48-hour window, across every league OddsPapi ingests. Filter by <code>tournamentName<\/code> if you only care about a specific league (e.g. <code>\"Premier League\"<\/code>, <code>\"La Liga\"<\/code>, <code>\"MLS\"<\/code>).<\/p>\n<h3>Filter to the league you care about<\/h3>\n<pre class=\"wp-block-code\"><code>target_tournaments = [\"Premier League\", \"La Liga\", \"Serie A\", \"Bundesliga\", \"Ligue 1\"]\n\ntop_leagues = [\n    f for f in fixtures\n    if f.get(\"tournamentName\") in target_tournaments\n]\n\nprint(f\"{len(top_leagues)} fixtures across top leagues\")\n<\/code><\/pre>\n<h2>Step 3: Pull Historical Odds for a Fixture<\/h2>\n<p>The <code>\/historical-odds<\/code> endpoint takes a <code>fixtureId<\/code> and up to three bookmakers. Each call returns the <strong>full price history<\/strong> for every market and outcome offered by those books \u2014 not a single snapshot, but the complete list of line moves with timestamps.<\/p>\n<pre class=\"wp-block-code\"><code># Pick a fixture (any from the filtered list)\nfixture = top_leagues[0]\nfixture_id = fixture[\"fixtureId\"]\n\n# Fetch historical odds from 3 sharp\/soft books\nparams = {\n    \"apiKey\": API_KEY,\n    \"fixtureId\": fixture_id,\n    \"bookmakers\": \"pinnacle,bet365,singbet\",\n}\nresp = requests.get(f\"{BASE_URL}\/historical-odds\", params=params)\ndata = resp.json()\n\nprint(\"Bookmakers returned:\", list(data[\"bookmakers\"].keys()))\n<\/code><\/pre>\n<p>Output:<\/p>\n<pre class=\"wp-block-code\"><code>Bookmakers returned: ['bet365', 'pinnacle', 'singbet']\n<\/code><\/pre>\n<p><strong>Note:<\/strong> The API enforces a max of 3 bookmakers per call. To pull more, loop the request with different bookmaker combinations and merge the responses \u2014 the code in Step 4 handles that naturally.<\/p>\n<h3>The nested response structure<\/h3>\n<p>Here&#8217;s the shape of what you get back:<\/p>\n<pre class=\"wp-block-code\"><code>{\n  \"fixtureId\": \"id1000119163078643\",\n  \"bookmakers\": {\n    \"pinnacle\": {\n      \"markets\": {\n        \"101\": {                   # marketId = Full Time Result (1X2)\n          \"outcomes\": {\n            \"101\": {                 # outcomeId = Home win\n              \"players\": {\n                \"0\": [\n                  {\"createdAt\": \"2026-04-05T14:58:17Z\", \"price\": 1.85, \"limit\": null, ...},\n                  {\"createdAt\": \"2026-04-05T13:02:18Z\", \"price\": 1.92, \"limit\": null, ...},\n                  {\"createdAt\": \"2026-04-05T11:41:42Z\", \"price\": 1.95, \"limit\": null, ...}\n                ]\n              }\n            },\n            \"102\": { ... X price history ... },\n            \"103\": { ... away price history ... }\n          }\n        },\n        \"1010\": { ... Over\/Under 2.5 price history ... }\n      }\n    },\n    \"bet365\": { ... }\n  }\n}\n<\/code><\/pre>\n<p>Every entry in the inner list is one line move. Timestamped. Free. This is the data OddsJam charges $299\/month for.<\/p>\n<h2>Step 4: Flatten the Nested JSON into Tabular Rows<\/h2>\n<p>To get this into a CSV or Excel file, we need one row per <em>bookmaker \u00d7 market \u00d7 outcome \u00d7 timestamp<\/em>. We also want human-readable market and outcome names, not just IDs, so we fetch the market catalog once and build a lookup table.<\/p>\n<pre class=\"wp-block-code\"><code># Fetch the market catalog for the sport (maps marketId\/outcomeId to names)\nresp = requests.get(f\"{BASE_URL}\/markets\", params={\"apiKey\": API_KEY, \"sportId\": 10})\nmarkets_catalog = resp.json()\n\nmarket_names = {m[\"marketId\"]: m[\"marketName\"] for m in markets_catalog}\noutcome_names = {\n    (m[\"marketId\"], o[\"outcomeId\"]): o[\"outcomeName\"]\n    for m in markets_catalog\n    for o in m.get(\"outcomes\", [])\n}\n\ndef flatten_historical_odds(data, fixture):\n    \"\"\"Flatten the nested historical-odds response into a list of flat dicts.\"\"\"\n    rows = []\n    home = fixture.get(\"participant1Name\")\n    away = fixture.get(\"participant2Name\")\n    start = fixture.get(\"startTime\")\n    fid = fixture[\"fixtureId\"]\n\n    for bm, bdata in data.get(\"bookmakers\", {}).items():\n        for mid_str, mdata in bdata.get(\"markets\", {}).items():\n            mid = int(mid_str)\n            for oid_str, odata in mdata.get(\"outcomes\", {}).items():\n                oid = int(oid_str)\n                for _player, price_history in odata.get(\"players\", {}).items():\n                    for snap in price_history:\n                        rows.append({\n                            \"fixture_id\": fid,\n                            \"start_time\": start,\n                            \"home\": home,\n                            \"away\": away,\n                            \"bookmaker\": bm,\n                            \"market_id\": mid,\n                            \"market_name\": market_names.get(mid, f\"Market {mid}\"),\n                            \"outcome_id\": oid,\n                            \"outcome_name\": outcome_names.get((mid, oid), f\"Outcome {oid}\"),\n                            \"price\": snap.get(\"price\"),\n                            \"limit\": snap.get(\"limit\"),\n                            \"recorded_at\": snap.get(\"createdAt\"),\n                        })\n    return rows\n\nrows = flatten_historical_odds(data, fixture)\ndf = pd.DataFrame(rows)\nprint(df.shape)\nprint(df.head())\n<\/code><\/pre>\n<p>Output:<\/p>\n<pre class=\"wp-block-code\"><code>(8881, 12)\n           fixture_id                home          away bookmaker  market_id       market_name outcome_name  price              recorded_at\n0  id1000119163078643  Berekum Chelsea  Aduana Stars    bet365        101  Full Time Result            1   1.18 2026-04-05T09:38:17+00:00\n1  id1000119163078643  Berekum Chelsea  Aduana Stars    bet365        101  Full Time Result            1   1.10 2026-04-04T12:03:18+00:00\n2  id1000119163078643  Berekum Chelsea  Aduana Stars    bet365        101  Full Time Result            1   1.12 2026-04-04T10:41:42+00:00\n3  id1000119163078643  Berekum Chelsea  Aduana Stars    bet365        101  Full Time Result            X   4.75 2026-04-05T09:38:17+00:00\n4  id1000119163078643  Berekum Chelsea  Aduana Stars    bet365        101  Full Time Result            X   7.00 2026-04-04T12:03:18+00:00\n<\/code><\/pre>\n<p><strong>8,881 rows from a single fixture with two bookmakers.<\/strong> That&#8217;s thousands of timestamped line moves across every market they offered \u2014 1X2, Over\/Under, Asian Handicaps, Both Teams To Score, Correct Score, the whole book. Multiply by hundreds of fixtures for a full backtest dataset.<\/p>\n<h2>Step 5: Export to CSV and Excel<\/h2>\n<p>With the rows flattened into a DataFrame, the export is one line.<\/p>\n<pre class=\"wp-block-code\"><code># Export to CSV \u2014 the universal option\ndf.to_csv(\"historical_odds.csv\", index=False)\n\n# Export to Excel \u2014 requires openpyxl: pip install openpyxl\ndf.to_excel(\"historical_odds.xlsx\", index=False, sheet_name=\"Historical Odds\")\n\nprint(f\"Exported {len(df)} rows to historical_odds.csv and historical_odds.xlsx\")\n<\/code><\/pre>\n<p>Output:<\/p>\n<pre class=\"wp-block-code\"><code>Exported 8881 rows to historical_odds.csv and historical_odds.xlsx\n<\/code><\/pre>\n<h3>Scaling up: loop over an entire season<\/h3>\n<p>To build a full season dataset, loop over fixtures and append each flattened batch to a single DataFrame. Keep in mind rate limits \u2014 a small <code>time.sleep(0.2)<\/code> between calls keeps you safe on the free tier.<\/p>\n<pre class=\"wp-block-code\"><code>import time\n\nall_rows = []\nfor fx in top_leagues[:50]:        # first 50 filtered fixtures\n    r = requests.get(\n        f\"{BASE_URL}\/historical-odds\",\n        params={\n            \"apiKey\": API_KEY,\n            \"fixtureId\": fx[\"fixtureId\"],\n            \"bookmakers\": \"pinnacle,bet365,singbet\",\n        },\n    )\n    if r.status_code == 200:\n        all_rows.extend(flatten_historical_odds(r.json(), fx))\n    time.sleep(0.2)\n\nseason_df = pd.DataFrame(all_rows)\nseason_df.to_csv(\"season_historical.csv\", index=False)\nprint(f\"{len(season_df):,} rows exported\")\n<\/code><\/pre>\n<h2>Step 6: Backtest Teaser \u2014 Compute Closing Line Value<\/h2>\n<p>Once the CSV is on disk, you can load it back and start doing real analysis. Here&#8217;s the simplest thing: compute the <strong>closing line<\/strong> (the last price recorded before kickoff) for every market on every book, then compare a hypothetical &#8220;bet&#8221; against it.<\/p>\n<pre class=\"wp-block-code\"><code>df = pd.read_csv(\"historical_odds.csv\", parse_dates=[\"recorded_at\", \"start_time\"])\n\n# Only keep rows recorded BEFORE kickoff\npre_match = df[df[\"recorded_at\"] &lt; df[\"start_time\"]]\n\n# The \"closing line\" is the last price per bookmaker \u00d7 market \u00d7 outcome\nclosing = (\n    pre_match\n    .sort_values(\"recorded_at\")\n    .groupby([\"fixture_id\", \"bookmaker\", \"market_id\", \"outcome_id\"])\n    .tail(1)\n    .rename(columns={\"price\": \"closing_price\"})\n)\n\n# Pinnacle's 1X2 home closing lines \u2014 your backtest benchmark\npinnacle_home_closes = closing[\n    (closing[\"bookmaker\"] == \"pinnacle\") &amp;\n    (closing[\"market_name\"] == \"Full Time Result\") &amp;\n    (closing[\"outcome_name\"] == \"1\")\n]\n\nprint(pinnacle_home_closes[[\"home\", \"away\", \"closing_price\"]].head())\n<\/code><\/pre>\n<p>That&#8217;s the foundation of every serious betting model: use the closing price from a sharp book (Pinnacle is the industry benchmark) as ground truth, compare your model&#8217;s predictions against it, and measure whether you&#8217;re beating the closing line. If you want the full walkthrough with staking strategy and Kelly sizing, see our <a href=\"https:\/\/oddspapi.io\/blog\/backtest-betting-model-free-historical-odds\/\">betting model backtest tutorial<\/a>.<\/p>\n<h2>What You Can Build Once the Data Is in a CSV<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Project<\/th>\n<th>What you need from the CSV<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Model training (ML features)<\/strong><\/td>\n<td>Closing lines, opening lines, line movement velocity, consensus vs Pinnacle disagreement<\/td>\n<\/tr>\n<tr>\n<td><strong>CLV tracker<\/strong><\/td>\n<td>Your bet&#8217;s price vs the Pinnacle closing price at the same timestamp<\/td>\n<\/tr>\n<tr>\n<td><strong>Line movement analysis<\/strong><\/td>\n<td>Full <code>recorded_at<\/code> timeline per market \u2014 plot with matplotlib or Plotly<\/td>\n<\/tr>\n<tr>\n<td><strong>Sharp vs soft divergence<\/strong><\/td>\n<td>Pinnacle\/Singbet vs Bet365\/DraftKings on the same fixtures<\/td>\n<\/tr>\n<tr>\n<td><strong>Arbitrage historical research<\/strong><\/td>\n<td>Cross-book pivot: was there ever a real arb on this fixture?<\/td>\n<\/tr>\n<tr>\n<td><strong>Steam move detector<\/strong><\/td>\n<td><code>price.diff()<\/code> per bookmaker per market, flagged when &gt; N%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>None of this requires a $299\/month plan. It requires a free API key, Python, and 30 lines of flattening code.<\/p>\n<h2>FAQ<\/h2>\n<h3>How far back does OddsPapi&#8217;s free historical data go?<\/h3>\n<p>Historical odds are available from our archive ingestion start date and are included on the free tier for all sports. Price history is stored per line move, so a fixture may have dozens or hundreds of snapshots depending on book activity.<\/p>\n<h3>Can I export player props historical data?<\/h3>\n<p>Yes. Player prop markets (NFL QB passing yards, NBA points, soccer shots on target) appear in the same <code>\/historical-odds<\/code> response under their own market IDs. The flattening function above handles them identically \u2014 no special case needed.<\/p>\n<h3>Why does the API limit me to 3 bookmakers per call?<\/h3>\n<p>Each bookmaker adds a full price history payload, and some fixtures have 8,000+ line moves per book. The 3-bookmaker cap keeps response sizes manageable. To pull more books, just loop the request with different bookmaker combinations \u2014 the flatten function in Step 4 merges them cleanly.<\/p>\n<h3>Is there a row count or request limit?<\/h3>\n<p>The free tier has per-minute rate limits but no hard row cap. A <code>time.sleep(0.2)<\/code> between requests keeps you well within the limit for most backtesting workflows. For heavy batch jobs or production backtest pipelines, the Pro tier lifts the rate limits.<\/p>\n<h3>Can I schedule automated exports?<\/h3>\n<p>Yes. Wrap the loop in a <code>cron<\/code> job or a GitHub Actions workflow, write the CSV to S3 or Google Cloud Storage, and you have a nightly historical odds pipeline for free. No paid &#8220;data export&#8221; add-on required.<\/p>\n<h3>Does the CSV include closed or settled fixtures?<\/h3>\n<p>Yes. The <code>\/fixtures<\/code> endpoint returns fixtures regardless of status \u2014 closed, settled, in-play, or upcoming. Filter by <code>statusId<\/code> if you only want settled fixtures for a backtest (&#8220;did the bet win?&#8221;) calculation.<\/p>\n<h2>Stop Paying for Data You Need Once<\/h2>\n<p>Historical odds should be a commodity. You need it once \u2014 to backtest a model or research a strategy \u2014 and then you&#8217;re done. Paying $299\/month for a one-time data pull is an extraction tax, not a product.<\/p>\n<p>OddsPapi gives historical data away free because we want developers to <em>build<\/em> on our API, not bounce off a paywall. Grab an API key, run the code above, and you&#8217;ll have a CSV in your working directory in about five minutes.<\/p>\n<p><strong><a href=\"https:\/\/oddspapi.io\">Get your free OddsPapi API key \u2192<\/a><\/strong><\/p>\n<p><em>Related reading: <a href=\"https:\/\/oddspapi.io\/blog\/bet365-historical-odds-guide-the-data-apis-and-strategy\/\">Bet365 Historical Odds Guide<\/a> \u00b7 <a href=\"https:\/\/oddspapi.io\/blog\/backtest-betting-model-free-historical-odds\/\">Backtest a Betting Model with Free Historical Odds<\/a> \u00b7 <a href=\"https:\/\/oddspapi.io\/blog\/value-betting-scanner-python\/\">Build a Value Betting Scanner in Python<\/a> \u00b7 <a href=\"https:\/\/oddspapi.io\/blog\/free-odds-api-350-bookmakers\/\">Free Odds API: 350+ Bookmakers<\/a><\/em><\/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\": \"How far back does OddsPapi's free historical data go?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Historical odds are available from our archive ingestion start date and are included on the free tier for all sports. Price history is stored per line move, so a fixture may have dozens or hundreds of snapshots depending on book activity.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can I export player props historical data?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. Player prop markets appear in the same \/historical-odds response under their own market IDs. The flattening function in this tutorial handles them identically \u2014 no special case needed.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Why does the API limit me to 3 bookmakers per call?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Each bookmaker adds a full price history payload, and some fixtures have 8,000+ line moves per book. The 3-bookmaker cap keeps response sizes manageable. To pull more books, loop the request with different bookmaker combinations.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is there a row count or request limit on historical odds exports?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"The free tier has per-minute rate limits but no hard row cap. A short sleep between requests keeps you within the limit for backtesting workflows. Pro tier lifts the rate limits for heavy batch jobs.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can I schedule automated historical odds exports?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. Wrap the export loop in a cron job or GitHub Actions workflow, write the CSV to S3 or cloud storage, and you have a nightly historical odds pipeline for free.\"\n      }\n    }\n  ]\n}\n<\/script><\/p>\n<p><!--\nFocus Keyphrase: historical odds csv\nSEO Title: Historical Odds Data: Export to CSV or Excel for Backtesting (Free API)\nMeta Description: Export historical odds to CSV or Excel for backtesting. Free across all sports via OddsPapi \u2014 350+ bookmakers, Python tutorial with tested code.\nSlug: historical-odds-csv-excel-backtesting\n--><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Export historical odds to CSV or Excel for backtesting. Free across all sports via OddsPapi \u2014 350+ bookmakers, Python tutorial with tested code.<\/p>\n","protected":false},"author":2,"featured_media":2825,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,7],"tags":[30,12,8,4,9,11],"class_list":["post-2824","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-historical-odds","category-how-to-guides","tag-backtesting","tag-betting-data","tag-free-api","tag-historical-odds","tag-odds-api","tag-python"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Historical Odds Data: Export to CSV or Excel for Backtesting (Free API) | Odds API Development 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\/historical-odds-csv-excel-backtesting\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API) | Odds API Development Blog\" \/>\n<meta property=\"og:description\" content=\"Export historical odds to CSV or Excel for backtesting. Free across all sports via OddsPapi \u2014 350+ bookmakers, Python tutorial with tested code.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/\" \/>\n<meta property=\"og:site_name\" content=\"Odds API Development Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-04-13T10:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp\" \/>\n\t<meta property=\"og:image:width\" content=\"2560\" \/>\n\t<meta property=\"og:image:height\" content=\"1429\" \/>\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\/historical-odds-csv-excel-backtesting\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/\"},\"author\":{\"name\":\"Odds API Writer\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\"},\"headline\":\"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API)\",\"datePublished\":\"2026-04-13T10:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/\"},\"wordCount\":1404,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp\",\"keywords\":[\"Backtesting\",\"Betting Data\",\"Free API\",\"historical odds\",\"Odds API\",\"Python\"],\"articleSection\":[\"Historical Odds\",\"How To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/\",\"name\":\"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API) | Odds API Development Blog\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp\",\"datePublished\":\"2026-04-13T10:00:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp\",\"width\":2560,\"height\":1429,\"caption\":\"Historical Odds Data - OddsPapi API Blog\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/oddspapi.io\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"name\":\"OddsPapi\",\"description\":\"Sports Odds APIs 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":"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API) | Odds API Development 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\/historical-odds-csv-excel-backtesting\/","og_locale":"en_US","og_type":"article","og_title":"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API) | Odds API Development Blog","og_description":"Export historical odds to CSV or Excel for backtesting. Free across all sports via OddsPapi \u2014 350+ bookmakers, Python tutorial with tested code.","og_url":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/","og_site_name":"Odds API Development Blog","article_published_time":"2026-04-13T10:00:00+00:00","og_image":[{"width":2560,"height":1429,"url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.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\/historical-odds-csv-excel-backtesting\/#article","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/"},"author":{"name":"Odds API Writer","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13"},"headline":"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API)","datePublished":"2026-04-13T10:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/"},"wordCount":1404,"commentCount":0,"publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp","keywords":["Backtesting","Betting Data","Free API","historical odds","Odds API","Python"],"articleSection":["Historical Odds","How To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/","url":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/","name":"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API) | Odds API Development Blog","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp","datePublished":"2026-04-13T10:00:00+00:00","breadcrumb":{"@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#primaryimage","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/historical-odds-csv-excel-backtesting-scaled.webp","width":2560,"height":1429,"caption":"Historical Odds Data - OddsPapi API Blog"},{"@type":"BreadcrumbList","@id":"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/oddspapi.io\/blog\/"},{"@type":"ListItem","position":2,"name":"Historical Odds Data: Export to CSV or Excel for Backtesting (Free API)"}]},{"@type":"WebSite","@id":"https:\/\/oddspapi.io\/blog\/#website","url":"https:\/\/oddspapi.io\/blog\/","name":"OddsPapi","description":"Sports Odds APIs 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\/2824","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=2824"}],"version-history":[{"count":1,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2824\/revisions"}],"predecessor-version":[{"id":2826,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2824\/revisions\/2826"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media\/2825"}],"wp:attachment":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media?parent=2824"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/categories?post=2824"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/tags?post=2824"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}