{"id":2909,"date":"2026-05-20T10:00:00","date_gmt":"2026-05-20T10:00:00","guid":{"rendered":"https:\/\/oddspapi.io\/blog\/?p=2909"},"modified":"2026-05-03T14:13:45","modified_gmt":"2026-05-03T14:13:45","slug":"pandas-odds-api-dataframe-tutorial","status":"publish","type":"post","link":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/","title":{"rendered":"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines"},"content":{"rendered":"<p><strong>The OddsPapi <code>\/v4\/odds<\/code> response is four levels deep: bookmaker \u2192 market \u2192 outcome \u2192 players[0]. That nesting is fine for a JSON viewer, useless for analysis. This post turns it into a flat pandas DataFrame in 10 lines, then walks through the four recipes that make the DataFrame worth building: best price per outcome, vig per book, price spread per market, and a live arb scanner.<\/strong><\/p>\n<p>Every number in this post was pulled live from the OddsPapi API on Manchester United vs Liverpool \u2014 122 bookmakers, 18,426 active price rows from a single fixture. If you want the data to follow along, grab a <a href=\"https:\/\/oddspapi.io\">free API key<\/a> and any soccer fixture ID; the structure is identical across sports.<\/p>\n<h2>Why You Want a DataFrame, Not a Dict<\/h2>\n<p>Most odds-API tutorials hand you a Python dict and stop there. That&#8217;s fine if you only ever check one bookmaker for one market. The moment you want to compare prices, calculate fair odds, find arbs, or backtest, you&#8217;ll write 30 lines of nested loops to flatten it \u2014 every single time.<\/p>\n<p>The whole point of pandas is that <em>once you have a DataFrame<\/em>, every analytical question becomes a one-line groupby. So the trick is getting from the raw JSON payload to a clean, long-format DataFrame as fast as possible, then never touching the dict again.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Workflow<\/th>\n<th>Without pandas<\/th>\n<th>With pandas<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Best price per outcome<\/td>\n<td>Loop over books, dict comparisons, manual max<\/td>\n<td><code>df.groupby(\"outcome_id\")[\"price\"].max()<\/code><\/td>\n<\/tr>\n<tr>\n<td>Vig per bookmaker<\/td>\n<td>Re-shape per book, sum inverse manually<\/td>\n<td><code>df.groupby(\"book\")[\"price\"].agg(vig)<\/code><\/td>\n<\/tr>\n<tr>\n<td>Price spread per market<\/td>\n<td>Min\/max passes per market<\/td>\n<td><code>df.groupby(\"market_id\")[\"price\"].agg([\"min\",\"max\"])<\/code><\/td>\n<\/tr>\n<tr>\n<td>Find arbs across 100+ books<\/td>\n<td>Triple nested loops<\/td>\n<td><code>df.groupby(\"market_id\").apply(scan)<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>The 10-Line Snippet<\/h2>\n<p>Here is the entire JSON-to-DataFrame conversion. Pull odds, comprehend, done.<\/p>\n<pre class=\"wp-block-code\"><code>import requests, pandas as pd\n\nAPI_KEY = \"YOUR_API_KEY\"\nodds = requests.get(\"https:\/\/api.oddspapi.io\/v4\/odds\", params={\n    \"apiKey\": API_KEY,\n    \"fixtureId\": \"id1000001761301215\",  # Man Utd vs Liverpool\n}).json()\n\ndf = pd.DataFrame([\n    {\"book\": book, \"market_id\": int(mid), \"outcome_id\": int(oid), \"price\": p[\"price\"]}\n    for book, b in odds[\"bookmakerOdds\"].items()\n    for mid, m in b[\"markets\"].items()\n    for oid, o in m[\"outcomes\"].items()\n    for p in [o[\"players\"].get(\"0\")] if p and p.get(\"active\") and p[\"price\"] &gt; 0\n])\n<\/code><\/pre>\n<p>Output:<\/p>\n<pre class=\"wp-block-code\"><code>&gt;&gt;&gt; df.shape\n(18426, 4)\n&gt;&gt;&gt; df.book.nunique()\n119\n&gt;&gt;&gt; df.market_id.nunique()\n342\n&gt;&gt;&gt; df.head()\n     book  market_id  outcome_id  price\n0     3et      10224       10225   4.65\n1     3et      10224       10224   1.15\n2     3et      10240       10240   1.19\n3     3et      10240       10241   4.15\n4     3et      10216       10216   1.86\n<\/code><\/pre>\n<p>That&#8217;s it \u2014 one fixture, 18,426 rows of clean tidy data, every bookmaker&#8217;s price for every market and outcome on a single row. From here, every analytical question is a one-liner.<\/p>\n<h3>Three things the snippet handles defensively<\/h3>\n<ul>\n<li><strong>Player prop markets are skipped.<\/strong> Standard match markets (1X2, totals, BTTS, handicaps) ship a single price under <code>players[\"0\"]<\/code>. Player props use the player&#8217;s ID as the key (<code>players[\"1886726\"]<\/code> etc.). Filtering on <code>players.get(\"0\")<\/code> keeps the DataFrame clean for game-level analysis. Add a second pass if you want props.<\/li>\n<li><strong><code>active=True<\/code> is required.<\/strong> Inactive outcomes (suspended, settled, withdrawn) ship with stale or zero prices. Always filter.<\/li>\n<li><strong><code>price &gt; 0<\/code> is required even when <code>active=True<\/code>.<\/strong> A small number of books (Bet365 is the usual offender) ship <code>active=True<\/code> with <code>price=0<\/code> for markets they don&#8217;t price yet. Without this filter, a single zero will tank every <code>min()<\/code> and <code>idxmin()<\/code> in your downstream analysis.<\/li>\n<\/ul>\n<h2>Add Human-Readable Names (Optional but Worth It)<\/h2>\n<p>The DataFrame above is enough for math, but market and outcome IDs aren&#8217;t memorable. The <code>\/v4\/markets<\/code> catalog gives you the lookup tables you need:<\/p>\n<pre class=\"wp-block-code\"><code>cat = requests.get(\"https:\/\/api.oddspapi.io\/v4\/markets\",\n    params={\"apiKey\": API_KEY, \"sportId\": 10}).json()\n\nmkt_name = {m[\"marketId\"]: m[\"marketName\"] for m in cat}\nout_name = {(m[\"marketId\"], o[\"outcomeId\"]): o[\"outcomeName\"]\n            for m in cat for o in m.get(\"outcomes\", [])}\n\ndf[\"market\"] = df.market_id.map(mkt_name)\ndf[\"outcome\"] = df.apply(lambda r: out_name.get((r.market_id, r.outcome_id)), axis=1)\n<\/code><\/pre>\n<p>Soccer&#8217;s catalog has 32,000+ markets across all handicap and total variants \u2014 way too many to hardcode. Build the lookup from the API and your code stays correct as new markets get added.<\/p>\n<h2>Recipe 1: Best Price Per Outcome<\/h2>\n<p>The most common question for any sharp bettor: <em>which book has the best price for each outcome on this market?<\/em> One line:<\/p>\n<pre class=\"wp-block-code\"><code>m101 = df[df.market_id == 101]  # 1X2 \/ Full Time Result\nbest = m101.loc[m101.groupby(\"outcome_id\")[\"price\"].idxmax()]\nprint(best[[\"outcome\", \"book\", \"price\"]])\n<\/code><\/pre>\n<p>Output (Man Utd vs Liverpool, live data captured for this post):<\/p>\n<pre class=\"wp-block-code\"><code>outcome        book  price\n      1  betfair-ex    2.50\n      X      kalshi    4.00\n      2 hardrockbet    3.00\n<\/code><\/pre>\n<p>Three different venues, three best prices: a UK exchange has the best Man Utd line, a US prediction market is paying the highest draw odds, and a US sportsbook tops the Liverpool side. <strong>If you placed your full stake at a single book, you&#8217;d be giving away vig on two of three legs every single time.<\/strong> This is the foundation of <a href=\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\">line shopping<\/a> \u2014 and pandas turns it into a one-liner.<\/p>\n<h2>Recipe 2: Vig Per Bookmaker<\/h2>\n<p>Vig (the bookmaker&#8217;s built-in margin) tells you how aggressive a book is pricing. Lower vig = sharper book = better long-term value. The math: sum the inverse decimal odds across a market&#8217;s outcomes; the excess over 1.0 is the vig.<\/p>\n<pre class=\"wp-block-code\"><code>def vig(prices):\n    return ((1 \/ prices).sum() - 1) * 100 if len(prices) == 3 else None\n\nvig_per_book = (\n    m101.groupby(\"book\")[\"price\"]\n        .agg(vig)\n        .dropna()\n        .sort_values()\n)\nprint(vig_per_book.head(10))\nprint(\"...\")\nprint(vig_per_book.tail(5))\n<\/code><\/pre>\n<p>Live output (Man Utd vs Liverpool 1X2, sorted by vig):<\/p>\n<pre class=\"wp-block-code\"><code>book\nkalshi           0.00      &lt;- prediction market, near-zero by design\nbetfair-ex       0.46      &lt;- exchange, 1-2% commission instead of vig\n1xbet            1.54\npolymarket       2.00\nadmiralbet.rs    2.02\nsportybet        2.14\npinnacle         2.70      &lt;- the sharp benchmark\nunibet.nl        2.35\n...\nlottoland        9.02\nsesamesport.bg   9.26\nvirginbet       10.89\nbwin.fr         14.38\nwinamax.fr      19.14      &lt;- 19% margin on a Premier League marquee\n<\/code><\/pre>\n<p>The spread is staggering: Pinnacle prices the same market at 2.7% margin, Winamax France at 19%. If you only ever check one book, this number is the silent tax you&#8217;re paying. The whole reason OddsPapi exists is so you don&#8217;t have to.<\/p>\n<p>Note that exchanges and prediction markets show near-zero vig \u2014 they monetize through commission on settled bets instead. Filter them out (<code>vig_per_book[vig_per_book &gt; 0.5]<\/code>) for an apples-to-apples sportsbook comparison.<\/p>\n<h2>Recipe 3: Price Spread Per Market<\/h2>\n<p>&#8220;Where are the books most disagreed?&#8221; One groupby:<\/p>\n<pre class=\"wp-block-code\"><code>spread = (\n    df.groupby(\"market_id\")[\"price\"]\n      .agg([\"min\", \"max\", \"count\"])\n)\nspread[\"spread_pct\"] = (spread[\"max\"] - spread[\"min\"]) \/ spread[\"min\"] * 100\nspread[\"market\"] = spread.index.map(mkt_name)\nprint(spread[spread[\"count\"] &gt;= 30].sort_values(\"spread_pct\", ascending=False).head(10))\n<\/code><\/pre>\n<p>The <code>count &gt;= 30<\/code> filter strips out illiquid markets where 2 books quote wildly different prices because nobody&#8217;s pricing them seriously. What you&#8217;re left with is a ranked list of markets where prices genuinely diverge across well-quoted books \u2014 the hunting ground for value bets.<\/p>\n<p>For Man Utd vs Liverpool, the highest-spread markets are correct-score variants (one book at 6.0, another at 1,250.0 for the same scoreline) \u2014 expected, because correct-score is structurally hard to model. The interesting ones are 1X2, Asian Handicaps, and totals where the spread should be tight but isn&#8217;t.<\/p>\n<h2>Recipe 4: Live Arb Scanner<\/h2>\n<p>An arbitrage opportunity exists when the inverse sum of best prices across all outcomes is less than 1.0. With pandas, you can scan every 3-way market on a fixture in a single pass:<\/p>\n<pre class=\"wp-block-code\"><code>three_way_markets = df.groupby(\"market_id\").outcome_id.nunique()\nthree_way_markets = three_way_markets[three_way_markets == 3].index\n\narbs = []\nfor mid in three_way_markets:\n    sub = df[df.market_id == mid]\n    best_per_outcome = sub.groupby(\"outcome_id\")[\"price\"].max()\n    if len(best_per_outcome) == 3:\n        invsum = (1 \/ best_per_outcome).sum()\n        if invsum &lt; 1:\n            arbs.append({\n                \"market_id\": mid,\n                \"market\": mkt_name.get(mid),\n                \"invsum\": invsum,\n                \"margin_pct\": (1 - invsum) * 100,\n            })\n\nprint(pd.DataFrame(arbs).sort_values(\"margin_pct\", ascending=False))\n<\/code><\/pre>\n<p>Live output:<\/p>\n<pre class=\"wp-block-code\"><code>   market_id                       market    invsum  margin_pct\n       10140       European Handicap (-1)    0.5955       40.45    &lt;- DATA ERROR, ignore\n      102050      To Win From Behind FT     0.8918       10.82    &lt;- stale, illiquid\n       10211           Second Half Result    0.9579        4.21    &lt;- plausible, verify\n         101         Full Time Result 1X2    0.9833        1.67    &lt;- real, tight margin\n<\/code><\/pre>\n<h3>Why your arb scanner needs sanity checks<\/h3>\n<p>The headline result (40% margin on European Handicap -1) is almost certainly a stale price or a data error from a low-quality book \u2014 not a real arb. <strong>The biggest &#8220;arbs&#8221; are usually the ones that aren&#8217;t real.<\/strong> Before you ever stake on a scanner output, layer in:<\/p>\n<ul>\n<li><strong>Margin ceiling.<\/strong> Real soft\/sharp arbs sit at 0.5\u20133%. Anything above 5% on a top-tier market is a stale-price flag, not a green light. Cap your scanner at <code>margin_pct &lt; 5<\/code>.<\/li>\n<li><strong>Book whitelist.<\/strong> Restrict best-price selection to books you actually have funded accounts at and that pay out reliably. The 40% &#8220;arb&#8221; disappears the moment you exclude one bookmaker. Pre-filter the DataFrame: <code>df = df[df.book.isin([\"pinnacle\", \"bet365\", \"draftkings\", ...])]<\/code>.<\/li>\n<li><strong>Liquidity floor.<\/strong> Require N books quoted on each outcome before trusting it as the &#8220;true&#8221; best. <code>sub.groupby(\"outcome_id\").size().min() &gt;= 5<\/code>.<\/li>\n<li><strong>Re-poll before stake.<\/strong> Lines move. Even a 5-second-old arb may have closed. The OddsPapi response includes a <code>changedAt<\/code> timestamp on every outcome \u2014 use it.<\/li>\n<\/ul>\n<p>For a complete arb implementation with these guardrails, see our <a href=\"https:\/\/oddspapi.io\/blog\/arbitrage-betting-bot-python\/\">arbitrage betting bot tutorial<\/a>.<\/p>\n<h2>From One Fixture to a Live Slate<\/h2>\n<p>Everything above used a single fixture. To run the same recipes across an entire matchday, fetch fixtures, then map the DataFrame builder over each one:<\/p>\n<pre class=\"wp-block-code\"><code>from datetime import datetime, timedelta, timezone\nimport time\n\nnow = datetime.now(timezone.utc)\nend = now + timedelta(days=1)\n\nfixtures = requests.get(\"https:\/\/api.oddspapi.io\/v4\/fixtures\", params={\n    \"apiKey\": API_KEY,\n    \"sportId\": 10,\n    \"from\": now.strftime(\"%Y-%m-%dT%H:%M:%S\"),\n    \"to\": end.strftime(\"%Y-%m-%dT%H:%M:%S\"),\n}).json()\n\nframes = []\nfor f in fixtures:\n    if not f.get(\"hasOdds\"):\n        continue\n    o = requests.get(\"https:\/\/api.oddspapi.io\/v4\/odds\", params={\n        \"apiKey\": API_KEY, \"fixtureId\": f[\"fixtureId\"]\n    }).json()\n    rows = [\n        {\n            \"fixture_id\": f[\"fixtureId\"],\n            \"match\": f\"{f['participant1Name']} vs {f['participant2Name']}\",\n            \"book\": book, \"market_id\": int(mid),\n            \"outcome_id\": int(oid), \"price\": p[\"price\"],\n        }\n        for book, b in o.get(\"bookmakerOdds\", {}).items()\n        for mid, m in b[\"markets\"].items()\n        for oid, oc in m[\"outcomes\"].items()\n        for p in [oc[\"players\"].get(\"0\")]\n        if p and p.get(\"active\") and p[\"price\"] &gt; 0\n    ]\n    frames.append(pd.DataFrame(rows))\n    time.sleep(0.2)  # respect rate limit\n\nslate = pd.concat(frames, ignore_index=True)\nprint(f\"Slate: {len(slate):,} rows across {slate.fixture_id.nunique()} fixtures\")\n<\/code><\/pre>\n<p>The free tier has a ~0.88s cooldown between calls to the same endpoint, so the <code>time.sleep(0.2)<\/code> keeps you safely inside it. A 50-fixture Premier League \/ La Liga \/ Serie A slate produces around 800,000\u20131,000,000 rows \u2014 comfortably in pandas&#8217;s wheelhouse on a laptop.<\/p>\n<h2>Why OddsPapi Pairs Well With Pandas<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Capability<\/th>\n<th>Why it matters for pandas workflows<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>350+ bookmakers in one response<\/strong><\/td>\n<td>You don&#8217;t need to merge feeds. One <code>\/v4\/odds<\/code> call gives you every book in a single nested dict, ready to flatten.<\/td>\n<\/tr>\n<tr>\n<td><strong>Free historical data<\/strong><\/td>\n<td>Same DataFrame shape, but with a price-history list per outcome instead of a single value. Backtest models without paying for snapshots.<\/td>\n<\/tr>\n<tr>\n<td><strong>Stable ID-based schema<\/strong><\/td>\n<td>Market and outcome IDs don&#8217;t move. You can persist DataFrames across days\/weeks without breaking joins.<\/td>\n<\/tr>\n<tr>\n<td><strong>Native exchange &amp; prediction-market data<\/strong><\/td>\n<td>Polymarket, Kalshi, Betfair Exchange, Matchbook all flatten into the same DataFrame. Exchange-specific data (lay prices) lives under <code>exchangeMeta<\/code>.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>Common Pitfalls<\/h2>\n<h3>1. Don&#8217;t iterate <code>iterrows()<\/code> over a 100K-row DataFrame<\/h3>\n<p>It&#8217;s 200x slower than vectorised pandas operations. If you&#8217;re tempted to <code>for row in df.iterrows()<\/code> to compute something, stop and reach for <code>groupby<\/code>, <code>apply<\/code>, or <code>merge<\/code> first.<\/p>\n<h3>2. Don&#8217;t trust the headline arb<\/h3>\n<p>Covered above \u2014 biggest &#8220;arb&#8221; is almost always a data quality issue. Filter aggressively.<\/p>\n<h3>3. Don&#8217;t forget timezones on historical data<\/h3>\n<p>The historical endpoint ships ISO timestamps in UTC (<code>\"2026-04-05T09:38:17.611441+00:00\"<\/code>). Parse with <code>pd.to_datetime(df[\"createdAt\"], utc=True)<\/code> and you avoid the daylight-savings traps that bite people running Python scripts on local time.<\/p>\n<h3>4. Player props use a different DataFrame<\/h3>\n<p>The 10-line snippet above filters to <code>players[\"0\"]<\/code>. Player prop markets have one row per player (key is the player ID). For props, build a parallel DataFrame:<\/p>\n<pre class=\"wp-block-code\"><code>props = pd.DataFrame([\n    {\"book\": book, \"market_id\": int(mid), \"outcome_id\": int(oid),\n     \"player_id\": pid, \"player_name\": p.get(\"playerName\"), \"price\": p[\"price\"]}\n    for book, b in odds[\"bookmakerOdds\"].items()\n    for mid, m in b[\"markets\"].items()\n    for oid, o in m[\"outcomes\"].items()\n    for pid, p in o[\"players\"].items()\n    if pid != \"0\" and p.get(\"active\") and p[\"price\"] &gt; 0\n])\n<\/code><\/pre>\n<p>For a full prop walkthrough, see our <a href=\"https:\/\/oddspapi.io\/blog\/player-props-api-nfl-nba-mlb-odds-python\/\">player props API tutorial<\/a>.<\/p>\n<h3>5. Historical odds need a list-aware flattener<\/h3>\n<p>The <code>\/v4\/historical-odds<\/code> endpoint ships <code>players[\"0\"]<\/code> as a <em>list<\/em> of price snapshots, not a single dict. Adjust the comprehension:<\/p>\n<pre class=\"wp-block-code\"><code>hist = requests.get(\"https:\/\/api.oddspapi.io\/v4\/historical-odds\", params={\n    \"apiKey\": API_KEY,\n    \"fixtureId\": \"id1000001761301215\",\n    \"bookmakers\": \"pinnacle,bet365,singbet\",  # max 3 per call\n}).json()\n\nhist_df = pd.DataFrame([\n    {\"book\": book, \"market_id\": int(mid), \"outcome_id\": int(oid),\n     \"price\": snap[\"price\"], \"ts\": snap[\"createdAt\"]}\n    for book, b in hist[\"bookmakers\"].items()\n    for mid, m in b[\"markets\"].items()\n    for oid, o in m[\"outcomes\"].items()\n    for snap in o[\"players\"][\"0\"]\n    if snap.get(\"active\") and snap[\"price\"] &gt; 0\n])\nhist_df[\"ts\"] = pd.to_datetime(hist_df[\"ts\"], utc=True)\n<\/code><\/pre>\n<p>For a full historical workflow including CSV export, see <a href=\"https:\/\/oddspapi.io\/blog\/historical-odds-csv-excel-backtesting\/\">Historical Odds CSV Export<\/a>.<\/p>\n<h2>What to Build With This DataFrame<\/h2>\n<ul>\n<li><strong><a href=\"https:\/\/oddspapi.io\/blog\/value-betting-scanner-python\/\">Value betting scanner<\/a><\/strong> \u2014 Compare each book&#8217;s price against a fair-odds benchmark (Pinnacle no-vig works well) and flag positive-EV opportunities.<\/li>\n<li><strong><a href=\"https:\/\/oddspapi.io\/blog\/consensus-odds-fair-odds-calculator-python\/\">Consensus odds calculator<\/a><\/strong> \u2014 Average the inverse prices across a quality-filtered set of books to get a &#8220;true&#8221; probability per outcome.<\/li>\n<li><strong><a href=\"https:\/\/oddspapi.io\/blog\/kelly-criterion-staking-calculator-python\/\">Kelly criterion stake sizing<\/a><\/strong> \u2014 Combined with fair-odds, compute optimal stakes on every +EV bet your scanner finds.<\/li>\n<li><strong><a href=\"https:\/\/oddspapi.io\/blog\/odds-comparison-dashboard-python-streamlit\/\">Live odds dashboard<\/a><\/strong> \u2014 Same DataFrame, visualised in Streamlit + Plotly with auto-refresh.<\/li>\n<li><strong><a href=\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\">Steam move detector<\/a><\/strong> \u2014 Use the <code>changedAt<\/code> timestamps + price history to detect when sharp money moves the line.<\/li>\n<\/ul>\n<h2>FAQ<\/h2>\n<h3>Why use pandas instead of just dicts and loops?<\/h3>\n<p>Performance and readability. A 1M-row DataFrame answers &#8220;best price per outcome across every fixture today&#8221; in 50ms via <code>groupby<\/code>. The equivalent nested-loop version is 30 lines, takes 2+ seconds, and breaks the moment a new market type appears.<\/p>\n<h3>How big can the DataFrame get before pandas struggles?<\/h3>\n<p>A full Saturday Premier League \/ La Liga \/ Serie A slate produces around 1M rows \u2014 comfortable on any laptop. Past 10M rows, switch to Polars (drop-in API, 5\u201310x faster) or chunk by sport\/league and persist to Parquet.<\/p>\n<h3>Why is <code>players[\"0\"]<\/code> the convention?<\/h3>\n<p>Standard match markets (1X2, BTTS, totals, handicaps) involve &#8220;the team&#8221; rather than a specific player, so OddsPapi files them under the synthetic <code>\"0\"<\/code> key. Player prop markets use the actual player&#8217;s ID. Filtering on <code>\"0\"<\/code> cleanly separates game-level markets from props in your DataFrame.<\/p>\n<h3>Why does <code>active=True<\/code> sometimes ship with <code>price=0<\/code>?<\/h3>\n<p>It&#8217;s an upstream feed quirk on a small number of bookmakers (Bet365 is the most frequent). The book&#8217;s UI hasn&#8217;t priced the market yet but their API endpoint returns the slot anyway. Always filter <code>price &gt; 0<\/code> in addition to <code>active<\/code>.<\/p>\n<h3>Can I store the DataFrame to disk between runs?<\/h3>\n<p>Yes \u2014 Parquet is the right format. <code>df.to_parquet(\"odds.parquet\")<\/code> is faster than CSV and preserves dtypes. For long-running collection (steam detection, historical capture), append to a partitioned dataset: <code>df.to_parquet(f\"odds\/{fixture_id}\/{ts}.parquet\")<\/code>.<\/p>\n<h3>What about Polars instead of pandas?<\/h3>\n<p>Polars uses the same flatten-then-analyse pattern. The 10-line snippet works as-is \u2014 change <code>pd.DataFrame(...)<\/code> to <code>pl.DataFrame(...)<\/code> and your <code>groupby\/agg<\/code> calls are nearly identical. For datasets above 5M rows, Polars is a clear win. Below that, pandas is fine and has the larger ecosystem.<\/p>\n<h2>Get Your Free API Key<\/h2>\n<p>Stop scraping. The full pandas workflow above runs on the free tier \u2014 350+ bookmakers, every sport, free historical data included. <a href=\"https:\/\/oddspapi.io\">Grab your API key<\/a> and pull your first DataFrame in under five minutes.<\/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\": \"Why use pandas instead of just dicts and loops for odds data?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Performance and readability. A 1M-row DataFrame answers 'best price per outcome across every fixture today' in 50ms via groupby. The equivalent nested-loop version is 30 lines, takes 2+ seconds, and breaks when a new market type appears.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How big can the odds DataFrame get before pandas struggles?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"A full Saturday Premier League \/ La Liga \/ Serie A slate produces around 1M rows \u2014 comfortable on any laptop. Past 10M rows, switch to Polars (drop-in API, 5-10x faster) or chunk by sport\/league and persist to Parquet.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Why is players[\\\"0\\\"] the convention in OddsPapi responses?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Standard match markets (1X2, BTTS, totals, handicaps) involve 'the team' rather than a specific player, so OddsPapi files them under the synthetic '0' key. Player prop markets use the actual player's ID. Filtering on '0' cleanly separates game-level markets from props.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Why does active=True sometimes ship with price=0?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"It's an upstream feed quirk on a small number of bookmakers (Bet365 is the most frequent). The book's UI hasn't priced the market yet but their API endpoint returns the slot anyway. Always filter price > 0 in addition to active.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can I store the OddsPapi DataFrame to disk between runs?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes \u2014 Parquet is the right format. df.to_parquet('odds.parquet') is faster than CSV and preserves dtypes. For long-running collection, append to a partitioned dataset: df.to_parquet(f'odds\/{fixture_id}\/{ts}.parquet').\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What about Polars instead of pandas for odds analysis?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Polars uses the same flatten-then-analyse pattern. The 10-line snippet works as-is \u2014 change pd.DataFrame(...) to pl.DataFrame(...) and your groupby\/agg calls are nearly identical. For datasets above 5M rows, Polars is a clear win. Below that, pandas is fine and has the larger ecosystem.\"\n      }\n    }\n  ]\n}\n<\/script><\/p>\n<p><!--\nFocus Keyphrase: pandas odds api\nSEO Title: Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines\nMeta Description: Convert nested OddsPapi JSON into a clean pandas DataFrame in 10 lines. Live odds from 350+ bookmakers, vig calculator, arb scanner \u2014 full Python tutorial.\nSlug: pandas-odds-api-dataframe-tutorial\n--><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Convert nested OddsPapi JSON into a clean pandas DataFrame in 10 lines. Live odds from 350+ bookmakers, vig calculator, arb scanner \u2014 full Python tutorial.<\/p>\n","protected":false},"author":2,"featured_media":2911,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[8,9,62,11,10],"class_list":["post-2909","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to-guides","tag-free-api","tag-odds-api","tag-pandas","tag-python","tag-sports-betting-api"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines | 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\/pandas-odds-api-dataframe-tutorial\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines | Odds API Development Blog\" \/>\n<meta property=\"og:description\" content=\"Convert nested OddsPapi JSON into a clean pandas DataFrame in 10 lines. Live odds from 350+ bookmakers, vig calculator, arb scanner \u2014 full Python tutorial.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/\" \/>\n<meta property=\"og:site_name\" content=\"Odds API Development Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-05-20T10:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-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=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/\"},\"author\":{\"name\":\"Odds API Writer\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\"},\"headline\":\"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines\",\"datePublished\":\"2026-05-20T10:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/\"},\"wordCount\":1730,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp\",\"keywords\":[\"Free API\",\"Odds API\",\"Pandas\",\"Python\",\"Sports Betting API\"],\"articleSection\":[\"How To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/\",\"name\":\"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines | Odds API Development Blog\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp\",\"datePublished\":\"2026-05-20T10:00:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp\",\"width\":2560,\"height\":1429,\"caption\":\"Pandas + Odds API - OddsPapi API Blog\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/oddspapi.io\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines\"}]},{\"@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":"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines | 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\/pandas-odds-api-dataframe-tutorial\/","og_locale":"en_US","og_type":"article","og_title":"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines | Odds API Development Blog","og_description":"Convert nested OddsPapi JSON into a clean pandas DataFrame in 10 lines. Live odds from 350+ bookmakers, vig calculator, arb scanner \u2014 full Python tutorial.","og_url":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/","og_site_name":"Odds API Development Blog","article_published_time":"2026-05-20T10:00:00+00:00","og_image":[{"width":2560,"height":1429,"url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-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":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#article","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/"},"author":{"name":"Odds API Writer","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13"},"headline":"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines","datePublished":"2026-05-20T10:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/"},"wordCount":1730,"commentCount":0,"publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp","keywords":["Free API","Odds API","Pandas","Python","Sports Betting API"],"articleSection":["How To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/","url":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/","name":"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines | Odds API Development Blog","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp","datePublished":"2026-05-20T10:00:00+00:00","breadcrumb":{"@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#primaryimage","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/05\/pandas-odds-api-dataframe-tutorial-scaled.webp","width":2560,"height":1429,"caption":"Pandas + Odds API - OddsPapi API Blog"},{"@type":"BreadcrumbList","@id":"https:\/\/oddspapi.io\/blog\/pandas-odds-api-dataframe-tutorial\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/oddspapi.io\/blog\/"},{"@type":"ListItem","position":2,"name":"Pandas + Odds API: Live Bookmaker Odds to DataFrame in 10 Lines"}]},{"@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\/2909","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=2909"}],"version-history":[{"count":1,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2909\/revisions"}],"predecessor-version":[{"id":2910,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2909\/revisions\/2910"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media\/2911"}],"wp:attachment":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media?parent=2909"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/categories?post=2909"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/tags?post=2909"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}