{"id":3684,"date":"2026-09-01T10:00:00","date_gmt":"2026-09-01T10:00:00","guid":{"rendered":"https:\/\/oddspapi.io\/blog\/?p=3684"},"modified":"2026-09-07T14:01:55","modified_gmt":"2026-09-07T14:01:55","slug":"us-open-odds-api","status":"publish","type":"post","link":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/","title":{"rendered":"US Open Odds API: 155 Books a Match Across 120 First-Round Ties"},"content":{"rendered":"<p>You want US Open odds in Python. You pull the tournament, get 253 fixtures back, and half of them have participants called <code>R64P49<\/code>. Then you check the sharp price and Pinnacle has quoted the first day of the draw and nothing else.<\/p>\n<p>Neither of those is a broken feed. A Grand Slam draw is published as a full bracket the moment the draw is made, and the empty rows are the slots nobody has won yet. Pinnacle opens the board one day at a time, and it moves fast enough that two censuses three hours apart disagree. This post shows you the queries, the numbers behind both behaviours, and the parser guards that stop them from wrecking your board.<\/p>\n<p>Everything below comes off the live <a href=\"https:\/\/oddspapi.io\/blog\/free-odds-api-350-bookmakers\/\">OddsPapi free tier<\/a> on 24 August 2026. The board was measured twice that day: once at about 09:00 UTC and again at about 11:45 UTC. Where the two readings differ, both are printed with their times.<\/p>\n<h2>What the US Open board actually looks like<\/h2>\n<p>Two tournament IDs carry the singles draws. Men&#8217;s Singles is <code>2591<\/code>, Women&#8217;s Singles is <code>2595<\/code>.<\/p>\n<p>The row called plain &#8220;US Open&#8221; is <code>2589<\/code> and it holds zero fixtures. So do the juniors, wheelchairs and legends rows. If you search the tournament list for the obvious name and take the first hit, you get an empty draw and conclude the Slam is not covered.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Measure<\/th>\n<th>Men&#8217;s Singles (2591)<\/th>\n<th>Women&#8217;s Singles (2595)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Bracket rows returned<\/td>\n<td>126<\/td>\n<td>127<\/td>\n<\/tr>\n<tr>\n<td>Rows with odds at 09:00 UTC<\/td>\n<td>64<\/td>\n<td>64<\/td>\n<\/tr>\n<tr>\n<td>Rows still priced at 11:45 UTC<\/td>\n<td>62<\/td>\n<td>58<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Eight first-day matches had started by the second run and dropped their live prices. That leaves 120 matches as the sample for every board-wide figure below.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Board census, 120 first-round matches<\/th>\n<th>Value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Books per fixture (median)<\/td>\n<td>155.5<\/td>\n<\/tr>\n<tr>\n<td>Books per fixture (max)<\/td>\n<td>174<\/td>\n<\/tr>\n<tr>\n<td>Books per fixture (min)<\/td>\n<td>37<\/td>\n<\/tr>\n<tr>\n<td>Prices sampled<\/td>\n<td>1,557,204<\/td>\n<\/tr>\n<tr>\n<td>Prices flagged <code>active: true<\/code><\/td>\n<td>765,387 (49.2%)<\/td>\n<\/tr>\n<tr>\n<td>Distinct market IDs<\/td>\n<td>434<\/td>\n<\/tr>\n<tr>\n<td>Market families<\/td>\n<td>57<\/td>\n<\/tr>\n<tr>\n<td>Books ranked on the match winner<\/td>\n<td>145<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h3>112 rows have no players in them<\/h3>\n<p>Future rounds ship as stubs. The participant names follow the pattern <code>R{round}P{position}<\/code>, so the second round arrives as <code>R64P49<\/code> against <code>R64P50<\/code>. Both draws carry 56 of these, 112 rows in total, every one of them <code>hasOdds: false<\/code>. Call <code>\/odds<\/code> on a stub and you get fixture metadata with no <code>bookmakerOdds<\/code> key at all.<\/p>\n<p>Their dates are stubs too. All 32 second-round rows in each draw carry the same timestamp, all 16 third-round rows carry the next one, and so on down to the final. That is a bracket layout, not an order of play. Do not build a schedule off it.<\/p>\n<p>One more count worth knowing: 141 rows carry two real player names, but only 128 of them had odds at the first census. The other 13 are resolved bracket slots that no book has priced yet.<\/p>\n<h2>The finding: Pinnacle opens a Slam draw by draw, and you can watch it happen<\/h2>\n<p>At 09:00 UTC, Pinnacle and SBOBet had priced 63 of the 64 first-day matches and none of the 64 second-day matches. Their covered sets were identical, same 63 fixtures, no exceptions.<\/p>\n<p>Re-measured at 11:45 UTC the same morning, Pinnacle had opened 25 of the 32 men&#8217;s second-day matches. The women&#8217;s second day was still at zero. SBOBet was still at zero across both.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Draw and day<\/th>\n<th>Fixtures still priced<\/th>\n<th>pinnacle<\/th>\n<th>sbobet<\/th>\n<th>bet365<\/th>\n<th>kalshi<\/th>\n<th>polymarket<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Men, 24 Aug (day 1)<\/td>\n<td>30<\/td>\n<td>30<\/td>\n<td>29<\/td>\n<td>28<\/td>\n<td>30<\/td>\n<td>30<\/td>\n<\/tr>\n<tr>\n<td>Men, 25 Aug (day 2)<\/td>\n<td>32<\/td>\n<td>25<\/td>\n<td>0<\/td>\n<td>31<\/td>\n<td>32<\/td>\n<td>32<\/td>\n<\/tr>\n<tr>\n<td>Women, 24 Aug (day 1)<\/td>\n<td>26<\/td>\n<td>26<\/td>\n<td>26<\/td>\n<td>26<\/td>\n<td>26<\/td>\n<td>26<\/td>\n<\/tr>\n<tr>\n<td>Women, 25 Aug (day 2)<\/td>\n<td>32<\/td>\n<td>0<\/td>\n<td>0<\/td>\n<td>31<\/td>\n<td>32<\/td>\n<td>32<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Zero to 25 of 32 in under three hours, on one draw, while the other draw stayed empty. That is an open in progress, not a finished coverage map. A census taken at one moment describes that moment.<\/p>\n<p>Free <a href=\"https:\/\/oddspapi.io\/blog\/backtest-betting-model-free-historical-odds\/\">historical odds<\/a> give you the open time for nothing, and that is the number to anchor on. First-snapshot timestamps on five first-day matches put Pinnacle at a median of <strong>T-6.2 hours<\/strong>, range 6.0 to 7.4. Query the same endpoint at 09:00 for five second-day matches and Pinnacle returns nothing, because at that point it had not quoted them. Three hours later most of the men&#8217;s second day had a price.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Bookmaker<\/th>\n<th>Opened, day-one match<\/th>\n<th>Opened, day-two match<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>pinnacle<\/td>\n<td>T-6.2h<\/td>\n<td>not yet quoted at 09:00 UTC<\/td>\n<\/tr>\n<tr>\n<td>bet365<\/td>\n<td>T-5.4h<\/td>\n<td>T-29.4h<\/td>\n<\/tr>\n<tr>\n<td>draftkings<\/td>\n<td>T-15.8h<\/td>\n<td>T-37.3h to T-40.5h<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>bet365&#8217;s numbers are the giveaway. All five day-one matches opened at T-5.4h and all five day-two matches at T-29.4h, matched to the minute, because both sets landed in one sweep at 09:34 UTC. bet365 posts the whole two-day board in a single batch. Pinnacle walks it match by match, and it walks a whole draw at a time.<\/p>\n<p>Compare that to football, where Pinnacle opens a league fixture a median of 47 days out. On a Slam you get about a quarter of a day. A cron job that polls at midnight collects a board with no sharp price on it, and a coverage audit run 48 hours ahead reports that Pinnacle does not cover the US Open. Poll the day&#8217;s matches in the morning, then poll again.<\/p>\n<h3>Old way versus OddsPapi<\/h3>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Job<\/th>\n<th>Scraping or a single-book API<\/th>\n<th>OddsPapi<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Get the draw<\/td>\n<td>Parse the tournament site&#8217;s bracket HTML<\/td>\n<td><code>\/fixtures<\/code> with <code>tournamentId<\/code><\/td>\n<\/tr>\n<tr>\n<td>Sharp benchmark<\/td>\n<td>Pinnacle has no public API<\/td>\n<td><code>pinnacle<\/code> slug in the same payload<\/td>\n<\/tr>\n<tr>\n<td>Find the open time<\/td>\n<td>Poll and hope you started early enough<\/td>\n<td><code>\/historical-odds<\/code>, free tier<\/td>\n<\/tr>\n<tr>\n<td>Set and game handicaps<\/td>\n<td>Rebuild from scratch per book<\/td>\n<td>Native market families with a handicap field<\/td>\n<\/tr>\n<tr>\n<td>Book count<\/td>\n<td>One per integration you write<\/td>\n<td>155 at the median on a first-round match, 350+ in the catalogue<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>The active-rate inversion: half the wide board is posted and suspended<\/h2>\n<p>The first census read 19 bookmakers, because the API key it ran under could only see 19 of the 352 in the catalogue. Across those 19 books, 63,840 of 67,752 prices were <code>active: true<\/code>, a rate of <strong>94.2%<\/strong>.<\/p>\n<p>The same board read with an uncapped key gives 1,557,204 prices across 145 ranked books, and <strong>49.2%<\/strong> of them are active. The wide board is half dead.<\/p>\n<p>The two numbers are not in conflict. They measure different populations. The books you already know are the ones that keep their prices live, so a sample built from them reports a high active rate and hides the shape of everything behind it. Add 126 more books and half of what arrives is a line that was posted and then suspended.<\/p>\n<p>This changes what a book count means in your code. A fixture with 174 books on it does not give you 174 usable quotes. Test <code>active<\/code> on the price object before a book enters a consensus, a best-price scan or a coverage report, and expect to throw away about half of what the payload hands you.<\/p>\n<h2>Step 1: find the draw<\/h2>\n<p>Auth is a query parameter. Every call takes <code>?apiKey=<\/code>, never a header. The free tier limits per endpoint and returns a real HTTP 429 with a <code>retryMs<\/code> you should honour.<\/p>\n<pre class=\"wp-block-code\"><code>import requests, time, re\n\nAPI_KEY = \"YOUR_API_KEY\"\nBASE_URL = \"https:\/\/api.oddspapi.io\/v4\"\nSPORT_TENNIS = 12\n\ndef call(path, **params):\n    \"\"\"Single request with 429 back-off. The free tier limits per endpoint.\"\"\"\n    params[\"apiKey\"] = API_KEY\n    for _ in range(5):\n        r = requests.get(f\"{BASE_URL}{path}\", params=params, timeout=30)\n        if r.status_code == 429:\n            time.sleep(r.json()[\"error\"].get(\"retryMs\", 1500) \/ 1000 + 0.3)\n            continue\n        if r.status_code == 404:      # empty window, not an error\n            return []\n        r.raise_for_status()\n        return r.json()\n    r.raise_for_status()\n\ntours = call(\"\/tournaments\", sportId=SPORT_TENNIS)\nfor t in tours:\n    if \"us open\" in t[\"tournamentName\"].lower():\n        print(f\"{t['tournamentId']:&gt;6}  {t['tournamentName']:&lt;34} \"\n              f\"{t['categoryName']:&lt;18} future={t['futureFixtures']}\")<\/code><\/pre>\n<pre class=\"wp-block-code\"><code>  2589  US Open                            ATP                future=0\n  2591  US Open Men Singles                ATP                future=127\n  2593  US Open Men Doubles                ATP                future=0\n  2595  US Open Women Singles              WTA                future=127\n  2597  US Open Women Doubles              WTA                future=25\n  2599  US Open Mixed Doubles              ATP                future=17<\/code><\/pre>\n<p>Twenty-one rows match &#8220;US Open&#8221; and four of them carry fixtures. Read <code>futureFixtures<\/code> before you pick one. The same trick works for the other Slams: filter the tennis tournament list by name, then read the counts.<\/p>\n<h2>Step 2: split the bracket from the board<\/h2>\n<p><code>tournamentId<\/code> is undocumented on <code>\/fixtures<\/code> but it works, and it cuts the payload by roughly 20x. Note the date window rule: <code>to<\/code> is a midnight-UTC instant, so set it to the day <em>after<\/em> the last day you want.<\/p>\n<pre class=\"wp-block-code\"><code>PLACEHOLDER = re.compile(r\"^R\\d+P\\d+$\")\n\ndef is_playable(f):\n    return not (PLACEHOLDER.match(f[\"participant1Name\"] or \"\")\n                or PLACEHOLDER.match(f[\"participant2Name\"] or \"\"))\n\ndraw = call(\"\/fixtures\", sportId=SPORT_TENNIS, tournamentId=2591,\n            **{\"from\": \"2026-08-24\", \"to\": \"2026-09-03\"})\n\nplayable = [f for f in draw if is_playable(f) and f[\"hasOdds\"]]\nbracket  = [f for f in draw if not is_playable(f)]\n\nprint(f\"rows returned : {len(draw)}\")\nprint(f\"real matches  : {len(playable)}\")\nprint(f\"bracket stubs : {len(bracket)}  e.g. \"\n      f\"{bracket[0]['participant1Name']} v {bracket[0]['participant2Name']}\")<\/code><\/pre>\n<pre class=\"wp-block-code\"><code>rows returned : 96\nreal matches  : 64\nbracket stubs : 32  e.g. R64P49 v R64P50<\/code><\/pre>\n<h3>Do not trust externalProviders as a coverage signal<\/h3>\n<p>Every one of the 253 bracket rows carries all ten provider IDs: <code>betradarId<\/code>, <code>opticoddsId<\/code>, <code>sofascoreId<\/code>, <code>flashscoreId<\/code>, <code>betgeniusId<\/code>, <code>lsportsId<\/code>, <code>mollybetId<\/code>, <code>txoddsId<\/code>, <code>oddinId<\/code> and <code>pinnacleId<\/code>. Complete on all 253, stubs included.<\/p>\n<p>That includes a <code>pinnacleId<\/code> on the second-day matches Pinnacle had not quoted at 09:00, and on rows whose players do not exist yet. The ID is a mapping key for joining feeds, which makes it useful for <a href=\"https:\/\/oddspapi.io\/blog\/fixture-mapping-compliance-betradar\/\">fixture mapping<\/a>. It says nothing about whether a book has posted a price.<\/p>\n<h2>Step 3: pull one board and dedupe it<\/h2>\n<p>Match winner is market <code>121<\/code>. Outcome <code>121<\/code> is participant 1 and <code>122<\/code> is participant 2, and the catalogue names them literally &#8220;1&#8221; and &#8220;2&#8221;, so carry the player names over from <code>\/fixtures<\/code> by joining on <code>fixtureId<\/code>.<\/p>\n<p>Three guards belong in this function. Skip books flagged <code>suspended<\/code>. Require both legs before you compute a margin. Test <code>active<\/code> on the price object, one level below the outcome.<\/p>\n<pre class=\"wp-block-code\"><code>WINNER, P1, P2 = \"121\", \"121\", \"122\"\n\ndef winner_quotes(fixture_id):\n    payload = call(\"\/odds\", fixtureId=fixture_id)\n    quotes = {}\n    for slug, book in payload.get(\"bookmakerOdds\", {}).items():\n        if book.get(\"suspended\"):\n            continue\n        market = book[\"markets\"].get(WINNER)\n        if not market:\n            continue\n        legs = {}\n        for oid in (P1, P2):\n            outcome = market[\"outcomes\"].get(oid)\n            if not outcome:\n                continue\n            price = outcome[\"players\"].get(\"0\")\n            if price and price[\"active\"]:\n                legs[oid] = price[\"price\"]\n        if len(legs) == 2:                       # never de-vig a half quote\n            quotes[slug] = (legs[P1], legs[P2])\n    return quotes\n\nfixture = playable[0]\nq = winner_quotes(fixture[\"fixtureId\"])\n\nseen = {}\nfor slug, tup in q.items():\n    seen.setdefault(tup, []).append(slug)\n\nprint(f\"{fixture['participant1Name']} v {fixture['participant2Name']}\")\nprint(f\"{len(q)} slugs -&gt; {len(seen)} independent quotes\")\nfor tup, slugs in sorted(seen.items(), key=lambda kv: 1\/kv[0][0] + 1\/kv[0][1]):\n    a, b = tup\n    print(f\"  {'\/'.join(sorted(slugs)):&lt;45} {a:&gt;6.3f} {b:&gt;6.3f}\"\n          f\"   margin {100*(1\/a+1\/b-1):5.2f}%\")<\/code><\/pre>\n<p>The output below is the 09:00 run, made with a key capped at 19 readable books. The code path is the same one that produces the full board, so it is kept as a readable worked example. The board-wide numbers underneath it come from the uncapped 11:45 run.<\/p>\n<pre class=\"wp-block-code\"><code>Lajal, Mark v Lee, Jordan\n15 slugs -> 10 independent quotes\n  kalshi\/polymarket                              1.124  8.333   margin  0.97%\n  draftkings                                     1.119  6.510   margin  4.73%\n  bet365                                         1.100  7.000   margin  5.19%\n  pinnacle                                       1.090  7.200   margin  5.63%\n  fanduel                                        1.080  7.600   margin  5.75%\n  pointsbet.com.au                               1.090  7.000   margin  6.03%\n  sbobet                                         1.100  6.550   margin  6.18%\n  hardrockbet                                    1.091  6.750   margin  6.47%\n  ballybet\/betparx\/betrivers\/fourwinds           1.090  6.750   margin  6.56%\n  betmgm\/borgata                                 1.090  6.500   margin  7.13%<\/code><\/pre>\n<p>Run the same loop across the full board and the collapse is <strong>59.1%<\/strong>: 16,490 slug-quotes become 6,746 independent ones across 120 matches. Three of every five bookmaker rows on a Grand Slam board are a duplicate feed.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Group<\/th>\n<th>Slugs<\/th>\n<th>Matches with an identical price<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Unibet family: <code>unibet<\/code>, <code>unibet.be<\/code>, <code>unibet.com.au<\/code>, <code>unibet.dk<\/code>, <code>unibet.ie<\/code>, <code>unibet.nl<\/code>, <code>unibet.se<\/code>, <code>atg.se<\/code>, <code>ballybet<\/code>, <code>betcity.nl<\/code>, <code>betmgm.co.uk<\/code>, <code>betparx<\/code>, <code>betplay<\/code>, <code>betrivers<\/code>, <code>betuk<\/code>, <code>bingoal.be<\/code>, <code>casumo<\/code>, <code>expekt.se<\/code>, <code>fourwinds<\/code>, <code>grosvenor<\/code>, <code>jacks.nl<\/code>, <code>kto<\/code>, <code>leovegas<\/code>, <code>leovegas.es<\/code>, <code>leovegas.it<\/code>, <code>paf<\/code>, <code>paf.es<\/code>, <code>prolineplus<\/code>, <code>scooore.be<\/code>, <code>svenskaspel<\/code>, <code>tabtouch<\/code>, <code>virginbet<\/code><\/td>\n<td>32<\/td>\n<td>64 of 120<\/td>\n<\/tr>\n<tr>\n<td><code>1xbet<\/code>, <code>22bet<\/code><\/td>\n<td>2<\/td>\n<td>107<\/td>\n<\/tr>\n<tr>\n<td><code>betfury<\/code>, <code>blaze<\/code>, <code>gamdom<\/code>, <code>megadice<\/code>, <code>rainbet<\/code><\/td>\n<td>5<\/td>\n<td>74<\/td>\n<\/tr>\n<tr>\n<td><code>napoleonsports.be<\/code>, <code>superbet.bet.br<\/code>, <code>superbet.ro<\/code>, <code>superbet.rs<\/code><\/td>\n<td>4<\/td>\n<td>73<\/td>\n<\/tr>\n<tr>\n<td><code>888sport.it<\/code>, <code>rushbet.co<\/code><\/td>\n<td>2<\/td>\n<td>69<\/td>\n<\/tr>\n<tr>\n<td><code>netbet<\/code>, <code>netbet.co.uk<\/code><\/td>\n<td>2<\/td>\n<td>66<\/td>\n<\/tr>\n<tr>\n<td><code>betway<\/code>, <code>betway.de<\/code>, <code>betway.es<\/code><\/td>\n<td>3<\/td>\n<td>59<\/td>\n<\/tr>\n<tr>\n<td><code>888sport<\/code>, <code>888sport.de<\/code>, <code>888sport.dk<\/code>, <code>888sport.es<\/code>, <code>888sport.ro<\/code>, <code>mrgreen<\/code>, <code>mrgreen.dk<\/code><\/td>\n<td>7<\/td>\n<td>58<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>The small groups from the capped sample still hold. <code>betmgm<\/code> and <code>borgata<\/code> matched on 108 matches, and <code>ballybet<\/code>, <code>betparx<\/code>, <code>betrivers<\/code> and <code>fourwinds<\/code> matched on 102, though those four now show up inside the much larger Unibet block. <code>\/bookmakers<\/code> reports <code>cloneOf: null<\/code> for all of them. Kalshi and Polymarket also landed on the same tuple 14 times, which is coincidence at a round number rather than a shared feed. Dedupe on the price tuple, per fixture, and never off a static list.<\/p>\n<h2>Book margins across 120 matches<\/h2>\n<p>145 books quoted the match winner on at least 60 of the 120 matches. Here is the top of that ranking by median margin, plus where the familiar US names land.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Rank<\/th>\n<th>Bookmaker<\/th>\n<th>Median winner margin<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1<\/td>\n<td><code>kalshi<\/code><\/td>\n<td>1.00%<\/td>\n<\/tr>\n<tr>\n<td>2<\/td>\n<td><code>polymarket.us<\/code><\/td>\n<td>1.00%<\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td><code>polymarket<\/code><\/td>\n<td>1.01%<\/td>\n<\/tr>\n<tr>\n<td>4<\/td>\n<td><code>betfair-ex<\/code><\/td>\n<td>1.30%<\/td>\n<\/tr>\n<tr>\n<td>5<\/td>\n<td><code>prophetx<\/code><\/td>\n<td>2.04%<\/td>\n<\/tr>\n<tr>\n<td>6<\/td>\n<td><code>duel<\/code><\/td>\n<td>2.05%<\/td>\n<\/tr>\n<tr>\n<td>7<\/td>\n<td><code>novig.us<\/code><\/td>\n<td>2.48%<\/td>\n<\/tr>\n<tr>\n<td>8<\/td>\n<td><code>sx.bet<\/code><\/td>\n<td>2.50%<\/td>\n<\/tr>\n<tr>\n<td>9<\/td>\n<td><code>dafabet<\/code><\/td>\n<td>4.00%<\/td>\n<\/tr>\n<tr>\n<td>10<\/td>\n<td><code>betpawa.cm<\/code><\/td>\n<td>4.02%<\/td>\n<\/tr>\n<tr>\n<td>11<\/td>\n<td><code>inbet<\/code><\/td>\n<td>4.34%<\/td>\n<\/tr>\n<tr>\n<td>12<\/td>\n<td><code>pinnacle<\/code><\/td>\n<td>4.69%<\/td>\n<\/tr>\n<tr>\n<td>13<\/td>\n<td><code>betway<\/code><\/td>\n<td>4.76%<\/td>\n<\/tr>\n<tr>\n<td>14<\/td>\n<td><code>betway.de<\/code><\/td>\n<td>4.76%<\/td>\n<\/tr>\n<tr>\n<td>15<\/td>\n<td><code>betway.es<\/code><\/td>\n<td>4.76%<\/td>\n<\/tr>\n<tr>\n<td>16<\/td>\n<td><code>draftkings<\/code><\/td>\n<td>4.83%<\/td>\n<\/tr>\n<tr>\n<td>25<\/td>\n<td><code>fanduel<\/code><\/td>\n<td>5.59%<\/td>\n<\/tr>\n<tr>\n<td>99<\/td>\n<td><code>bet365<\/code><\/td>\n<td>7.24%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Pinnacle is rank 12 of 145. Eleven books beat it and eight of those are exchanges or prediction markets, so three ordinary sportsbooks price the winner tighter than the sharp does. On a 19-book sample Pinnacle looked like third place behind Kalshi and Polymarket. Widen the board and it drops nine places. The lesson is about the sample, not about Pinnacle: a ranking built from the books you already integrated tells you where you sit among your own books.<\/p>\n<p>bet365&#8217;s count needs a footnote. It quotes 115 of the 120 matches, and only 100 of those carry a usable two-sided active winner. On 14 it ships both outcomes with <code>active: false<\/code>, and on one it omits the market. The prices are still sitting there in the payload. Skip the <code>active<\/code> check and you feed a dead bet365 line into your <a href=\"https:\/\/oddspapi.io\/blog\/consensus-odds-fair-odds-calculator-python\/\">consensus<\/a>.<\/p>\n<p>De-vigging Pinnacle on the worked fixture from step 3 gives you the reference number. 1.090 and 7.200 sum to 105.63%, and proportional de-vig returns 86.85% and 13.15%, so fair prices of 1.151 and 7.605. Use it as a fair-price anchor, not as a signal to bet: see the <a href=\"https:\/\/oddspapi.io\/blog\/no-vig-odds-api\/\">three de-vig methods<\/a> for where proportional breaks down on long shots.<\/p>\n<h2>The negative result: tennis has no cheap market<\/h2>\n<p>On football, a sharp charges you more on corners than on the match result, and SBOBet runs a 10% three-way next to a 3% Asian handicap. Both patterns die on tennis.<\/p>\n<p>Here is each book&#8217;s tightest rung in each family, median across the 128 matches of the 09:00 census. These are each book&#8217;s own prices, so the figures stand whatever else is on the board.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Market family<\/th>\n<th>pinnacle<\/th>\n<th>sbobet<\/th>\n<th>draftkings<\/th>\n<th>hardrockbet<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Winner<\/td>\n<td>4.63%<\/td>\n<td>6.00%<\/td>\n<td>4.82%<\/td>\n<td>6.29%<\/td>\n<\/tr>\n<tr>\n<td>Game Handicap<\/td>\n<td>4.78%<\/td>\n<td>6.11%<\/td>\n<td>8.86%<\/td>\n<td>7.42%<\/td>\n<\/tr>\n<tr>\n<td>Set Handicap<\/td>\n<td>4.74%<\/td>\n<td>7.03%<\/td>\n<td>6.59%<\/td>\n<td>8.33%<\/td>\n<\/tr>\n<tr>\n<td>Total Games Over Under<\/td>\n<td>4.77%<\/td>\n<td>6.11%<\/td>\n<td>8.77%<\/td>\n<td>6.94%<\/td>\n<\/tr>\n<tr>\n<td>Total Sets Over Under<\/td>\n<td>4.80%<\/td>\n<td>n\/a<\/td>\n<td>8.33%<\/td>\n<td>6.83%<\/td>\n<\/tr>\n<tr>\n<td>First Set Winner<\/td>\n<td>4.89%<\/td>\n<td>5.87%<\/td>\n<td>6.56%<\/td>\n<td>6.97%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Pinnacle sits between 4.63% and 4.89% on all six. A spread of 0.26 percentage points across the whole menu means it sets one margin for the match and applies it everywhere, then moves the fair line underneath. There is no derivative market to go hunting in.<\/p>\n<p>SBOBet is 6.00% on the winner and 6.11% on the game handicap. The handicap discount that shows up on six football competitions is not there. If you carried that rule over from a <a href=\"https:\/\/oddspapi.io\/blog\/why-sharps-bet-asian-handicap\/\">football handicap study<\/a>, drop it for tennis.<\/p>\n<p>One flip does show up inside that four-book sample. bet365 is the widest of the four on the match winner and the tightest of them on first-set total games at 3.44%, ahead of Pinnacle&#8217;s 5.72%. Rank books per family, not once per sport, and rank them against the board you can actually read.<\/p>\n<h2>Step 4: resolve the total-games line without mainLine<\/h2>\n<p><code>mainLine<\/code> is worse on tennis than on any sport we have measured, and widening the board makes it worse still. Against the consensus total-games line it hits <strong>1,156 times out of 13,403<\/strong> across 154 ladder books, <strong>8.6%<\/strong>.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Bookmaker<\/th>\n<th>mainLine matched consensus<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>1xbet<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>22bet<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>napoleonsports.be<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>superbet.bet.br<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>superbet.pl<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>superbet.ro<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>superbet.rs<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>winbet.bg<\/code><\/td>\n<td>0.0%<\/td>\n<\/tr>\n<tr>\n<td><code>inbet<\/code><\/td>\n<td>1.7%<\/td>\n<\/tr>\n<tr>\n<td><code>polymarket<\/code><\/td>\n<td>49.2%<\/td>\n<\/tr>\n<tr>\n<td>All 154 ladder books<\/td>\n<td>8.6%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Eight books flag the wrong rung every single time across 120 matches. Treat the field as decoration.<\/p>\n<p>What works instead: take each book&#8217;s own most balanced rung, then take the mode across books. Median agreement is 82% across the 120 matches, which is enough to settle a line.<\/p>\n<pre class=\"wp-block-code\"><code>from collections import Counter\n\ncatalogue = call(\"\/markets\", sportId=SPORT_TENNIS)\nmeta = {str(m[\"marketId\"]): m for m in catalogue}\n\ndef total_games_line(fixture_id):\n    payload = call(\"\/odds\", fixtureId=fixture_id)\n    votes = []\n    for slug, book in payload.get(\"bookmakerOdds\", {}).items():\n        best = None\n        for mid, market in book[\"markets\"].items():\n            if meta.get(mid, {}).get(\"marketName\") != \"Total Games Over Under\":\n                continue\n            legs = [p for o in market[\"outcomes\"].values()\n                      for p in o[\"players\"].values() if p[\"active\"]]\n            if len(legs) != 2:\n                continue\n            imbalance = abs(1\/legs[0][\"price\"] - 1\/legs[1][\"price\"])\n            if best is None or imbalance &lt; best[0]:\n                best = (imbalance, meta[mid][\"handicap\"])\n        if best:\n            votes.append(best[1])\n    tally = Counter(votes)\n    line, agree = tally.most_common(1)[0]\n    return line, agree, len(votes), sorted(tally.items())\n\nprint(total_games_line(fixture[\"fixtureId\"]))<\/code><\/pre>\n<p>Output from the same 09:00 capped-key run, so the vote count is 14 books rather than the full board:<\/p>\n<pre class=\"wp-block-code\"><code>(19.5, 8, 14, [(19.5, 8), (20.0, 2), (20.5, 2), (22.5, 1), (23.5, 1)])<\/code><\/pre>\n<p>Eight of fourteen books land on 19.5. Ladder widths on that match: DraftKings 8 rungs, Pinnacle 6, SBOBet 5, Kalshi 3, bet365 2. Kalshi&#8217;s three rungs were 17.5, 22.5 and 27.5, so its &#8220;closest&#8221; rung sits two and a half games off the real line. Snapping a wide ladder to a consensus you built from narrow ones is how you end up comparing prices for different bets.<\/p>\n<p>Resolve every handicap and total by <code>marketName<\/code> plus <code>handicap<\/code> from <code>\/markets?sportId=12<\/code>. There is one market ID per line, so hardcoding an ID pins you to one number and returns empty dicts everywhere else. The board carries 434 distinct market IDs across 57 families, which is what one market ID per rung looks like at scale.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Market family<\/th>\n<th>Books quoting it<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Winner<\/td>\n<td>173<\/td>\n<\/tr>\n<tr>\n<td>Total Games Over Under<\/td>\n<td>155<\/td>\n<\/tr>\n<tr>\n<td>Game Handicap<\/td>\n<td>152<\/td>\n<\/tr>\n<tr>\n<td>First Set Winner<\/td>\n<td>145<\/td>\n<\/tr>\n<tr>\n<td>Participant 1 Total Games<\/td>\n<td>132<\/td>\n<\/tr>\n<tr>\n<td>Second Set Winner<\/td>\n<td>131<\/td>\n<\/tr>\n<tr>\n<td>Participant 2 Total Games<\/td>\n<td>130<\/td>\n<\/tr>\n<tr>\n<td>Set Handicap<\/td>\n<td>126<\/td>\n<\/tr>\n<tr>\n<td>Total Games First Set<\/td>\n<td>110<\/td>\n<\/tr>\n<tr>\n<td>Total Games Second Set<\/td>\n<td>99<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>Step 5: check whether the sharp price exists yet<\/h2>\n<p>This is the query that saves you. Before you score a match, ask <code>\/historical-odds<\/code> whether Pinnacle has ever quoted it.<\/p>\n<pre class=\"wp-block-code\"><code>def pinnacle_opened_at(fixture_id):\n    \"\"\"Returns the first Pinnacle snapshot, or None if it never priced this match.\"\"\"\n    for _ in range(5):\n        r = requests.get(f\"{BASE_URL}\/historical-odds\",\n                         params={\"apiKey\": API_KEY, \"fixtureId\": fixture_id,\n                                 \"bookmakers\": \"pinnacle\"}, timeout=60)\n        if r.status_code == 429:\n            time.sleep(r.json()[\"error\"].get(\"retryMs\", 4500) \/ 1000 + 0.5)\n            continue\n        break\n    books = r.json().get(\"bookmakers\", {}) if r.status_code == 200 else {}\n    snaps = [s[\"createdAt\"]\n             for m in books.get(\"pinnacle\", {}).get(\"markets\", {}).values()\n             for o in m[\"outcomes\"].values()\n             for hist in o[\"players\"].values()\n             for s in hist]\n    return min(snaps) if snaps else None\n\nfor f in (day_one_match, day_two_match):\n    opened = pinnacle_opened_at(f[\"fixtureId\"])\n    print(f\"{f['startTime'][:16]}  {f['participant1Name'][:20]:&lt;20} \"\n          f\"pinnacle opened: {opened or 'NEVER'}\")<\/code><\/pre>\n<pre class=\"wp-block-code\"><code>2026-08-24T15:00  Lajal, Mark          pinnacle opened: 2026-08-24T08:43:01.698Z\n2026-08-25T15:00  Antonius, Michael    pinnacle opened: NEVER<\/code><\/pre>\n<p>That <code>NEVER<\/code> is a reading at 09:00 UTC, and this is exactly why the function belongs in your pipeline rather than in a one-off audit. By 11:45 the same day, 25 of the 32 men&#8217;s second-day matches returned a timestamp from this call. Re-run it, do not cache the answer.<\/p>\n<p><code>\/historical-odds<\/code> is slower than <code>\/odds<\/code>. Allow roughly 4.5 seconds between calls, take a maximum of three bookmakers per call, and expect no market filter. Filtered to one book a first-round match comes back at about 0.1 MB, so a whole-draw sweep is cheap.<\/p>\n<h3>Pinnacle&#8217;s limits rank the market families<\/h3>\n<p>Only <code>pinnacle<\/code>, <code>kalshi<\/code> and <code>polymarket<\/code> populate <code>limit<\/code>. Every other book returns <code>null<\/code>, so null-check before any arithmetic.<\/p>\n<p>Pinnacle&#8217;s limit is a capped max win rather than a capped stake, so recover the base with <code>limit if price &gt;= 2 else limit * (price - 1)<\/code>. Median bases on the US Open first round:<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Market family<\/th>\n<th>Median implied base<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Winner<\/td>\n<td>$300<\/td>\n<\/tr>\n<tr>\n<td>Game Handicap<\/td>\n<td>$300<\/td>\n<\/tr>\n<tr>\n<td>Set Handicap \/ First Set Winner \/ Total Sets<\/td>\n<td>$287<\/td>\n<\/tr>\n<tr>\n<td>Total Games Over Under<\/td>\n<td>$162<\/td>\n<\/tr>\n<tr>\n<td>Per-player total games, first-set derivatives<\/td>\n<td>$125<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Three hundred dollars on the headline market of a Grand Slam first round, against $7,500 on an MLB moneyline. Pinnacle takes a fraction as much on early-round tennis, which is worth knowing before you treat its number as a settled fair price. More on reading limits as a confidence signal in the <a href=\"https:\/\/oddspapi.io\/blog\/betting-limits-api-stake-sizing\/\">betting limits guide<\/a>.<\/p>\n<h2>Exchange prices need a depth check<\/h2>\n<p>Exchanges and prediction markets take the top eight ranks on the winner. Kalshi sits at 1.00% and Polymarket at 1.01%, ahead of every sportsbook on the board. Those margins read as free money until you look at the ladder.<\/p>\n<p>Both ship <code>exchangeMeta<\/code> with a three-level <code>back<\/code> ladder where <code>limit = size \u00d7 cents<\/code> holds exactly. Score on the thinner side, because that is what caps your position.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Venue<\/th>\n<th>Matches<\/th>\n<th>Median margin<\/th>\n<th>Median thinnest-side stake<\/th>\n<th>Max<\/th>\n<th>Passes &lt;2% and \u2265$500<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>kalshi<\/code><\/td>\n<td>118<\/td>\n<td>1.00%<\/td>\n<td>$275.53<\/td>\n<td>$16,368<\/td>\n<td>45 of 118<\/td>\n<\/tr>\n<tr>\n<td><code>polymarket<\/code><\/td>\n<td>120<\/td>\n<td>1.01%<\/td>\n<td>$72.44<\/td>\n<td>$9,196<\/td>\n<td>28 of 120<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Kalshi clears the screen on 38% of the board and Polymarket on 23%. Polymarket&#8217;s median of $72 tells you most of its quotes are a price with nothing much behind them, and its top end of $9,196 tells you the distribution is long-tailed rather than uniform. Screen on margin and depth together. Neither one alone survives contact with a real stake.<\/p>\n<h2>Parser guards, collected<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Trap<\/th>\n<th>What you see<\/th>\n<th>Guard<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Bracket stubs<\/td>\n<td>112 rows named <code>R64P49<\/code><\/td>\n<td>Regex <code>^R\\d+P\\d+$<\/code> on both participant names<\/td>\n<\/tr>\n<tr>\n<td>Stub dates<\/td>\n<td>32 matches sharing one timestamp<\/td>\n<td>Never build a schedule from unplayed rounds<\/td>\n<\/tr>\n<tr>\n<td>Empty tournament row<\/td>\n<td>&#8220;US Open&#8221; (2589) returns nothing<\/td>\n<td>Read <code>futureFixtures<\/code> before picking an ID<\/td>\n<\/tr>\n<tr>\n<td>Coverage moves under you<\/td>\n<td>0 to 25 of 32 in three hours<\/td>\n<td>Re-poll the sharp benchmark, never cache &#8220;not covered&#8221;<\/td>\n<\/tr>\n<tr>\n<td>Suspended prices<\/td>\n<td>Half the wide board ships <code>active: false<\/code><\/td>\n<td>Test <code>active<\/code> on <code>players[\"0\"]<\/code>, not the outcome<\/td>\n<\/tr>\n<tr>\n<td>Dead bet365 winner<\/td>\n<td>Both legs present, both <code>active: false<\/code><\/td>\n<td>Require two live legs before a margin<\/td>\n<\/tr>\n<tr>\n<td><code>marketActive<\/code><\/td>\n<td>Records flagged true with every price dead<\/td>\n<td>Trust only the price-level <code>active<\/code> flag<\/td>\n<\/tr>\n<tr>\n<td>Duplicate feeds<\/td>\n<td>59.1% of slug-quotes collapse<\/td>\n<td>Dedupe on the price tuple, per fixture<\/td>\n<\/tr>\n<tr>\n<td><code>mainLine<\/code><\/td>\n<td>8.6% accurate<\/td>\n<td>Balanced rung per book, then mode across books<\/td>\n<\/tr>\n<tr>\n<td><code>pinnacleId<\/code><\/td>\n<td>Present on all 253 rows<\/td>\n<td>A mapping key, not a coverage signal<\/td>\n<\/tr>\n<tr>\n<td>Empty date window<\/td>\n<td>HTTP 404 with a JSON body<\/td>\n<td>Treat 404 as an empty list<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>What is not on the board<\/h2>\n<p>No player props. Tennis participants are the players, so every price is keyed <code>players[\"0\"]<\/code> and there is no aces or double-faults menu in the payload. The 57 families are all match, set and game markets: winner, correct score, set and game handicaps, total games and sets, first-set variants, tie breaks, odd\/even games.<\/p>\n<p>No outright winner market either. The draw is priced match by match, so a &#8220;win the tournament&#8221; post is not possible from this feed.<\/p>\n<p>Doubles is thin. Women&#8217;s Doubles carries 25 rows and Mixed Doubles 17, against 127 in each singles draw. Men&#8217;s Doubles was empty at the time of the census.<\/p>\n<h2>Get the board<\/h2>\n<p>Every number above came off the free tier. 350+ bookmakers in the catalogue, 155 at the median on a single first-round match, and historical snapshots you can query without paying for a backfill.<\/p>\n<p><a href=\"https:\/\/oddspapi.io\/\">Grab a free API key<\/a> and run the five steps against tomorrow&#8217;s order of play. Then read the <a href=\"https:\/\/oddspapi.io\/blog\/tennis-odds-api-live-atp-wta\/\">ATP and WTA tour guide<\/a> for the weeks between Slams, and <a href=\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\">line shopping in Python<\/a> for turning a 155-book board into one best price.<\/p>\n<h2>FAQ<\/h2>\n<h3>What is the US Open tournament ID in the OddsPapi API?<\/h3>\n<p>Men&#8217;s Singles is 2591 and Women&#8217;s Singles is 2595, both under sportId 12. The row named plain &#8220;US Open&#8221; is 2589 and holds no fixtures. Twenty-one tournament rows match the name, so read <code>futureFixtures<\/code> before you pick one.<\/p>\n<h3>Why do US Open fixtures have participants called R64P49?<\/h3>\n<p>Those are bracket stubs for rounds nobody has played yet. The name follows <code>R{round}P{position}<\/code>. All 112 of them carry <code>hasOdds: false<\/code>, and their dates are bracket placeholders rather than a real order of play. Filter them with a regex on both participant names.<\/p>\n<h3>Does Pinnacle cover the US Open?<\/h3>\n<p>Yes, and it opens the board draw by draw during the day. At 09:00 UTC on 24 August 2026 Pinnacle had priced 63 of the 64 first-day matches and none of the second day. By 11:45 the same morning it had opened 25 of the 32 men&#8217;s second-day matches, with the women&#8217;s second day still at zero. Historical snapshots put its median open at 6.2 hours before a match, so poll the order of play through the morning rather than auditing the whole draw once.<\/p>\n<h3>How many bookmakers price a US Open first-round match?<\/h3>\n<p>155.5 at the median, 174 at the maximum and 37 at the minimum across 120 first-round matches. Deduping identical price tuples removes 59.1% of the winner quotes: 16,490 slug-quotes across those matches become 6,746 independent ones. About half the prices on the wide board also ship <code>active: false<\/code>.<\/p>\n<h3>Which market ID is the tennis match winner?<\/h3>\n<p>Market 121, with outcome 121 for participant 1 and 122 for participant 2. Handicaps and totals use one market ID per line, so resolve them by <code>marketName<\/code> plus <code>handicap<\/code> from <code>\/markets?sportId=12<\/code> instead of hardcoding an ID.<\/p>\n<h3>Are there US Open player props in the API?<\/h3>\n<p>No. Every price is keyed <code>players[\"0\"]<\/code> and the 57 market families cover match, set and game markets only. There is also no outright tournament winner market.<\/p>\n<p><script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\"@type\":\"Question\",\"name\":\"What is the US Open tournament ID in the OddsPapi API?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Men's Singles is 2591 and Women's Singles is 2595, both under sportId 12. The row named plain \\\"US Open\\\" is 2589 and holds no fixtures. Twenty-one tournament rows match the name, so read futureFixtures before you pick one.\"}},\n    {\"@type\":\"Question\",\"name\":\"Why do US Open fixtures have participants called R64P49?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Those are bracket stubs for rounds nobody has played yet. The name follows R{round}P{position}. All 112 of them carry hasOdds: false, and their dates are bracket placeholders rather than a real order of play. Filter them with a regex on both participant names.\"}},\n    {\"@type\":\"Question\",\"name\":\"Does Pinnacle cover the US Open?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes, and it opens the board draw by draw during the day. At 09:00 UTC on 24 August 2026 Pinnacle had priced 63 of the 64 first-day matches and none of the second day. By 11:45 the same morning it had opened 25 of the 32 men's second-day matches, with the women's second day still at zero. Historical snapshots put its median open at 6.2 hours before a match, so poll the order of play through the morning rather than auditing the whole draw once.\"}},\n    {\"@type\":\"Question\",\"name\":\"How many bookmakers price a US Open first-round match?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"155.5 at the median, 174 at the maximum and 37 at the minimum across 120 first-round matches. Deduping identical price tuples removes 59.1% of the winner quotes: 16,490 slug-quotes across those matches become 6,746 independent ones. About half the prices on the wide board also ship active: false.\"}},\n    {\"@type\":\"Question\",\"name\":\"Which market ID is the tennis match winner?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Market 121, with outcome 121 for participant 1 and 122 for participant 2. Handicaps and totals use one market ID per line, so resolve them by marketName plus handicap from \/markets?sportId=12 instead of hardcoding an ID.\"}},\n    {\"@type\":\"Question\",\"name\":\"Are there US Open player props in the API?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"No. Every price is keyed players[\\\"0\\\"] and the 57 market families cover match, set and game markets only. There is also no outright tournament winner market.\"}}\n  ]\n}\n<\/script><\/p>\n<p><!--\nFocus Keyphrase: US Open odds API\nSEO Title: US Open Odds API: 155 Books a Match Across 120 First-Round Ties\nMeta Description: Pull live US Open odds from up to 174 bookmakers in Python, with the bracket walk, the dedupe step and the hour each round's board opens.\nSlug: us-open-odds-api\n--><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Pull live US Open odds from up to 174 bookmakers in Python, with the bracket walk, the dedupe step and the hour each round&#8217;s board opens.<\/p>\n","protected":false},"author":2,"featured_media":3685,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[8,9,11,10,25],"class_list":["post-3684","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to-guides","tag-free-api","tag-odds-api","tag-python","tag-sports-betting-api","tag-tennis"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>US Open Odds API: 155 Books a Match Across 120 First-Round Ties | 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\/us-open-odds-api\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"US Open Odds API: 155 Books a Match Across 120 First-Round Ties | OddsPapi Blog\" \/>\n<meta property=\"og:description\" content=\"Pull live US Open odds from up to 174 bookmakers in Python, with the bracket walk, the dedupe step and the hour each round&#039;s board opens.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/\" \/>\n<meta property=\"og:site_name\" content=\"OddsPapi Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-01T10:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-07T14:01:55+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-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=\"20 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/\"},\"author\":{\"name\":\"Odds API Writer\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\"},\"headline\":\"US Open Odds API: 155 Books a Match Across 120 First-Round Ties\",\"datePublished\":\"2026-09-01T10:00:00+00:00\",\"dateModified\":\"2026-09-07T14:01:55+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/\"},\"wordCount\":3149,\"commentCount\":2,\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp\",\"keywords\":[\"Free API\",\"Odds API\",\"Python\",\"Sports Betting API\",\"Tennis\"],\"articleSection\":[\"How To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/\",\"name\":\"US Open Odds API: 155 Books a Match Across 120 First-Round Ties | OddsPapi Blog\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp\",\"datePublished\":\"2026-09-01T10:00:00+00:00\",\"dateModified\":\"2026-09-07T14:01:55+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp\",\"width\":2560,\"height\":1429,\"caption\":\"US Open Odds API - OddsPapi API Blog\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/oddspapi.io\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"US Open Odds API: 155 Books a Match Across 120 First-Round Ties\"}]},{\"@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":"US Open Odds API: 155 Books a Match Across 120 First-Round Ties | 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\/us-open-odds-api\/","og_locale":"en_US","og_type":"article","og_title":"US Open Odds API: 155 Books a Match Across 120 First-Round Ties | OddsPapi Blog","og_description":"Pull live US Open odds from up to 174 bookmakers in Python, with the bracket walk, the dedupe step and the hour each round's board opens.","og_url":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/","og_site_name":"OddsPapi Blog","article_published_time":"2026-09-01T10:00:00+00:00","article_modified_time":"2026-09-07T14:01:55+00:00","og_image":[{"width":2560,"height":1429,"url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-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":"20 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#article","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/"},"author":{"name":"Odds API Writer","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13"},"headline":"US Open Odds API: 155 Books a Match Across 120 First-Round Ties","datePublished":"2026-09-01T10:00:00+00:00","dateModified":"2026-09-07T14:01:55+00:00","mainEntityOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/"},"wordCount":3149,"commentCount":2,"publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp","keywords":["Free API","Odds API","Python","Sports Betting API","Tennis"],"articleSection":["How To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/","url":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/","name":"US Open Odds API: 155 Books a Match Across 120 First-Round Ties | OddsPapi Blog","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp","datePublished":"2026-09-01T10:00:00+00:00","dateModified":"2026-09-07T14:01:55+00:00","breadcrumb":{"@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/oddspapi.io\/blog\/us-open-odds-api\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#primaryimage","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/us-open-odds-api-scaled.webp","width":2560,"height":1429,"caption":"US Open Odds API - OddsPapi API Blog"},{"@type":"BreadcrumbList","@id":"https:\/\/oddspapi.io\/blog\/us-open-odds-api\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/oddspapi.io\/blog\/"},{"@type":"ListItem","position":2,"name":"US Open Odds API: 155 Books a Match Across 120 First-Round Ties"}]},{"@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\/3684","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=3684"}],"version-history":[{"count":4,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/3684\/revisions"}],"predecessor-version":[{"id":3899,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/3684\/revisions\/3899"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media\/3685"}],"wp:attachment":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media?parent=3684"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/categories?post=3684"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/tags?post=3684"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}