# Data Explorer - Browse API Data Schema URL: https://sportsgameodds.com/docs/explorer --- # FAQ - Common Questions and Troubleshooting URL: https://sportsgameodds.com/docs/faq FAQ - Common Questions and Troubleshooting [#faq---common-questions-and-troubleshooting] Common questions about using the SportsGameOdds API. Don't see your question? [Contact us](/contact-us) or join our [Discord](https://discord.gg/HhP9E7ZZaE). *** [Email](mailto:api@sportsgameodds.com) · [Chat](/) · [Discord](https://discord.gg/HhP9E7ZZaE) --- # Support - Get Help with the API URL: https://sportsgameodds.com/docs/help Support - Get Help with the API [#support---get-help-with-the-api] Support times can vary, but we typically respond within 1 business day. We typically prioritize issues based on severity and impact. We also prioritize requests from customers on paid plans. FAQ [#faq] [Check this first.](/docs/faq) You may find you're not the first person to ask this question. Email [#email] [Send us an email](mailto:api@sportsgameodds.com) to ask questions, report issues, request new features, or anything else. Chat [#chat] [Chat with us](/) via the chat widget on our website. We'll respond if we're online. Otherwise, we'll get back to you as soon as we're back. Discord [#discord] [Join our Discord community](https://discord.gg/HhP9E7ZZaE) for real-time help, feature discussions, and community support. * Endpoint and parameters used (full URL) * Relevant response data * Expected response behavior * Code sample (if possible) Please use the dedicated support email you received during onboarding for priority support. If you don't have it, email us at [api@sportsgameodds.com](mailto:api@sportsgameodds.com) and we'll re-send it. --- # SportsGameOdds API - Sports Betting Odds Data for Developers URL: https://sportsgameodds.com/docs SportsGameOdds API - Sports Betting Odds Data for Developers [#sportsgameodds-api---sports-betting-odds-data-for-developers] The easiest and most powerful way to add sports and betting odds data to your application, model, or whatever else! Quick Links [#quick-links] * [Setup Guide](/docs/basics/setup) — Get your API key and start making requests * [Quickstart](/docs/basics/quickstart) — Make your first API call in minutes * [API Reference](/docs/reference) — Interactive API reference with all endpoints * [SDK](/docs/sdk) — Official SDK for JavaScript/TypeScript, Python, and more * [FAQ](/docs/faq) — Common questions and troubleshooting --- # API Reference - Endpoints and Parameters URL: https://sportsgameodds.com/docs/reference --- # SDK Guide - TypeScript, Python, Ruby, Go, Java URL: https://sportsgameodds.com/docs/sdk SDK Guide - TypeScript, Python, Ruby, Go, Java [#sdk-guide---typescript-python-ruby-go-java] While you can always make requests to the API directly, using our SDKs is an easy way to get started. Here you'll learn how to install, configure, and use our SDK across multiple programming languages with real-world examples. All code examples shown in this guide are available in the relevant [SportsGameOdds GitHub repositories](https://github.com/SportsGameOdds). Installation [#installation] Install the SDK using your language's package manager: --- # API Cheat Sheet - Quick Reference URL: https://sportsgameodds.com/docs/basics/cheat-sheet API Cheat Sheet - Quick Reference [#api-cheat-sheet---quick-reference] * Get an API key [here](/pricing). The key will be sent to your email. * Place your API key in the `apiKey` query param or the `x-api-key` header. * A full list of request endpoints and their parameters can be found [here](/docs/reference/). * Responses are in JSON format. The `data` field contains the data you queried for. * Use our [Data Explorer](/docs/explorer/) see the schema of the data that is returned. * The most commonly used endpoint is the `/events` endpoint. This returns a list of events with odds data. Common params include: * `oddsAvailable=true` - only return live/upcoming events with odds * `leagueID=NBA,NFL,MLB` - only return events for specified leagues * `oddID=points-home-game-ml-home,points-home-game-sp-home` - only return specified odds markets * `includeAltLines=true` - include alternate spread/over-under lines Endpoints [#endpoints] A full reference of all endpoints and their parameters can be found [here](/docs/reference/). The API is currently on version 2 so each endpoint is prefixed with `https://api.sportsgameodds.com/v2`. For example the `/leagues` endpoint should be accessed at `https://api.sportsgameodds.com/v2/leagues`. The main endpoint you'll use to fetch odds and scores/stats is the [`/events`](/docs/reference/#tag/events/GET/events/) endpoint. Authentication [#authentication] All requests require an [API key](/pricing) Place it in the header as `x-api-key`. ```javascript fetch("https://api.sportsgameodds.com/v2/events", { headers: { "x-api-key": "your-api-key-here" }, }); ``` Or, place it in the query params as `apiKey`. ```javascript fetch("https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY_GOES_HERE"); ``` Response Format [#response-format] All responses follow a consistent JSON structure: ```json { "success": true, "data": [...], "error": "...", "nextCursor": "...", } ``` The `success` field tells you if the request was successful and returned data or not. The `data` field contains a list of objects you queried for. The format of each object is dependent on the endpoint you called. The `error` field contains an error message. It's only present if `success` is false. The `nextCursor` field contains a cursor used to get the next page of data in some cases. More info [here](/docs/guides/data-batches). Example Request [#example-request] `https://api.sportsgameodds.com/v2/events?leagueID=NBA,NFL,MLB&oddsAvailable=true&limit=1&apiKey=YOUR_API_KEY` To test this, simply replace `YOUR_API_KEY` with your actual API key and visit this URL in your browser. You shoud see the next upcoming or live game for either NBA, NFL, or MLB along with all the odds data for that game. * `leagueID=NBA,NFL,MLB` - Tells the API to only return data for NBA, NFL, and MLB games * `oddsAvailable=true` - Tells the API to only return data for games where there are available/active odds markets * `limit=1` - Tells the API to only return 1 game. By default the API sorts results by start time in ascending order. * `apiKey=YOUR_API_KEY` - Authenticates your request. Data Schema [#data-schema] The best way to get a sense of how our data is structured is to play around with our [Data Explorer](/docs/explorer/). In general, the data is organized into the following hierarchy: 1. Sports - fairly self-explanatory. 2. Leagues - each sport has one or more leagues. 3. Teams - each league has one or more teams. 4. Events - each team has one or more events. The core unit that you'll likely be working with is the Event. Each Event represents a single game/match/fight/etc. The Event object also contains all odds markets for that event. An odds market represents both the type of bet and the side of the bet. Each has its own unique ID called an `oddID`. You can find data for a specific odds market on an Event object at the path: `Event.odds.`. If you're looking for bookmaker specific data, you can find it here `Event.odds..byBookmaker.`. Each oddID is a combination of other identifiers which uniquely identify the odds market. These identifiers are: * `statID` - the statistic being measured (ex: "points", "assists", "rebounds", etc.) * `statEntityID` - Who's performance on the stat is being measured (ex: "home", "away", "LEBRON\_JAMES", "all" (both teams), etc.) * `periodID` - the period being tracked (ex: full game ("game"), first half ("1h"), etc.) * `betTypeID` - the type of bet (ex: spread ("sp"), moneyline ("ml"), over/under ("ou"), etc.) * `sideID` - the side of the bet (ex: "home", "away", "over", "under", "yes", "no", etc.) By combining these identifiers, we can uniquely identify any odds market! Pretty cool, right? --- # Introduction - SportsGameOdds API URL: https://sportsgameodds.com/docs/basics Introduction - SportsGameOdds API [#introduction---sportsgameodds-api] Welcome to SportsGameOdds - We provide real-time odds, scores, stats and tons of other data for every major sports league in the world. Our system updates millions of data points every minute and serves them to you though this API. Whether you're working on an enterprise-level application or a personal side project, we want to make it dead simple to integrate sports and odds data into your application. Philosophy [#philosophy] * **Good odds data shouldn't cost tens of thousands of dollars per month**. We work hard to optimize our costs and pass the savings on to you. * **You should be able to get all the data you need with 1 request**. Most odds providers require certain parameters like the league or bookmaker on all requests. We don't. Our goal is to provide you with more data AND fewer requests. * **Repeatable schemas make APIs easy to understand and use**. Our system is designed from the ground up to be easy to work with. It's highly composable meaning you just need to understand a few basic concepts in order to understand the entire API. Pricing Model [#pricing-model] **We charge per event returned, not per market or bookmaker returned.** This means 1k credits on SportsGameOdds are typically worth the same as 100k-1M+ on other platforms. **Example:** A user needs to get data on 10 NBA games. Each has 250 odds markets and each market has odds for an average of 50 bookmakers. * **SportsGameOdds**: This consumes 10 credits ("objects") * **Other APIs**: This consumes 125,000 credits. (10 x 250 x 50) Full Coverage [#full-coverage] * **50+ leagues across 25+ sports\*** NFL, NBA, MLB, NHL, soccer, tennis, MMA, and more. * **80+ bookmakers\*** DraftKings, FanDuel, BetMGM, Caesars, PointsBet, and more * **Pre-game and live odds** with sub-minute update frequency * **Main markets, player props, game props** - thousands of markets supported * **Scores, stats, and results** - Everything in one place * **Historical odds data\*** for backtesting and analysis * **Partial game odds** (halves, quarters, periods, rounds, sets) * **Deeplinks** Navigate straight to relevant data on a bookmaker's website (\* depends on plan) Battle-Tested [#battle-tested] * **Millions of requests** handled daily from production applications * **Sub-100ms response times** with an average of 50ms * **99.9% uptime** with redundant infrastructure * **Real-time WebSocket streaming** for AllStar plan subscribers How It Works [#how-it-works] 1. Sign Up & Get Your API Key [#1-sign-up--get-your-api-key] [Sign up here](/pricing) and get your API key instantly. All plans include a 7 day free trial which we're always happy to extend if needed. We also have a 100% free tier available for testing and small projects. 2. Make Your First Request [#2-make-your-first-request] ```bash curl -X GET "https://api.sportsgameodds.com/v2/events?leagueID=NBA,NFL,MLB&oddsAvailable=true&apiKey=YOUR_API_KEY_GOES_HERE" ``` Simply replace `YOUR_API_KEY_GOES_HERE` with your actual API key 3. Build Something Amazing [#3-build-something-amazing] Use our [SDK](/docs/sdk/), [examples](/docs/examples/live-odds-tracker), and [guides](/docs/guides/data-batches) to build faster. --- # Quickstart - First API Call in 5 Minutes URL: https://sportsgameodds.com/docs/basics/quickstart Quickstart - First API Call in 5 Minutes [#quickstart---first-api-call-in-5-minutes] A complete, copy-paste tutorial to fetch live odds Step 1: Get Your API Key [#step-1-get-your-api-key] 1. Sign up at [sportsgameodds.com/pricing](/pricing) 2. Check your email for your API key 3. Copy your API key - you'll need it in the next step Step 2: Make A Request [#step-2-make-a-request] 1. Copy one of the examples below into a file 2. Replace `YOUR_API_KEY` with your actual API key 3. Run it! **Ready to build something amazing?** Pick a recipe and start coding! 🚀 --- # Setup Guide - API Keys and Authentication URL: https://sportsgameodds.com/docs/basics/setup Setup Guide - API Keys and Authentication [#setup-guide---api-keys-and-authentication] There are only 2 things you need to get started - an API key and a way to make requests. API Key [#api-key] * **[Get an API key here](/pricing)**. An API key is required to make requests and retrieve data from the API. * **We offer an eternally free plan**. Simply select the "Amateur" plan. During checkout, it may require you to add a credit card but it will never be charged unless you later decide to upgrade. * **Your key will be sent to your email address**. If you haven't received your API key within a couple of minutes of checking out, please check your spam folder. If it isn't there either, please reach out to support at [support@sportsgameodds.com](mailto:support@sportsgameodds.com) and we'll re-generate it for you. * **Keep your API key secret**. Never expose it publicly. Your API key is your password to the API. * **Include your API key in all requests**. It can be added in either the `x-api-key` header or the `apiKey` query param. Making Requests Manually [#making-requests-manually] Reference Docs Tool [#reference-docs-tool] You can make requests directly from our [API Reference](/docs/reference) page: 1. Navigate to [sportsgameodds.com/docs/reference](/docs/reference). 2. Enter your API key in the **Token** field under **API KEY** (Either ApiKeyHeader or ApiKeyParam should be selected). 3. Select an endpoint from the left sidebar (Events, Teams, Sports, etc.). 4. Click **Test Request** to open the interactive request panel. 5. Use the **Query** (or **Query Parameters**) section to set request parameters 6. Click **Send** to execute the request. 7. The response will be displayed in the **Response** section under **Body**. The reference docs page can also give you additional ready-to-use code snippets in multiple languages Postman [#postman] Postman is a popular tool for testing APIs. We provide an official collection you can import. 1. **Install Postman**: [postman.com/downloads](https://www.postman.com/downloads/) and install it. 2. **Download our collection**: SportsGameOdds Postman Collection 3. **Import the collection**: In Postman, click `Import`, then drag select the file you just downloaded. 4. **Set your API key**: Ensure `SportsGameOdds API` is selected → Go to the `Variables` tab → replace `YOUR_API_KEY_HERE` with your actual API key → click `Save`. 5. **Make a request**: Expand the `Events` folder → click `Get Events` → Adjust parameters (ex: set `oddsAvailable` to `true`) → click `Send` Directly in the Browser [#directly-in-the-browser] You can make requests directly in your browser's address bar using the apiKey parameter to authenticate. Simply visit a URL like this: ``` https://api.sportsgameodds.com/v2/sports/?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true&limit=3 ``` You can add or remove query parameters to adjust the data you receive. For example to only get moneylines, add `&oddIDs=points-home-game-ml-home,points-away-game-ml-away`to the end of the URL. API responses can be large and may slow/crash your browser tab. This method is great for quick/simple tests but not recommended for regular usage. Making Requests In Code [#making-requests-in-code] With HTTP Libraries [#with-http-libraries] Replace `YOUR_API_KEY` with your actual API key in the examples below: ```js // This example uses fetch() but you can use any HTTP library fetch("https://api.sportsgameodds.com/v2/sports/", {// [!code focus] method: "GET", // [!code focus] headers: { "X-Api-Key": "YOUR_API_KEY" }, // [!code focus] }) // [!code focus] .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ```python # This example uses the requests library import requests headers = { 'X-Api-Key': 'YOUR_API_KEY' } url = 'https://api.sportsgameodds.com/v2/sports/' # [!code focus] response = requests.get(url, headers=headers) # [!code focus] print(response.json()) ``` ```ruby # This example uses the net/http library require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/sports/') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_API_KEY' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end parsed_response = JSON.parse(response.body) puts parsed_response ``` ```php // This example uses cURL $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/sports/'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-Key: YOUR_API_KEY']); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java // This example uses HttpURLConnection import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/sports/"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_API_KEY"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); System.out.println(content.toString()); } catch (Exception e) { e.printStackTrace(); } } } ``` With Our SDK [#with-our-sdk] First, install the SDK: ```bash npm install sports-odds-api # [!code focus] yarn add sports-odds-api # if you're using yarn pnpm add sports-odds-api # if you're using pnpm ``` ```bash pip install sports-odds-api ``` ```bash gem install sports-odds-api ``` ```bash go get github.com/SportsGameOdds/sports-odds-api-go ``` ```groovy // Maven (pom.xml) com.sportsgameodds.api sports-odds-api 1.0.0 // Gradle (build.gradle) implementation 'com.sportsgameodds.api:sports-odds-api:1.0.0' ``` Then, make a request. Simply replace `YOUR_API_KEY` with your actual API key in the examples below: ```js import SportsGameOdds from "sports-odds-api"; const client = new SportsGameOdds({ apiKeyHeader: "YOUR_API_KEY", }); const sports = await client.sports.get(); // [!code focus] console.log(sports.data); // [!code focus] ``` ```python from sports_odds_api import SportsGameOdds client = SportsGameOdds( api_key_param='YOUR_API_KEY' ) # Fetch sports data sports = client.sports.get() # [!code focus] print(sports.data) # [!code focus] ``` ```ruby require "sports_odds_api" client = SportsOddsAPI::Client.new( api_key_param: 'YOUR_API_KEY' ) # Fetch sports data sports = client.sports.get # [!code focus] puts sports.data # [!code focus] ``` ```go package main import ( "context" "fmt" sportsoddsapi "github.com/SportsGameOdds/sports-odds-api-go" "github.com/SportsGameOdds/sports-odds-api-go/option" ) func main() { client := sportsoddsapi.NewClient( option.WithAPIKeyParam("YOUR_API_KEY"), ) // Fetch sports data ctx := context.Background() // [!code focus] sports, _ := client.Sports.Get(ctx) // [!code focus] fmt.Println(sports.Data) // [!code focus] } ``` ```java import com.sportsgameodds.api.client.SportsGameOddsClient; import com.sportsgameodds.api.client.okhttp.SportsGameOddsOkHttpClient; public class Main { public static void main(String[] args) { SportsGameOddsClient client = SportsGameOddsOkHttpClient.builder() .apiKeyHeader("YOUR_API_KEY") .build(); // Fetch sports data var sports = client.sports().get(); // [!code focus] System.out.println(sports.items()); // [!code focus] } } ``` Check out our [SDK Guide](/v2/sdk) for comprehensive documentation including pagination, error handling, filtering, and advanced features. --- # Bet Type and Side Data Types - betTypeID and sideID URL: https://sportsgameodds.com/docs/data-types/bet-types Bet Type and Side Data Types - betTypeID and sideID [#bet-type-and-side-data-types---bettypeid-and-sideid] A `betTypeID` corresponds to the style/grading system of a bet. A `sideID` corresponds outcome of that bet which is being selected. Number of Sides [#number-of-sides] For a 2-way betTypeID, there are 2 possible sideID values for each betTypeID * Example: `ou` (Over/Under) has 2 possible sideID values: `over` and `under` For a 3-way betTypeID, there are 6 possible sideID values for each betTypeID * 1 sideID for each of the 3 core outcomes plus 1 sideID for the inverse outcome of each * In a 3-way bet, one person (typically the bettor) gets 1 of the 3 outcomes and the other (typically the sportsbook) gets the other 2 outcomes. * Ex: `ml3way` (3-Way Moneyline) has an outcome for `home` (home team wins) plus the inverse of `away+draw` (away team wins or draw). We also support special-case Events where **Event.type** = `prop`. These are one-off/custom prop bets which are not part of the standard betting schema. The definitions of these bets (and their sides) are defined on the Event object itself. The betTypeID is always `prop` and the sideID is always `side1` or `side2`. If you're just getting started with your integration, we recommend you ignore these for now as the vast majority of data uses the other betTypeID and sideID values. --- # Bookmaker Data Type - bookmakerID URL: https://sportsgameodds.com/docs/data-types/bookmakers Bookmaker Data Type - bookmakerID [#bookmaker-data-type---bookmakerid] Each `bookmakerID` corresponds to a sportbook, daily fantasy site, or betting platform. Some data (used for testing or consensus calculations) isn't attributable and is assigned a bookmakerID of `unknown`. Not seeing a bookmaker you're looking for? More bookmakers (including ones not shown here) can be made available upon request through a custom (AllStar) plan. [Contact us](/contact-us) to discuss your needs. --- # Data Types Overview - API Schema Reference URL: https://sportsgameodds.com/docs/data-types Data Types Overview - API Schema Reference [#data-types-overview---api-schema-reference] The API organizes data into a hierarchy. Understanding these relationships helps you navigate the API efficiently and build powerful integrations. ```mermaid %%{init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#162b1e', 'primaryTextColor': '#fff', 'primaryBorderColor': '#37954c', 'lineColor': '#3d7a52' }, 'flowchart': { 'nodeSpacing': 20, 'rankSpacing': 24, 'curve': 'basis', 'padding': 16, 'htmlLabels': true } }}%% flowchart TB sport["Sports
sportID"] league["Leagues
leagueID"] team["Teams
teamID"] player["Players
playerID"] event["Events
eventID"] odd["Odds Markets
oddID"] book["Bookmaker Odds
bookmakerID"] sport --> league --> team team --> player team --> event --> odd --> book click sport "sports" click league "leagues" click odd "odds" click book "bookmakers" classDef default rx:5,ry:5 ``` Odds markets are identified using an **oddID** which is made up of a combination of: **`{periodID}-{statEntityID}-{statID}-{betTypeID}-{sideID}`** | Data Type | Identifier | Description | Example Values | | -------------------------- | -------------- | ----------------------------- | ------------------------------------------- | | [Sports](sports) | `sportID` | Top-level sports | `BASKETBALL`, `FOOTBALL`, `SOCCER` | | [Leagues](leagues) | `leagueID` | Leagues within each sport | `NBA`, `NFL`, `EPL` | | [Bookmakers](bookmakers) | `bookmakerID` | Sportsbooks that offer odds | `draftkings`, `fanduel`, `betmgm` | | [Periods](periods) | `periodID` | Time period for a stat or bet | `game`, `1h`, `1q` | | [Stats](stats) | `statID` | Statistic being tracked | `points`, `touchdowns`, `assists` | | [Stat Entity](stat-entity) | `statEntityID` | Who the stat applies to | `home`, `away`, `all`, `LEBRON_JAMES_1_NBA` | | [Bet Types](bet-types) | `betTypeID` | Type of bet | `ml`, `sp`, `ou` | | [Sides](bet-types) | `sideID` | Outcome a bet is taking | `home`, `away`, `over`, `under` | | [Odds](odds) | `oddID` | A betting odds market | `points-all-game-ou-over` | --- # League Data Type - leagueID URL: https://sportsgameodds.com/docs/data-types/leagues League Data Type - leagueID [#league-data-type---leagueid] A `leagueID` is used to uniquely identify each sports league. Each league belongs to only one `sportID` (no cross-sport leagues), but each `sportID` typically has multiple leagues. Below is a list of leagueIDs. One thing worth mentioning is that Teams (`teamID`) and Players (`playerID`) are defined on a per-league basis. So for example, if Arsenal competes both in the English Premier League and the UEFA Champions League then they will have two different `teamID` values, `ARSENAL_EPL` and `ARSENAL_UEFA_CHAMPIONS_LEAGUE`. Not seeing a league you're looking for? More leagues (including ones not shown here) can be made available upon request through a custom (AllStar) plan. [Contact us](/contact-us) to discuss your needs. --- # Supported Markets - Odds Coverage by League URL: https://sportsgameodds.com/docs/data-types/markets Supported Markets - Odds Coverage by League [#supported-markets---odds-coverage-by-league] --- # Odds Data Type - oddID URL: https://sportsgameodds.com/docs/data-types/odds Odds Data Type - oddID [#odds-data-type---oddid] An `oddID` is a unique identifier for a specific betting option. It identifies a specific side/outcome to bet on within a specific market. oddID Format [#oddid-format] The oddID combines multiple identifiers into a single unique value: ``` {statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID} ``` | Component | Description | Examples | | --------------------------- | ------------------------------ | ------------------------------------ | | [statID](stats) | The statistic being wagered on | `points`, `touchdowns`, `assists` | | [statEntityID](stat-entity) | Who the stat applies to | `home`, `away`, `all`, or a playerID | | [periodID](periods) | The time period covered | `game`, `1h`, `1q` | | [betTypeID](bet-types) | The type of bet | `ml`, `sp`, `ou` | | [sideID](bet-types) | Which side of the bet | `home`, `away`, `over`, `under` | Example oddIDs [#example-oddids] | oddID | Description | | --------------------------------------- | --------------------------------------------------- | | `points-home-game-ml-home` | Moneyline bet on the home team to win the full game | | `points-away-game-sp-away` | Spread bet on the away team for the full game | | `points-all-game-ou-over` | Over bet on total points for the full game | | `points-home-1h-ml-home` | Moneyline bet on the home team to win the 1st half | | `assists-LEBRON_JAMES_NBA-game-ou-over` | Over bet on Lebron James assists for the full game | Accessing Odds [#accessing-odds] Each event returned by the `/events` endpoint contains odds data in the `odds` field. The `odds` field is an object where each key is an `oddID` and the value contains all data related to that betting option. In other words, you can access odds data by looking at `odds.` for a given oddID. ```json { "eventID": "...", "odds": { "points-home-game-ml-home": { "oddID": "points-home-game-ml-home", "statID": "points", "statEntityID": "home", "periodID": "game", "betTypeID": "ml", "sideID": "home", "fairOdds": "-110", "bookOdds": "-115", "byBookmaker": { "draftkings": { "odds": "-112", "available": true }, "fanduel": { "odds": "-118", "available": true } } }, "points-all-game-ou-over": { "oddID": "points-all-game-ou-over", "statID": "points", "statEntityID": "all", "periodID": "game", "betTypeID": "ou", "sideID": "over", "fairOdds": "-108", "fairOverUnder": "224.5", "bookOdds": "-110", "bookOverUnder": "224.5", "byBookmaker": { "draftkings": { "odds": "-110", "overUnder": "224.5", "available": true } } } } } ``` Key Odds Fields [#key-odds-fields] | Field | Description | | --------------------------------- | ------------------------------------------------- | | `fairOdds` | Consensus odds without juice (vig removed) | | `bookOdds` | Consensus odds across bookmakers (includes juice) | | `fairSpread` / `bookSpread` | The spread/handicap value (for spread bets) | | `fairOverUnder` / `bookOverUnder` | The over/under line (for O/U bets) | | `byBookmaker` | Odds broken down by individual bookmaker | | `started` / `ended` | Whether the bet period has started or ended | | `score` | The final result value (when available) | Open/Close Odds by Bookmaker [#openclose-odds-by-bookmaker] The `byBookmaker` object can include historical open and close values when requested with `includeOpenCloseOdds=true`: | Field | Description | | ---------------- | ------------------------------------------------------ | | `openOdds` | Odds when first available for betting at this book | | `closeOdds` | Odds at event start time at this book | | `openSpread` | Spread when first available for betting at this book | | `closeSpread` | Spread at event start time at this book | | `openOverUnder` | O/U line when first available for betting at this book | | `closeOverUnder` | O/U line at event start time at this book | These fields are **not included by default** to reduce response size. Add `includeOpenCloseOdds=true` to your request to include them. Filtering by oddID [#filtering-by-oddid] Use the `oddID` query parameter on the `/events` endpoint to request specific odds: ``` /events?oddID=points-home-game-ml-home,points-all-game-ou-over ``` Use `includeOpposingOdds=true` to automatically include the opposite side of each requested oddID. See [Supported Markets](/v2/data-types/markets) for a complete list of oddID values supported for each league and bookmaker. --- # Period Data Type - periodID URL: https://sportsgameodds.com/docs/data-types/periods Period Data Type - periodID [#period-data-type---periodid] A period corresponds to a unit of time in a sport or league that covers the portion of an event. Each sport has its own periods. Different odds can correspond to different periods. Each period for each sport is listed below. --- # Sports Data Type - sportID URL: https://sportsgameodds.com/docs/data-types/sports Sports Data Type - sportID [#sports-data-type---sportid] --- # Stat Entity Data Type - statEntityID URL: https://sportsgameodds.com/docs/data-types/stat-entity Stat Entity Data Type - statEntityID [#stat-entity-data-type---statentityid] A `statEntityID` identifies **who's** performance on a given statistic we're tracking. In other words, if the [statID](/v2/data-types/stats) tracks WHAT we're tracking, the `statEntityID` tracks WHO we're tracking. In the context of odds data, the `statEntityID` determines who's performance on a given `statID` dictates the outcome of the bet. Possible Values [#possible-values] | Value | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `all` | Tracks the combined performance of all teams/participants. Example: an over/under bet on the total points scored in a game uses `all` because the bet tracks points scored by both teams combined. | | `home` | Tracks the performance of the home team. | | `away` | Tracks the performance of the away team. | | *playerID* | When tracking player-specific stats or player prop bets, the `statEntityID` will be a specific playerID. | | *teamID* | Used only in Events where `type` = `tournament`. In a normal event (`type` = `match`), the `statEntityID` will be `home` or `away`. However, in tournament-type events (like a Golf tournament), there isn't a home and away team, so the `statEntityID` will be a specific teamID. | Usage in Odds [#usage-in-odds] In odds data, the `statEntityID` determines who's performance on a given `statID` dictates the outcome of the bet. **Example:** If you're looking at odds for an over-under rebounds bet (`sideID` = `over`, `betTypeID` = `ou`, `statID` = `rebounds`): * If `statEntityID` = `LEBRON_JAMES_LA_LAKERS` → The bet is on LeBron James' individual rebounds * If `statEntityID` = `home` → The bet is on the home team's total rebounds * If `statEntityID` = `all` → The bet is on total rebounds from both teams combined Fixed statEntityID Values [#fixed-statentityid-values] In some cases, the `statEntityID` has a fixed value determined by the `betTypeID` and `sideID` combination. In these cases, `statEntityID` is somewhat redundant with `sideID` but is included for consistency. **Why?** For example, if you're taking the home team on a spread bet, the outcome of your bet is always dictated by the performance of the home team. | betTypeID | sideID | statEntityID | | --------- | ----------- | ------------ | | `ml` | `home` | `home` | | `ml` | `away` | `away` | | `sp` | `home` | `home` | | `sp` | `away` | `away` | | `ml3way` | `home` | `home` | | `ml3way` | `away` | `away` | | `ml3way` | `draw` | `all` | | `ml3way` | `home+draw` | `home` | | `ml3way` | `away+draw` | `away` | | `ml3way` | `not_draw` | `all` | | `prop` | `side1` | `side1` | | `prop` | `side2` | `side2` | For more details on bet types and sides, see the [Bet Types and Sides](/v2/data-types/bet-types) documentation. --- # Stat Data Type - statID URL: https://sportsgameodds.com/docs/data-types/stats Stat Data Type - statID [#stat-data-type---statid] A **statID** corresponds to a specific statistic or value. Each sport has its own set of supported statIDs. You can use it to find event/game statistics: * When looking at the `results` object of an Event, the statID allows you to find a specific stat value at `results...`. It's also used to define odds markets: * When looking at the `odds` object of an Event, each item contains a statID corresponding to the stat which will determine the outcome of the bet. This is found at `odds..statID`. The points statID [#the-points-statid] The statID `points` is special. It's used in every sport to define the stat which determines the winner of an event. We've found that maintaining a single statID for the main (most important) stat across all sports helps keep things simple and consistent. However, this also means the `points` statID is used even in sports where the word "points" isn't necessarily used to describe that stat. Here are some examples: * In Baseball, `points` is the statID for "runs scored" * In Hockey, `points` is the statID for "goals scored" and `goals+assists` is the statID for traditional "hockey points" * In Golf, `points` is the number of strokes (to par) * In Tennis, `points` is the statID for "sets won" (since most sets won wins the match) and `truePoints` is used for the actual number of tennis points won * In MMA or Boxing, `points` is used to determine the winner of a fight. The winner will have a value of 1 and the loser will have a value of 0. --- # GET /events - Fetch Games, Odds, and Results URL: https://sportsgameodds.com/docs/endpoints/getEvents Get a list of Events --- # GET /leagues - List All Leagues URL: https://sportsgameodds.com/docs/endpoints/getLeagues Get a list of Leagues --- # GET /markets - Fetch Market Metadata URL: https://sportsgameodds.com/docs/endpoints/getMarkets Get a list of Markets --- # GET /players - Fetch Player Data URL: https://sportsgameodds.com/docs/endpoints/getPlayers Get a list of Players for a specific Team or Event --- # GET /sports - List All Sports URL: https://sportsgameodds.com/docs/endpoints/getSports Get a list of sports --- # GET /stats - List All Stats URL: https://sportsgameodds.com/docs/endpoints/getStats Get a list of StatIDs --- # GET /teams - Fetch Team Data URL: https://sportsgameodds.com/docs/endpoints/getTeams Get a list of Teams by ID or league --- # GET /account/usage - Check API Usage and Limits URL: https://sportsgameodds.com/docs/endpoints/getUsageData Get rate-limits and usage data about your API key --- # Stream Events URL: https://sportsgameodds.com/docs/endpoints/streamEvents Setup streamed (WebSocket) connection --- # Arbitrage Calculator Example - Python URL: https://sportsgameodds.com/docs/examples/arbitrage-calculator Arbitrage Calculator Example - Python [#arbitrage-calculator-example---python] Find guaranteed profit opportunities by identifying odds discrepancies across bookmakers. What You'll Build [#what-youll-build] A Python script that: * Scans all bookmakers for odds discrepancies * Calculates arbitrage opportunities * Shows optimal bet sizing * Calculates guaranteed profit percentage **Perfect for:** Finding risk-free profit opportunities, comparing bookmaker odds, beating the vig What is Arbitrage? [#what-is-arbitrage] **Arbitrage** (or "arbing") is betting on all possible outcomes across different bookmakers to guarantee profit regardless of the result. **Example:** * **Book A:** Lakers -5.5 @ +105 * **Book B:** Celtics +6.0 @ -105 By betting both sides across different books, you can sometimes lock in profit. Prerequisites [#prerequisites] * Python 3.8+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic Python knowledge Complete Code [#complete-code] Step 1: Setup Project [#step-1-setup-project] ```bash mkdir arb-calculator && cd arb-calculator python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install requests ``` Step 2: Create arb_calculator.py [#step-2-create-arb_calculatorpy] ```python # arb_calculator.py from collections import defaultdict API_KEY = os.environ.get('SPORTSGAMEODDS_KEY') API_BASE = 'https://api.sportsgameodds.com/v2' def fetch_events(league='NBA'): """Fetch upcoming events with odds""" try: response = requests.get( f'{API_BASE}/events', params={ 'leagueID': league, 'finalized': 'false', 'oddsAvailable': 'true', 'limit': 100 }, headers={'x-api-key': API_KEY} ) response.raise_for_status() return response.json()['data'] except requests.exceptions.RequestException as e: print(f'Error fetching events: {e}') return [] def american_to_decimal(american_odds): """Convert American odds to decimal odds""" if american_odds > 0: return (american_odds / 100) + 1 else: return (100 / abs(american_odds)) + 1 def calculate_arbitrage(odds_list): """ Calculate if arbitrage exists and profit percentage odds_list: List of decimal odds for all outcomes Returns: (has_arb, profit_percentage, stake_distribution) """ # Calculate implied probability sum implied_prob_sum = sum(1 / odd for odd in odds_list) # Arbitrage exists when sum < 1 (overround is negative) has_arb = implied_prob_sum < 1 if not has_arb: return False, 0, [] # Calculate profit percentage profit_pct = ((1 / implied_prob_sum) - 1) * 100 # Calculate optimal stake distribution (for $100 total) total_stake = 100 stakes = [(total_stake / odd) / implied_prob_sum for odd in odds_list] return True, profit_pct, stakes def find_arbitrage_opportunities(events): """Find arbitrage opportunities in events""" opportunities = [] for event in events: # Get team names - API returns names in a nested structure away_name = event['teams']['away']['names']['long'] home_name = event['teams']['home']['names']['long'] matchup = f"{away_name} @ {home_name}" # Group odds by market type and side # Structure: markets[betType][periodID][side] = list of {bookmaker, price, line, decimal} markets = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) for odd_id, odd in (event.get('odds') or {}).items(): bet_type = odd['betTypeID'] side = odd['sideID'] period_id = odd.get('periodID', 'game') # Only process full game odds for arbitrage if period_id != 'game': continue # Iterate through each bookmaker's odds for bookmaker_id, bookmaker_data in (odd.get('byBookmaker') or {}).items(): # Skip if not available if not bookmaker_data.get('available', True): continue # Get odds value (API returns as string) odds_str = bookmaker_data.get('odds') if not odds_str: continue try: price = int(odds_str) except (ValueError, TypeError): continue # Get line value based on bet type if bet_type == 'sp': line = bookmaker_data.get('spread') elif bet_type == 'ou': line = bookmaker_data.get('overUnder') else: line = None decimal_odds = american_to_decimal(price) markets[bet_type][period_id][side].append({ 'bookmaker': bookmaker_id, 'american': price, 'decimal': decimal_odds, 'line': line }) # Check each market for arbitrage for bet_type, periods in markets.items(): for period_id, sides in periods.items(): # For spread markets if bet_type == 'sp': if sides.get('home') and sides.get('away'): # Find best odds for each side best_home = max(sides['home'], key=lambda x: x['decimal']) best_away = max(sides['away'], key=lambda x: x['decimal']) # Calculate arbitrage has_arb, profit_pct, stakes = calculate_arbitrage([ best_home['decimal'], best_away['decimal'] ]) if has_arb: opportunities.append({ 'matchup': matchup, 'market': 'spread', 'profit_pct': profit_pct, 'legs': [ { 'side': 'home', 'bookmaker': best_home['bookmaker'], 'odds': best_home['american'], 'line': best_home['line'], 'stake_pct': stakes[0] }, { 'side': 'away', 'bookmaker': best_away['bookmaker'], 'odds': best_away['american'], 'line': best_away['line'], 'stake_pct': stakes[1] } ] }) # For total (over/under) markets elif bet_type == 'ou': if sides.get('over') and sides.get('under'): best_over = max(sides['over'], key=lambda x: x['decimal']) best_under = max(sides['under'], key=lambda x: x['decimal']) has_arb, profit_pct, stakes = calculate_arbitrage([ best_over['decimal'], best_under['decimal'] ]) if has_arb: opportunities.append({ 'matchup': matchup, 'market': 'total', 'profit_pct': profit_pct, 'legs': [ { 'side': 'over', 'bookmaker': best_over['bookmaker'], 'odds': best_over['american'], 'line': best_over['line'], 'stake_pct': stakes[0] }, { 'side': 'under', 'bookmaker': best_under['bookmaker'], 'odds': best_under['american'], 'line': best_under['line'], 'stake_pct': stakes[1] } ] }) # For moneyline markets elif bet_type == 'ml': if sides.get('home') and sides.get('away'): best_home = max(sides['home'], key=lambda x: x['decimal']) best_away = max(sides['away'], key=lambda x: x['decimal']) has_arb, profit_pct, stakes = calculate_arbitrage([ best_home['decimal'], best_away['decimal'] ]) if has_arb: opportunities.append({ 'matchup': matchup, 'market': 'moneyline', 'profit_pct': profit_pct, 'legs': [ { 'side': 'home', 'bookmaker': best_home['bookmaker'], 'odds': best_home['american'], 'stake_pct': stakes[0] }, { 'side': 'away', 'bookmaker': best_away['bookmaker'], 'odds': best_away['american'], 'stake_pct': stakes[1] } ] }) return opportunities def display_opportunities(opportunities, total_stake=100): """Display arbitrage opportunities""" if not opportunities: print('No arbitrage opportunities found') print('\nNote: True arbitrage is rare. The market is usually efficient.') return # Sort by profit percentage (highest first) opportunities.sort(key=lambda x: x['profit_pct'], reverse=True) print(f'\nFound {len(opportunities)} ARBITRAGE OPPORTUNIT{"Y" if len(opportunities) == 1 else "IES"}!\n') for i, opp in enumerate(opportunities, 1): print('=' * 60) print(f'#{i} - {opp["matchup"]}') print(f'Market: {opp["market"].upper()}') print(f'Guaranteed Profit: {opp["profit_pct"]:.2f}%') print('-' * 60) for leg in opp['legs']: stake = total_stake * (leg['stake_pct'] / 100) payout = stake * american_to_decimal(leg['odds']) line_str = f" {leg['line']}" if leg.get('line') else "" print(f" {leg['side'].upper()}{line_str} @ {leg['odds']:+d} ({leg['bookmaker']})") print(f" Bet: ${stake:.2f} -> Payout: ${payout:.2f}") print() profit = total_stake * (opp['profit_pct'] / 100) print(f"Total Stake: ${total_stake:.2f}") print(f"Guaranteed Profit: ${profit:.2f}") print('=' * 60) print() def main(): print('Scanning for arbitrage opportunities...\n') # Scan multiple leagues leagues = ['NBA', 'NFL', 'NHL'] all_opportunities = [] for league in leagues: print(f'Fetching {league} events...') events = fetch_events(league) if events: print(f'Found {len(events)} {league} events') opportunities = find_arbitrage_opportunities(events) all_opportunities.extend(opportunities) else: print(f'No {league} events found') print() display_opportunities(all_opportunities, total_stake=100) if __name__ == '__main__': main() ``` Step 3: Run It [#step-3-run-it] ```bash export SPORTSGAMEODDS_KEY=your_api_key_here python arb_calculator.py ``` Expected Output [#expected-output] ``` Scanning for arbitrage opportunities... Fetching NBA events... Found 8 NBA events Fetching NFL events... Found 14 NFL events Fetching NHL events... Found 6 NHL events Found 2 ARBITRAGE OPPORTUNITIES! ============================================================ #1 - Boston Celtics @ Los Angeles Lakers Market: SPREAD Guaranteed Profit: 2.34% ------------------------------------------------------------ HOME -5.5 @ +105 (fanduel) Bet: $48.78 -> Payout: $100.00 AWAY +6.0 @ +100 (draftkings) Bet: $51.22 -> Payout: $102.44 Total Stake: $100.00 Guaranteed Profit: $2.34 ============================================================ ============================================================ #2 - Tampa Bay Lightning @ Florida Panthers Market: TOTAL Guaranteed Profit: 1.15% ------------------------------------------------------------ OVER 6.0 @ -105 (betmgm) Bet: $51.22 -> Payout: $100.00 UNDER 6.5 @ +110 (caesars) Bet: $48.78 -> Payout: $102.44 Total Stake: $100.00 Guaranteed Profit: $1.15 ============================================================ ``` How It Works [#how-it-works] 1. Fetch Events with Odds [#1-fetch-events-with-odds] ```python response = requests.get( f'{API_BASE}/events', params={ 'leagueID': league, 'finalized': 'false', # Upcoming games only 'oddsAvailable': 'true' # Must have odds } ) ``` 2. Convert American to Decimal Odds [#2-convert-american-to-decimal-odds] ```python def american_to_decimal(american_odds): if american_odds > 0: return (american_odds / 100) + 1 # e.g., +150 -> 2.50 else: return (100 / abs(american_odds)) + 1 # e.g., -110 -> 1.909 ``` **Why decimal?** Makes arbitrage math easier. 3. Process Odds by Bookmaker [#3-process-odds-by-bookmaker] The API returns odds with a nested `byBookmaker` structure: ```python for odd_id, odd in event.get('odds').items(): bet_type = odd['betTypeID'] # 'sp', 'ml', 'ou' side = odd['sideID'] # 'home', 'away', 'over', 'under' # Each odd contains prices from multiple bookmakers for bookmaker_id, bookmaker_data in odd.get('byBookmaker', {}).items(): price = int(bookmaker_data['odds']) decimal_odds = american_to_decimal(price) ``` 4. Find Best Odds for Each Side [#4-find-best-odds-for-each-side] ```python best_home = max(sides['home'], key=lambda x: x['decimal']) best_away = max(sides['away'], key=lambda x: x['decimal']) ``` We want the **highest odds** (best payout) for each outcome. 5. Calculate Arbitrage [#5-calculate-arbitrage] ```python implied_prob_sum = sum(1 / odd for odd in [home_decimal, away_decimal]) if implied_prob_sum < 1: # Arbitrage exists! profit_pct = ((1 / implied_prob_sum) - 1) * 100 ``` **Math explained:** * Normal market: implied probabilities sum to >100% (bookmaker edge) * Arbitrage: implied probabilities sum to \<100% (your edge) **Example:** * Home @ 2.10 (decimal) = 47.62% implied probability * Away @ 2.10 (decimal) = 47.62% implied probability * Sum = 95.24% \< 100% -> **4.76% arbitrage!** 6. Calculate Optimal Stakes [#6-calculate-optimal-stakes] ```python stakes = [(100 / odd) / implied_prob_sum for odd in odds_list] ``` This distributes your total stake to guarantee equal profit on all outcomes. Real-World Example [#real-world-example] **Scenario:** Lakers vs Celtics | Bookmaker | Market | Odds | Decimal | Implied Prob | | ---------- | ------------ | ---- | ------- | ------------ | | FanDuel | Lakers -5.5 | +105 | 2.05 | 48.78% | | DraftKings | Celtics +6.0 | +100 | 2.00 | 50.00% | **Sum:** 48.78% + 50.00% = **98.78%** **Profit:** (1 / 0.9878) - 1 = **1.23%** **Stakes (for $100 total):** * Lakers: $100 x (48.78 / 98.78) = **$49.38** * Celtics: $100 x (50.00 / 98.78) = **$50.62** **Outcome 1:** Lakers cover -5.5 * Win $49.38 x 2.05 = $101.23 * Profit: $101.23 - $100 = **$1.23** **Outcome 2:** Celtics cover +6.0 * Win $50.62 x 2.00 = $101.24 * Profit: $101.24 - $100 = **$1.24** **Guaranteed profit either way!** Enhancements [#enhancements] Add Minimum Profit Filter [#add-minimum-profit-filter] ```python MIN_PROFIT_PCT = 1.0 # Only show 1%+ opportunities opportunities = [ opp for opp in opportunities if opp['profit_pct'] >= MIN_PROFIT_PCT ] ``` Include Middle Opportunities [#include-middle-opportunities] A "middle" is when you can win both bets: ```python # Lakers -5.5 @ Book A # Celtics +6.0 @ Book B # If Lakers win by exactly 6, you win BOTH bets! def find_middles(sides): for home_odd in sides['home']: for away_odd in sides['away']: home_line = float(home_odd['line'] or 0) away_line = float(away_odd['line'] or 0) gap = abs(home_line) - abs(away_line) if gap >= 0.5: # Middle exists yield { 'gap': gap, 'home': home_odd, 'away': away_odd } ``` Real-Time Monitoring [#real-time-monitoring] ```python def monitor_arbitrage(interval=60): """Check for arbitrage every minute""" while True: opportunities = find_all_arbitrage() display_opportunities(opportunities) time.sleep(interval) monitor_arbitrage() ``` Send Alerts [#send-alerts] ```python def send_telegram_alert(opportunity): """Send Telegram notification""" bot_token = os.environ.get('TELEGRAM_BOT_TOKEN') chat_id = os.environ.get('TELEGRAM_CHAT_ID') message = f"Arbitrage Alert!\n" message += f"{opportunity['matchup']}\n" message += f"Profit: {opportunity['profit_pct']:.2f}%" requests.post( f'https://api.telegram.org/bot{bot_token}/sendMessage', json={'chat_id': chat_id, 'text': message} ) ``` Important Considerations [#important-considerations] Account Limitations [#account-limitations] **Warning:** Bookmakers may limit or ban accounts that consistently arbitrage. **Tips to avoid detection:** * Don't bet only arbitrage opportunities * Vary bet sizes * Use round numbers ($50 not $49.38) * Space out bets across time * Use different devices/IPs Execution Speed [#execution-speed] Arbitrage opportunities disappear quickly (seconds to minutes). **Solutions:** * Use our [Real-time Streaming](/docs/guides/realtime-streaming-api) (AllStar plan) * Automate bet placement (requires bookmaker APIs) * Set up price alerts Line Differences [#line-differences] In our example: * Book A: -5.5 * Book B: +6.0 The 0.5 point difference creates the arbitrage opportunity (a "middle"). Commission & Fees [#commission--fees] Remember to account for: * Withdrawal fees * Currency conversion * Deposit bonuses with rollover requirements Troubleshooting [#troubleshooting] "No arbitrage opportunities found" [#no-arbitrage-opportunities-found] **Causes:** 1. **Efficient markets** - Bookmakers adjust quickly 2. **Not enough bookmakers** - Need odds from 5+ books 3. **Wrong timing** - Arbitrage appears closer to game time **Solutions:** * Scan more leagues * Check closer to event start * Look for less popular markets Negative profit calculation [#negative-profit-calculation] **Cause:** Bug in odds conversion or stake calculation. **Solution:** Add validation: ```python def validate_arbitrage(opportunity): """Verify calculations are correct""" total_stake = sum(leg['stake_pct'] for leg in opportunity['legs']) assert abs(total_stake - 100) < 0.01, "Stakes don't sum to 100%" # Verify profit on all outcomes for leg in opportunity['legs']: payout = leg['stake_pct'] * american_to_decimal(leg['odds']) profit = payout - 100 assert profit > 0, f"Negative profit on {leg['side']}" ``` --- # Live Odds Tracker Example - JavaScript URL: https://sportsgameodds.com/docs/examples/live-odds-tracker Live Odds Tracker Example - JavaScript [#live-odds-tracker-example---javascript] Complete working example that tracks NBA odds every 30 seconds and detects line movement. What You'll Build [#what-youll-build] A Node.js script that: * Fetches live NBA odds every 30 seconds * Tracks line movement across all bookmakers * Alerts when odds change * Shows before/after comparisons **Perfect for:** Detecting sharp money, identifying steam moves, monitoring arbitrage opportunities Prerequisites [#prerequisites] * Node.js 18+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic JavaScript knowledge Complete Code [#complete-code] Step 1: Setup Project [#step-1-setup-project] ```bash mkdir odds-tracker && cd odds-tracker npm init -y npm install node-fetch@2 ``` > **Note:** We use `node-fetch@2` because v3+ is ESM-only. The `@2` ensures CommonJS compatibility. Step 2: Create tracker.js [#step-2-create-trackerjs] ```javascript // tracker.js const fetch = require("node-fetch"); const API_KEY = process.env.SPORTSGAMEODDS_KEY; const API_BASE = "https://api.sportsgameodds.com/v2"; const POLL_INTERVAL = 30000; // 30 seconds // Store previous odds for comparison let previousOdds = {}; // Fetch current NBA odds async function fetchNBAOdds() { try { // Use finalized=false to get upcoming games (live=true only works during live games) const response = await fetch(`${API_BASE}/events?leagueID=NBA&finalized=false&oddsAvailable=true`, { headers: { "x-api-key": API_KEY }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.data; } catch (error) { console.error("Error fetching odds:", error.message); return []; } } // Compare current odds with previous and detect changes function detectLineMovement(events) { const movements = []; events.forEach((event) => { // Get team names from the nested names structure const awayName = event.teams.away.names.long; const homeName = event.teams.home.names.long; const matchup = `${awayName} @ ${homeName}`; // Check each odd for movement Object.entries(event.odds || {}).forEach(([oddID, currentOdd]) => { // Check each bookmaker's odds for this market Object.entries(currentOdd.byBookmaker || {}).forEach(([bookmakerID, currentBookmakerOdds]) => { const key = `${event.eventID}-${oddID}-${bookmakerID}`; const previousBookmakerOdds = previousOdds[key]; if (previousBookmakerOdds) { const currentLine = currentBookmakerOdds.spread || currentBookmakerOdds.overUnder; const previousLine = previousBookmakerOdds.spread || previousBookmakerOdds.overUnder; const lineChanged = previousLine !== currentLine; const priceChanged = previousBookmakerOdds.odds !== currentBookmakerOdds.odds; if (lineChanged || priceChanged) { movements.push({ matchup, eventID: event.eventID, betType: currentOdd.betTypeID, bookmaker: bookmakerID, side: currentOdd.sideID, previous: { line: previousLine, price: previousBookmakerOdds.odds, }, current: { line: currentLine, price: currentBookmakerOdds.odds, }, lineChanged, priceChanged, }); } } // Store current odds for next comparison previousOdds[key] = { spread: currentBookmakerOdds.spread, overUnder: currentBookmakerOdds.overUnder, odds: currentBookmakerOdds.odds, lastUpdatedAt: currentBookmakerOdds.lastUpdatedAt, }; }); }); }); return movements; } // Display line movements in console function displayMovements(movements) { if (movements.length === 0) { console.log("No line movement detected"); return; } console.log(`\n${movements.length} LINE MOVEMENT(S) DETECTED\n`); movements.forEach((movement) => { console.log("----------------------------------------"); console.log(`${movement.matchup}`); console.log(`${movement.betType.toUpperCase()} (${movement.side}) - ${movement.bookmaker}`); if (movement.lineChanged) { console.log(`Line: ${movement.previous.line} -> ${movement.current.line}`); } if (movement.priceChanged) { console.log(`Price: ${movement.previous.price} -> ${movement.current.price}`); } console.log("----------------------------------------\n"); }); } // Main tracking loop async function trackOdds() { console.log(new Date().toLocaleTimeString(), "- Checking for line movement..."); const events = await fetchNBAOdds(); if (events.length === 0) { console.log("No NBA games found\n"); return; } console.log(`Tracking ${events.length} NBA game(s)`); const movements = detectLineMovement(events); displayMovements(movements); } // Start tracking console.log("Starting NBA Live Odds Tracker..."); console.log(`Polling every ${POLL_INTERVAL / 1000} seconds\n`); // Run immediately, then every 30 seconds trackOdds(); setInterval(trackOdds, POLL_INTERVAL); ``` Step 3: Run It [#step-3-run-it] ```bash export SPORTSGAMEODDS_KEY=your_api_key_here node tracker.js ``` Expected Output [#expected-output] ``` Starting NBA Live Odds Tracker... Polling every 30 seconds 7:45:32 PM - Checking for line movement... Tracking 3 NBA game(s) No line movement detected 7:46:02 PM - Checking for line movement... Tracking 3 NBA game(s) 2 LINE MOVEMENT(S) DETECTED ---------------------------------------- Boston Celtics @ Los Angeles Lakers SP (home) - draftkings Line: -5.5 -> -6.0 ---------------------------------------- ---------------------------------------- Golden State Warriors @ Phoenix Suns ML (away) - fanduel Price: +145 -> +150 ---------------------------------------- ``` How It Works [#how-it-works] 1. Fetch Odds [#1-fetch-odds] ```javascript const response = await fetch(`${API_BASE}/events?leagueID=NBA&finalized=false&oddsAvailable=true`); ``` **Query parameters:** * `leagueID=NBA` - Only NBA games * `finalized=false` - Games not yet completed * `oddsAvailable=true` - Must have odds data > **Tip:** Use `live=true` instead of `finalized=false` to only get games currently in progress. 2. Process Nested Bookmaker Structure [#2-process-nested-bookmaker-structure] ```javascript // Each odd has a byBookmaker object with odds from each bookmaker Object.entries(currentOdd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { const currentLine = bookmakerOdds.spread || bookmakerOdds.overUnder; const currentPrice = bookmakerOdds.odds; }); ``` The API returns odds nested by bookmaker: ```json { "byBookmaker": { "draftkings": { "odds": "-110", "spread": "-5.5", "available": true }, "fanduel": { "odds": "-108", "spread": "-5.5", "available": true } } } ``` 3. Compare with Previous Odds [#3-compare-with-previous-odds] ```javascript // Key includes bookmakerID since odds are per-bookmaker const key = `${eventID}-${oddID}-${bookmakerID}`; const previousBookmakerOdds = previousOdds[key]; if (previousBookmakerOdds) { const lineChanged = previousLine !== currentLine; const priceChanged = previousBookmakerOdds.odds !== currentBookmakerOdds.odds; } ``` We store every odd by a unique key (`eventID-oddID-bookmakerID`) and compare on each poll. 4. Detect Significant Movement [#4-detect-significant-movement] The script detects: * **Line changes** - Spread/total moves (e.g., -5.5 -> -6.0) * **Price changes** - Juice moves (e.g., -110 -> -115) 5. Store for Next Comparison [#5-store-for-next-comparison] ```javascript previousOdds[key] = { spread: currentBookmakerOdds.spread, overUnder: currentBookmakerOdds.overUnder, odds: currentBookmakerOdds.odds, lastUpdatedAt: currentBookmakerOdds.lastUpdatedAt, }; ``` Enhancements [#enhancements] Track Multiple Leagues [#track-multiple-leagues] ```javascript const LEAGUES = ["NBA", "NFL", "NHL"]; async function trackAllLeagues() { for (const league of LEAGUES) { console.log(`\n${league} Updates:`); const events = await fetchOdds(league); const movements = detectLineMovement(events); displayMovements(movements); } } ``` Add Discord/Slack Notifications [#add-discordslack-notifications] ```javascript async function sendDiscordAlert(movement) { await fetch(process.env.DISCORD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: `Line moved: ${movement.matchup}\n${movement.betType} ${movement.previous.line || "N/A"} -> ${movement.current.line || "N/A"}`, }), }); } // Call after detecting movement movements.forEach(sendDiscordAlert); ``` Filter by Significant Moves Only [#filter-by-significant-moves-only] ```javascript function isSignificantMove(movement) { // Only alert on moves of 0.5 points or more if (movement.lineChanged && movement.current.line && movement.previous.line) { const currentLine = parseFloat(movement.current.line); const previousLine = parseFloat(movement.previous.line); const lineDiff = Math.abs(currentLine - previousLine); return lineDiff >= 0.5; } // Or price moves of 5 points or more if (movement.priceChanged) { const currentPrice = parseInt(movement.current.price); const previousPrice = parseInt(movement.previous.price); const priceDiff = Math.abs(currentPrice - previousPrice); return priceDiff >= 5; } return false; } const significantMoves = movements.filter(isSignificantMove); ``` Store Historical Movement Data [#store-historical-movement-data] ```javascript const fs = require("fs"); function logMovement(movement) { const logEntry = { timestamp: new Date().toISOString(), ...movement, }; fs.appendFileSync("movement-history.jsonl", JSON.stringify(logEntry) + "\n"); } movements.forEach(logMovement); ``` Add Real-Time Streaming (AllStar Plan) [#add-real-time-streaming-allstar-plan] Instead of polling, use WebSocket streaming for instant updates: ```javascript // Requires AllStar plan const stream = await client.stream.events({ feed: "events:live" }); channel.bind("update", (event) => { const movements = detectLineMovement([event]); displayMovements(movements); }); ``` See [Real-time Streaming Guide](/docs/guides/realtime-streaming-api) Troubleshooting [#troubleshooting] "No NBA games found" [#no-nba-games-found] **Cause:** No NBA games currently available (off-season or no games scheduled). **Solution:** Try different leagues or check schedule: ```javascript // Try multiple leagues const LEAGUES = ["NBA", "NFL", "NHL", "MLB"]; for (const league of LEAGUES) { const events = await fetchOdds(league); if (events.length > 0) { console.log(`Found ${events.length} ${league} events`); } } ``` Rate Limit Errors (429) [#rate-limit-errors-429] **Cause:** Polling too frequently for your plan tier. **Solutions:** 1. Increase `POLL_INTERVAL` to 60 seconds (60000) 2. Reduce number of leagues tracked 3. Upgrade to higher tier See [Rate Limiting Guide](/docs/info/rate-limiting) Missing Odds Data [#missing-odds-data] **Cause:** Some games may not have odds available yet. **Solution:** Add null check: ```javascript Object.entries(event.odds || {}).forEach(([oddID, odd]) => { // Check if any bookmaker has odds before processing if (!odd.byBookmaker || Object.keys(odd.byBookmaker).length === 0) return; // ... rest of code }); ``` Next Steps [#next-steps] Combine with Other Examples [#combine-with-other-examples] * **[Arbitrage Calculator](/docs/examples/arbitrage-calculator)** - Detect arb opportunities from line movements * **[Odds Comparison Dashboard](/docs/examples/odds-comparison-dashboard)** - Visualize movements in real-time * **[Player Props Analyzer](/docs/examples/player-props-analyzer)** - Track prop line movements Learn More [#learn-more] * **[Understanding oddID](/v2/data-types/odds)** - Deep dive into odds structure * **[Best Practices](/docs/info/best-practices)** - Optimize your polling strategy * **[SDK Guide](/docs/sdk/)** - Use our TypeScript/Python SDK for easier development --- # Odds Comparison Dashboard Example - React/Next.js URL: https://sportsgameodds.com/docs/examples/odds-comparison-dashboard Odds Comparison Dashboard Example - React/Next.js [#odds-comparison-dashboard-example---reactnextjs] Create a React/Next.js dashboard that compares odds across multiple sportsbooks and highlights the best lines. What You'll Build [#what-youll-build] A web dashboard that: * Displays live odds from all bookmakers side-by-side * Highlights best odds for each market * Auto-refreshes every 30 seconds * Shows line movement indicators * Responsive design for mobile/desktop **Perfect for:** Odds comparison sites, betting platforms, research tools Prerequisites [#prerequisites] * Node.js 18+ * Basic React/Next.js knowledge * SportsGameOdds API key ([Get one free](/pricing)) Complete Code [#complete-code] Step 1: Create Next.js Project [#step-1-create-nextjs-project] ```bash npx create-next-app@latest odds-dashboard cd odds-dashboard npm install ``` When prompted, choose: * TypeScript: Yes * ESLint: Yes * Tailwind CSS: Yes * App Router: Yes Step 2: Create API Route [#step-2-create-api-route] Create `app/api/odds/route.ts`: ```typescript // app/api/odds/route.ts const API_KEY = process.env.SPORTSGAMEODDS_KEY!; const API_BASE = "https://api.sportsgameodds.com/v2"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const league = searchParams.get("league") || "NBA"; try { const response = await fetch(`${API_BASE}/events?leagueID=${league}&finalized=false&oddsAvailable=true&limit=20`, { headers: { "x-api-key": API_KEY }, // Cache for 30 seconds next: { revalidate: 30 }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); return NextResponse.json(data); } catch (error) { console.error("Error fetching odds:", error); return NextResponse.json({ error: "Failed to fetch odds" }, { status: 500 }); } } ``` Step 3: Create Dashboard Component [#step-3-create-dashboard-component] Create `components/OddsDashboard.tsx`: ```typescript // components/OddsDashboard.tsx 'use client'; interface BookmakerOdds { odds: string; spread?: string; overUnder?: string; available: boolean; lastUpdatedAt: string; } interface Odd { oddID: string; betTypeID: string; sideID: string; periodID: string; byBookmaker: Record; } interface Event { eventID: string; teams: { home: { names: { long: string; medium: string; short: string } }; away: { names: { long: string; medium: string; short: string } }; }; status: { startsAt: string; }; odds: Record; } interface GroupedOdds { [bookmaker: string]: { home?: BookmakerOdds & { sideID: string }; away?: BookmakerOdds & { sideID: string }; }; } export default function OddsDashboard() { const [events, setEvents] = useState([]); const [league, setLeague] = useState('NBA'); const [loading, setLoading] = useState(true); const [lastUpdate, setLastUpdate] = useState(null); const fetchOdds = async () => { try { const response = await fetch(`/api/odds?league=${league}`); const data = await response.json(); if (data.data) { setEvents(data.data); setLastUpdate(new Date()); } } catch (error) { console.error('Error:', error); } finally { setLoading(false); } }; useEffect(() => { fetchOdds(); // Auto-refresh every 30 seconds const interval = setInterval(fetchOdds, 30000); return () => clearInterval(interval); }, [league]); const groupOddsByMarket = (odds: Record, betType: string): GroupedOdds => { const grouped: GroupedOdds = {}; Object.values(odds).forEach(odd => { // Only process full game odds for the specified bet type if (odd.betTypeID !== betType || odd.periodID !== 'game') return; // Iterate through each bookmaker's odds Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { if (!bookmakerOdds.available) return; if (!grouped[bookmakerID]) { grouped[bookmakerID] = {}; } grouped[bookmakerID][odd.sideID as 'home' | 'away'] = { ...bookmakerOdds, sideID: odd.sideID }; }); }); return grouped; }; const findBestOdds = (odds: Record, betType: string, side: 'home' | 'away') => { let bestBookmaker: string | null = null; let bestOddsValue = -Infinity; Object.values(odds).forEach(odd => { if (odd.betTypeID !== betType || odd.sideID !== side || odd.periodID !== 'game') return; Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { if (!bookmakerOdds.available) return; const oddsNum = parseInt(bookmakerOdds.odds); if (!isNaN(oddsNum) && oddsNum > bestOddsValue) { bestOddsValue = oddsNum; bestBookmaker = bookmakerID; } }); }); return bestBookmaker; }; const formatOdds = (price: string | number) => { const num = typeof price === 'string' ? parseInt(price) : price; if (isNaN(num)) return 'N/A'; return num > 0 ? `+${num}` : num.toString(); }; const formatSpread = (spread: string | undefined) => { if (!spread) return ''; // API already includes + or - sign return spread; }; if (loading) { return (
Loading odds...
); } return (

Odds Comparison

{['NBA', 'NFL', 'NHL', 'MLB'].map(l => ( ))}
{lastUpdate && (
Last updated: {lastUpdate.toLocaleTimeString()}
)}
{events.map(event => { const spreadOdds = groupOddsByMarket(event.odds, 'sp'); const bestHomeSpread = findBestOdds(event.odds, 'sp', 'home'); const bestAwaySpread = findBestOdds(event.odds, 'sp', 'away'); return (
{event.teams.away.names.long} @ {event.teams.home.names.long}
{event.status?.startsAt ? new Date(event.status.startsAt).toLocaleString() : 'TBD'}
{Object.entries(spreadOdds).map(([bookmaker, odds]) => ( ))}
Bookmaker {event.teams.away.names.medium} {event.teams.home.names.medium}
{bookmaker} {odds.away ? (
{formatSpread(odds.away.spread)} {formatOdds(odds.away.odds)}
) : ( - )}
{odds.home ? (
{formatSpread(odds.home.spread)} {formatOdds(odds.home.odds)}
) : ( - )}
Best Odds:
{bestAwaySpread && spreadOdds[bestAwaySpread]?.away && (
{event.teams.away.names.medium}:{' '} {formatSpread(spreadOdds[bestAwaySpread].away?.spread)}{' '} {formatOdds(spreadOdds[bestAwaySpread].away?.odds || '')}{' '} ({bestAwaySpread})
)} {bestHomeSpread && spreadOdds[bestHomeSpread]?.home && (
{event.teams.home.names.medium}:{' '} {formatSpread(spreadOdds[bestHomeSpread].home?.spread)}{' '} {formatOdds(spreadOdds[bestHomeSpread].home?.odds || '')}{' '} ({bestHomeSpread})
)}
); })}
{events.length === 0 && (
No upcoming {league} games found
)}
); } ``` Step 4: Update Main Page [#step-4-update-main-page] Update `app/page.tsx`: ```typescript // app/page.tsx export default function Home() { return ; } ``` Step 5: Add Environment Variable [#step-5-add-environment-variable] Create `.env.local`: ```bash SPORTSGAMEODDS_KEY=your_api_key_here ``` Step 6: Run It [#step-6-run-it] ```bash npm run dev ``` Open `http://localhost:3000` in your browser Expected Output [#expected-output] **Features you'll see:** * All bookmakers side-by-side * Best odds highlighted in green * League switcher (NBA/NFL/NHL/MLB) * Auto-refresh every 30 seconds * Start time for each game * Best odds summary at bottom How It Works [#how-it-works] 1. Proxy API Calls Through Backend [#1-proxy-api-calls-through-backend] ```typescript // app/api/odds/route.ts const response = await fetch(`${API_BASE}/events?leagueID=${league}&finalized=false`, { headers: { "x-api-key": API_KEY }, next: { revalidate: 30 }, // Cache for 30 seconds }); ``` **Why backend?** Keeps API key secure (never exposed in browser). 2. Process Nested Bookmaker Structure [#2-process-nested-bookmaker-structure] The API returns odds with a nested `byBookmaker` structure: ```json { "odds": { "points-home-game-sp-home": { "oddID": "points-home-game-sp-home", "betTypeID": "sp", "sideID": "home", "periodID": "game", "byBookmaker": { "draftkings": { "odds": "-110", "spread": "-5.5", "available": true }, "fanduel": { "odds": "-108", "spread": "-5.5", "available": true } } } } } ``` We transform this into a grouped format by bookmaker: ```typescript const groupOddsByMarket = (odds, betType) => { const grouped = {}; Object.values(odds).forEach((odd) => { if (odd.betTypeID !== betType || odd.periodID !== "game") return; Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { if (!grouped[bookmakerID]) { grouped[bookmakerID] = {}; } grouped[bookmakerID][odd.sideID] = bookmakerOdds; }); }); return grouped; }; ``` 3. Find Best Odds [#3-find-best-odds] ```typescript // Find best odds across all bookmakers for a specific market let bestBookmaker = null; let bestOddsValue = -Infinity; Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { const oddsNum = parseInt(bookmakerOdds.odds); // Higher (less negative or more positive) odds = better value if (oddsNum > bestOddsValue) { bestOddsValue = oddsNum; bestBookmaker = bookmakerID; } }); ``` **Higher odds value = better odds** for bettors (e.g., +150 is better than +120, -110 is better than -120). 4. Highlight Best Odds [#4-highlight-best-odds] ```typescript className={ bestAwaySpread === bookmaker ? 'bg-green-100 border-2 border-green-500 font-bold' : 'bg-gray-100' } ``` Green highlight shows best available odds. 5. Auto-Refresh [#5-auto-refresh] ```typescript useEffect(() => { fetchOdds(); const interval = setInterval(fetchOdds, 30000); // Every 30s return () => clearInterval(interval); }, [league]); ``` Enhancements [#enhancements] Add Moneyline and Totals [#add-moneyline-and-totals] ```typescript // In OddsDashboard component const [selectedMarket, setSelectedMarket] = useState('sp'); // Market selector // Use selectedMarket in groupOddsByMarket const odds = groupOddsByMarket(event.odds, selectedMarket); ``` Show Line Movement [#show-line-movement] ```typescript const [previousOdds, setPreviousOdds] = useState>({}); useEffect(() => { const current: Record = {}; events.forEach((event) => { Object.entries(event.odds).forEach(([oddID, odd]) => { Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { const key = `${oddID}-${bookmakerID}`; current[key] = parseInt(bookmakerOdds.odds); }); }); }); setPreviousOdds(current); }, [events]); // In render const getMovement = (oddID: string, bookmakerID: string, currentOdds: string) => { const key = `${oddID}-${bookmakerID}`; const previous = previousOdds[key]; const current = parseInt(currentOdds); if (!previous) return null; if (current > previous) return "up"; // Improved for bettor if (current < previous) return "down"; // Worsened for bettor return null; }; ``` Add Filters [#add-filters] ```typescript const [filters, setFilters] = useState({ minOdds: -200, maxOdds: 200, bookmakers: [] as string[], }); const filteredOdds = Object.entries(spreadOdds).filter(([bookmaker, odds]) => { // Filter by bookmaker if (filters.bookmakers.length > 0 && !filters.bookmakers.includes(bookmaker)) { return false; } // Filter by odds range const homeOdds = odds.home?.odds ? parseInt(odds.home.odds) : 0; const awayOdds = odds.away?.odds ? parseInt(odds.away.odds) : 0; return homeOdds >= filters.minOdds && homeOdds <= filters.maxOdds && awayOdds >= filters.minOdds && awayOdds <= filters.maxOdds; }); ``` Export to CSV [#export-to-csv] ```typescript const exportToCSV = () => { const rows: any[] = []; events.forEach((event) => { Object.entries(event.odds).forEach(([oddID, odd]) => { Object.entries(odd.byBookmaker || {}).forEach(([bookmaker, bOdds]) => { rows.push({ game: `${event.teams.away.names.long} @ ${event.teams.home.names.long}`, bookmaker, market: odd.betTypeID, side: odd.sideID, line: bOdds.spread || bOdds.overUnder, price: bOdds.odds, }); }); }); }); const csv = [Object.keys(rows[0]).join(","), ...rows.map((row) => Object.values(row).join(","))].join("\n"); const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `odds-${Date.now()}.csv`; a.click(); }; ``` Deployment [#deployment] Deploy to Vercel [#deploy-to-vercel] ```bash npm install -g vercel vercel ``` Add environment variable in Vercel dashboard: * `SPORTSGAMEODDS_KEY=your_api_key` Deploy to Netlify [#deploy-to-netlify] ```bash npm run build netlify deploy --prod --dir=.next ``` Add environment variable in Netlify settings. Troubleshooting [#troubleshooting] "Failed to fetch odds" Error [#failed-to-fetch-odds-error] **Cause:** Missing or invalid API key. **Solution:** Check `.env.local`: ```bash # Verify file exists cat .env.local # Should output: SPORTSGAMEODDS_KEY=your_key_here # Restart dev server after adding npm run dev ``` Odds Not Updating [#odds-not-updating] **Cause:** Browser caching. **Solution:** Add cache-busting: ```typescript const response = await fetch( `/api/odds?league=${league}&t=${Date.now()}`, // Add timestamp { cache: "no-store" } // Disable caching ); ``` Best Odds Not Highlighted [#best-odds-not-highlighted] **Cause:** Type mismatch (string vs number). **Solution:** Ensure odds values are parsed as numbers: ```typescript const oddsNum = parseInt(bookmakerOdds.odds); if (isNaN(oddsNum)) return; // Skip invalid odds ``` Next Steps [#next-steps] Combine with Other Examples [#combine-with-other-examples] * **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Add real-time movement alerts * **[Arbitrage Calculator](/docs/examples/arbitrage-calculator)** - Highlight arb opportunities in UI Learn More [#learn-more] * **[Understanding oddID](/v2/data-types/odds)** - Decode odds structure * **[SDK Guide](/docs/sdk/)** - Use TypeScript SDK for type safety * **[Best Practices](/docs/info/best-practices)** - Optimize performance --- # Parlay Calculator Example - JavaScript URL: https://sportsgameodds.com/docs/examples/parlay-builder Parlay Calculator Example - JavaScript [#parlay-calculator-example---javascript] Build a parlay calculator that combines multiple bets and calculates total odds and payout. What You'll Build [#what-youll-build] A JavaScript tool that: * Fetches live odds from multiple games * Allows selecting multiple bets (legs) * Calculates combined parlay odds * Shows potential payout * Supports American and decimal odds **Perfect for:** Parlay betting tools, sportsbook calculators, bet slip builders What is a Parlay? [#what-is-a-parlay] A **parlay** (or accumulator) combines multiple bets into one wager. **All bets must win** for the parlay to pay out, but the payout is much higher than individual bets. **Example:** * Lakers -5.5 @ -110 * Warriors ML @ -150 * Over 220.5 @ -110 **Individual bets:** Risk $10 each = $30 total -> Win \~$27 **As parlay:** Risk $30 -> Win \~$170 Prerequisites [#prerequisites] * Node.js 18+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic JavaScript knowledge Complete Code [#complete-code] Step 1: Setup Project [#step-1-setup-project] ```bash mkdir parlay-calculator && cd parlay-calculator npm init -y npm install node-fetch@2 ``` > **Note:** We use `node-fetch@2` because v3+ is ESM-only. The `@2` ensures CommonJS compatibility with `require()`. Step 2: Create calculator.js [#step-2-create-calculatorjs] ```javascript // calculator.js const fetch = require("node-fetch"); const readline = require("readline"); const API_KEY = process.env.SPORTSGAMEODDS_KEY; const API_BASE = "https://api.sportsgameodds.com/v2"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // Utility functions const americanToDecimal = (american) => { if (american > 0) { return american / 100 + 1; } return 100 / Math.abs(american) + 1; }; const decimalToAmerican = (decimal) => { if (decimal >= 2.0) { return Math.round((decimal - 1) * 100); } return Math.round(-100 / (decimal - 1)); }; const formatOdds = (american) => { return american > 0 ? `+${american}` : american.toString(); }; // Fetch games async function fetchGames(league = "NBA") { try { const response = await fetch(`${API_BASE}/events?leagueID=${league}&finalized=false&oddsAvailable=true&limit=20`, { headers: { "x-api-key": API_KEY } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error("Error fetching games:", error.message); return { data: [] }; } } // Display available bets function displayAvailableBets(events) { const bets = []; let betId = 1; events.forEach((event) => { // Get team names from nested structure const awayName = event.teams.away.names.long; const homeName = event.teams.home.names.long; const matchup = `${awayName} @ ${homeName}`; // Get spread odds (focusing on DraftKings for simplicity) const spreadOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "sp" && odd.periodID === "game" && odd.byBookmaker?.draftkings); spreadOdds.forEach((odd) => { const dkOdds = odd.byBookmaker.draftkings; if (!dkOdds.available) return; const team = odd.sideID === "home" ? homeName : awayName; const spread = dkOdds.spread || "0"; // Spread already includes + or - from API const line = spread; bets.push({ id: betId++, matchup, description: `${team} ${line}`, odds: parseInt(dkOdds.odds), decimalOdds: americanToDecimal(parseInt(dkOdds.odds)), eventID: event.eventID, oddID: odd.oddID, betTypeID: odd.betTypeID, sideID: odd.sideID, spread: dkOdds.spread, }); }); // Get moneyline odds const mlOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "ml" && odd.periodID === "game" && odd.byBookmaker?.draftkings); mlOdds.forEach((odd) => { const dkOdds = odd.byBookmaker.draftkings; if (!dkOdds.available) return; const team = odd.sideID === "home" ? homeName : awayName; bets.push({ id: betId++, matchup, description: `${team} ML`, odds: parseInt(dkOdds.odds), decimalOdds: americanToDecimal(parseInt(dkOdds.odds)), eventID: event.eventID, oddID: odd.oddID, }); }); // Get totals (over/under) const totalOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "ou" && odd.periodID === "game" && odd.byBookmaker?.draftkings); totalOdds.forEach((odd) => { const dkOdds = odd.byBookmaker.draftkings; if (!dkOdds.available) return; const side = odd.sideID === "over" ? "Over" : "Under"; bets.push({ id: betId++, matchup, description: `${side} ${dkOdds.overUnder}`, odds: parseInt(dkOdds.odds), decimalOdds: americanToDecimal(parseInt(dkOdds.odds)), eventID: event.eventID, oddID: odd.oddID, betTypeID: odd.betTypeID, sideID: odd.sideID, overUnder: dkOdds.overUnder, }); }); }); return bets; } // Calculate parlay odds function calculateParlay(legs, stake = 100) { if (legs.length === 0) { return null; } // Multiply all decimal odds const combinedDecimalOdds = legs.reduce((acc, leg) => acc * leg.decimalOdds, 1); // Convert back to American const combinedAmericanOdds = decimalToAmerican(combinedDecimalOdds); // Calculate payout const payout = stake * combinedDecimalOdds; const profit = payout - stake; return { legs: legs.length, combinedDecimalOdds: combinedDecimalOdds.toFixed(2), combinedAmericanOdds: formatOdds(combinedAmericanOdds), stake, payout: payout.toFixed(2), profit: profit.toFixed(2), }; } // Display parlay summary function displayParlay(parlay, legs) { console.log("\n" + "=".repeat(80)); console.log("PARLAY SUMMARY"); console.log("=".repeat(80)); console.log("\nSelected Legs:\n"); legs.forEach((leg, i) => { console.log(`${i + 1}. ${leg.description} (${leg.matchup})`); console.log(` Odds: ${formatOdds(leg.odds)} (${leg.decimalOdds.toFixed(2)} decimal)`); console.log(); }); console.log("-".repeat(80)); console.log(`\nCombined Odds: ${parlay.combinedAmericanOdds} (${parlay.combinedDecimalOdds} decimal)`); console.log(`Stake: $${parlay.stake.toFixed(2)}`); console.log(`Potential Payout: $${parlay.payout}`); console.log(`Potential Profit: $${parlay.profit}`); // Calculate implied probability const impliedProb = (1 / parseFloat(parlay.combinedDecimalOdds)) * 100; console.log(`Implied Probability: ${impliedProb.toFixed(1)}%`); console.log("\n" + "=".repeat(80)); console.log("Remember: ALL legs must win for parlay to pay out!"); console.log("=".repeat(80) + "\n"); } // Interactive CLI async function main() { console.log("Parlay Calculator - SportsGameOdds API\n"); // Fetch games console.log("Fetching NBA games...\n"); const { data: events } = await fetchGames("NBA"); if (events.length === 0) { console.log("No upcoming NBA games found"); console.log("Tip: Try during NBA season (October-June) or use a different league"); rl.close(); process.exit(1); } const availableBets = displayAvailableBets(events); if (availableBets.length === 0) { console.log("No DraftKings odds available for these games"); console.log("Tip: Try a different bookmaker or check back later"); rl.close(); process.exit(1); } console.log("Available Bets:\n"); availableBets.forEach((bet) => { console.log(`${bet.id}. ${bet.description} ${formatOdds(bet.odds)} - ${bet.matchup}`); }); const selectedLegs = []; const selectLeg = () => { rl.question('\nEnter bet ID to add to parlay (or "done" to finish): ', (answer) => { if (answer.toLowerCase() === "done") { if (selectedLegs.length < 2) { console.log("\nParlay requires at least 2 legs!\n"); selectLeg(); return; } rl.question("\nEnter stake amount ($): ", (stakeInput) => { const stake = parseFloat(stakeInput) || 100; const parlay = calculateParlay(selectedLegs, stake); displayParlay(parlay, selectedLegs); rl.close(); }); return; } const betId = parseInt(answer); const bet = availableBets.find((b) => b.id === betId); if (!bet) { console.log("Invalid bet ID"); selectLeg(); return; } // Check if already selected if (selectedLegs.find((l) => l.id === bet.id)) { console.log("Bet already in parlay"); selectLeg(); return; } // Check if from same game (not allowed in most parlays) const sameGame = selectedLegs.find((l) => l.eventID === bet.eventID); if (sameGame) { console.log("Cannot parlay multiple bets from same game (correlated)"); selectLeg(); return; } selectedLegs.push(bet); console.log(`Added: ${bet.description} ${formatOdds(bet.odds)}`); console.log(` Current legs: ${selectedLegs.length}`); if (selectedLegs.length >= 2) { const preview = calculateParlay(selectedLegs, 100); console.log(` Preview: $100 -> $${preview.payout} (${preview.combinedAmericanOdds})`); } selectLeg(); }); }; selectLeg(); } main(); ``` Step 3: Run It [#step-3-run-it] ```bash export SPORTSGAMEODDS_KEY=your_api_key_here node calculator.js ``` Expected Output [#expected-output] ``` Parlay Calculator - SportsGameOdds API Fetching NBA games... Available Bets: 1. Los Angeles Lakers -5.5 -110 - Boston Celtics @ Los Angeles Lakers 2. Boston Celtics +5.5 -110 - Boston Celtics @ Los Angeles Lakers 3. Los Angeles Lakers ML -220 - Boston Celtics @ Los Angeles Lakers 4. Boston Celtics ML +180 - Boston Celtics @ Los Angeles Lakers 5. Over 220.5 -110 - Boston Celtics @ Los Angeles Lakers 6. Under 220.5 -110 - Boston Celtics @ Los Angeles Lakers 7. Golden State Warriors -3.5 -110 - Phoenix Suns @ Golden State Warriors ... Enter bet ID to add to parlay (or "done" to finish): 1 Added: Los Angeles Lakers -5.5 -110 Current legs: 1 Enter bet ID to add to parlay (or "done" to finish): 7 Added: Golden State Warriors -3.5 -110 Current legs: 2 Preview: $100 -> $364.46 (+264) Enter bet ID to add to parlay (or "done" to finish): 13 Added: Miami Heat ML +150 Current legs: 3 Preview: $100 -> $911.16 (+811) Enter bet ID to add to parlay (or "done" to finish): done Enter stake amount ($): 50 ================================================================================ PARLAY SUMMARY ================================================================================ Selected Legs: 1. Los Angeles Lakers -5.5 (Boston Celtics @ Los Angeles Lakers) Odds: -110 (1.91 decimal) 2. Golden State Warriors -3.5 (Phoenix Suns @ Golden State Warriors) Odds: -110 (1.91 decimal) 3. Miami Heat ML (Orlando Magic @ Miami Heat) Odds: +150 (2.50 decimal) -------------------------------------------------------------------------------- Combined Odds: +811 (9.11 decimal) Stake: $50.00 Potential Payout: $455.58 Potential Profit: $405.58 Implied Probability: 11.0% ================================================================================ Remember: ALL legs must win for parlay to pay out! ================================================================================ ``` How It Works [#how-it-works] 1. Calculate Combined Odds [#1-calculate-combined-odds] ```javascript // Multiply all decimal odds const combinedDecimalOdds = legs.reduce((acc, leg) => acc * leg.decimalOdds, 1); ``` **Example:** * Leg 1: -110 -> 1.91 decimal * Leg 2: -110 -> 1.91 decimal * Leg 3: +150 -> 2.50 decimal **Combined:** 1.91 x 1.91 x 2.50 = **9.11 decimal** (+811 American) 2. Calculate Payout [#2-calculate-payout] ```javascript const payout = stake * combinedDecimalOdds; const profit = payout - stake; ``` **Example:** * Stake: $50 * Combined odds: 9.11 decimal * Payout: $50 x 9.11 = **$455.58** * Profit: $455.58 - $50 = **$405.58** 3. Prevent Correlated Bets [#3-prevent-correlated-bets] ```javascript const sameGame = selectedLegs.find((l) => l.eventID === bet.eventID); if (sameGame) { console.log("Cannot parlay bets from same game"); return; } ``` **Why?** Most sportsbooks don't allow parlaying bets from the same game because they're correlated (e.g., if Lakers win big, they'll likely cover the spread too). Parlay Odds Table [#parlay-odds-table] Quick reference for common parlay combinations: | Legs | All @ -110 | Combined Odds | $100 Pays | | ---- | ---------------------------- | ------------- | --------- | | 2 | -110, -110 | +264 | $264 | | 3 | -110, -110, -110 | +596 | $596 | | 4 | -110, -110, -110, -110 | +1228 | $1,228 | | 5 | -110, -110, -110, -110, -110 | +2436 | $2,436 | **Mixed odds example (2-leg):** * -110 + +150 = +355 ($100 -> $455) * -200 + +200 = +150 ($100 -> $250) * +100 + +100 = +300 ($100 -> $400) Enhancements [#enhancements] Add Same Game Parlays [#add-same-game-parlays] Some books allow same-game parlays (SGP). You need to check for correlation: ```javascript function checkCorrelation(leg1, leg2) { // Example: Can't parlay "Lakers -5.5" with "Over 220" // If Lakers cover, game likely went over if (leg1.eventID !== leg2.eventID) { return false; // Different games, no correlation } // Check for correlated bets const correlatedPairs = [ ["sp", "ou"], // Spread and total often correlated ["ml", "ou"], // Moneyline and total often correlated ]; const pair = [leg1.betTypeID, leg2.betTypeID].sort(); return correlatedPairs.some((cp) => cp[0] === pair[0] && cp[1] === pair[1]); } ``` Calculate True Odds [#calculate-true-odds] Adjust for bookmaker vig: ```javascript function calculateTrueOdds(americanOdds) { // Remove vig to get true probability const decimal = americanToDecimal(americanOdds); const impliedProb = 1 / decimal; // Assuming ~5% vig const trueProb = impliedProb * 0.95; return 1 / trueProb; } // Use true odds for expected value calculations const trueDecimal = calculateTrueOdds(leg.odds); ``` Add Round Robin [#add-round-robin] Round robin creates multiple parlays from your selections: ```javascript function generateRoundRobin(legs, parlaySize) { const combinations = []; function combine(start, combo) { if (combo.length === parlaySize) { combinations.push([...combo]); return; } for (let i = start; i < legs.length; i++) { combo.push(legs[i]); combine(i + 1, combo); combo.pop(); } } combine(0, []); return combinations; } // Example: 4 legs, 2-team parlays (6 combinations) const legs = [leg1, leg2, leg3, leg4]; const roundRobin = generateRoundRobin(legs, 2); console.log(`Creating ${roundRobin.length} 2-team parlays`); roundRobin.forEach((combo, i) => { const parlay = calculateParlay(combo, 10); // $10 each console.log(`Parlay ${i + 1}: $${parlay.payout}`); }); ``` Show Win Probability [#show-win-probability] ```javascript function calculateWinProbability(legs) { // Convert odds to probabilities (removing vig) const probs = legs.map((leg) => { const decimal = leg.decimalOdds; return (1 / decimal) * 0.95; // Adjust for ~5% vig }); // Multiply probabilities (independent events) const combinedProb = probs.reduce((acc, prob) => acc * prob, 1); return (combinedProb * 100).toFixed(1); } console.log(`Win Probability: ${calculateWinProbability(legs)}%`); ``` Add Teaser Calculation [#add-teaser-calculation] Teasers adjust spreads in your favor for lower payout: ```javascript function applyTeaser(legs, points = 6) { return legs.map((leg) => { if (leg.betTypeID === "sp") { // Adjust spread by teaser points const currentSpread = parseFloat(leg.spread); const newSpread = leg.sideID === "home" ? currentSpread - points : currentSpread + points; return { ...leg, spread: newSpread.toString(), odds: -110, // Standard teaser odds decimalOdds: americanToDecimal(-110), }; } if (leg.betTypeID === "ou") { // Move total in bettor's favor const currentTotal = parseFloat(leg.overUnder); const newTotal = leg.sideID === "over" ? currentTotal - points : currentTotal + points; return { ...leg, overUnder: newTotal.toString(), odds: -110, decimalOdds: americanToDecimal(-110), }; } return leg; }); } // 6-point teaser const teaserLegs = applyTeaser(selectedLegs, 6); const teaserParlay = calculateParlay(teaserLegs, 100); ``` Troubleshooting [#troubleshooting] "Cannot parlay bets from same game" [#cannot-parlay-bets-from-same-game] **Cause:** Selected multiple bets from one game. **Solution:** Most sportsbooks don't allow this. Use "Same Game Parlay" feature if available (different odds calculation). Calculated odds don't match sportsbook [#calculated-odds-dont-match-sportsbook] **Causes:** 1. **Correlation adjustments** - Books adjust odds for correlated bets 2. **Rounding** - Slight rounding differences 3. **Promotions** - Odds boosts not reflected **Solution:** Use our calculator for estimates. Always verify with actual sportsbook before placing. Parlay odds seem too good [#parlay-odds-seem-too-good] **Remember:** Parlays are -EV (negative expected value) over time. The high payouts are offset by low win probability. **Example:** * 3-leg parlay @ -110 each * Pays: +596 ($100 -> $696) * True odds: Each leg 52.4% -> Combined 14.4% * Expected value: $696 x 0.144 = $100.22 (barely break-even after vig) Best Practices [#best-practices] 1. Limit Leg Count [#1-limit-leg-count] **Recommendation:** 2-4 legs max **Why:** * Win probability drops exponentially * 5+ leg parlays rarely hit * Better to do multiple 2-leg parlays | Legs | Win % (all 50/50) | Reality | | ---- | ----------------- | ---------------- | | 2 | 25% | \~22% (with vig) | | 3 | 12.5% | \~10% | | 4 | 6.25% | \~5% | | 5 | 3.12% | \~2% | 2. Avoid Heavy Favorites [#2-avoid-heavy-favorites] **Problem:** Adding -400 favorites doesn't help much **Example:** * 2-leg: -110 + -110 = +264 * 2-leg: -110 + -400 = +110 (much worse!) **Better:** Use favorites in straights, underdogs in parlays 3. Shop for Best Odds [#3-shop-for-best-odds] **Impact of small differences:** Base parlay (3 legs @ -110 each): **+596** If you improve each leg by 5 cents (-105): * 3 legs @ -105 each: **+649** (+53 better!) 4. Consider Round Robins [#4-consider-round-robins] Instead of one 4-leg parlay: * **Option A:** 1x 4-leg @ +1228 (risk $100, all must win) * **Option B:** 6x 2-leg @ +264 each (risk $100 total, need 2+ to profit) **Option B is safer** - you can lose 1-2 legs and still profit. Next Steps [#next-steps] Combine with Other Examples [#combine-with-other-examples] * **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Monitor parlay legs in real-time * **[Odds Comparison Dashboard](/docs/examples/odds-comparison-dashboard)** - Find best odds for each leg Learn More [#learn-more] * **[Understanding oddID](/v2/data-types/odds)** - Decode bet identifiers * **[Best Practices](/docs/info/best-practices)** - Optimize your betting strategy * **[Glossary](/docs/info/glossary)** - Learn all terminology --- # Player Props Analyzer Example - Python URL: https://sportsgameodds.com/docs/examples/player-props-analyzer Player Props Analyzer Example - Python [#player-props-analyzer-example---python] Analyze player prop bets by fetching props for a specific game and comparing lines across bookmakers. What You'll Build [#what-youll-build] A Python script that: * Fetches all player props for an NBA game * Compares lines across bookmakers * Shows consensus lines * Identifies outlier books * Highlights potential value bets **Perfect for:** Finding value in player props, comparing bookmaker offerings, prop bet research Prerequisites [#prerequisites] * Python 3.8+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic Python knowledge Complete Code [#complete-code] Step 1: Setup Project [#step-1-setup-project] ```bash mkdir props-analyzer && cd props-analyzer python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install requests ``` Step 2: Create analyzer.py [#step-2-create-analyzerpy] ```python # analyzer.py from collections import defaultdict from statistics import mean, median API_KEY = os.environ.get('SPORTSGAMEODDS_KEY') API_BASE = 'https://api.sportsgameodds.com/v2' def fetch_game_props(event_id): """Fetch all props for a specific game""" try: response = requests.get( f'{API_BASE}/events', params={'eventIDs': event_id}, headers={'x-api-key': API_KEY} ) response.raise_for_status() data = response.json() if not data['data']: print(f'Event {event_id} not found') return None return data['data'][0] except requests.exceptions.RequestException as e: print(f'Error fetching event: {e}') return None def find_upcoming_nba_games(): """Find upcoming NBA games to analyze""" try: response = requests.get( f'{API_BASE}/events', params={ 'leagueID': 'NBA', 'finalized': 'false', 'oddsAvailable': 'true', 'limit': 10 }, headers={'x-api-key': API_KEY} ) response.raise_for_status() return response.json()['data'] except requests.exceptions.RequestException as e: print(f'Error fetching games: {e}') return [] def extract_player_props(odds): """ Extract and organize player props from odds data. Player props are identified by statEntityID NOT being 'all', 'home', or 'away'. The player identifier is stored in statEntityID (e.g., 'LEBRON_JAMES_1_NBA'). """ props_by_player = defaultdict(lambda: defaultdict(list)) for odd_id, odd in odds.items(): stat_entity = odd.get('statEntityID', 'all') # Skip team-level odds (not player props) if stat_entity in ['all', 'home', 'away']: continue # This is a player prop player = stat_entity stat_id = odd.get('statID', 'unknown') bet_type = odd['betTypeID'] side_id = odd['sideID'] # Build a readable prop type from statID and betTypeID prop_type = f"{stat_id}_{bet_type}" # Process each bookmaker's odds for bookmaker_id, bookmaker_data in (odd.get('byBookmaker') or {}).items(): if not bookmaker_data.get('available', True): continue props_by_player[player][prop_type].append({ 'bookmaker': bookmaker_id, 'line': bookmaker_data.get('overUnder'), 'over_price': bookmaker_data.get('odds') if side_id == 'over' else None, 'under_price': bookmaker_data.get('odds') if side_id == 'under' else None, 'side': side_id, 'lastUpdatedAt': bookmaker_data.get('lastUpdatedAt') }) return props_by_player def format_player_name(player_id): """Convert player ID like 'LEBRON_JAMES_1_NBA' to 'LeBron James'""" if not player_id: return 'Unknown' # Remove suffix like '_1_NBA' parts = player_id.split('_') if len(parts) >= 2: # Take all parts except the last 2 (number and league) name_parts = parts[:-2] if len(parts) > 2 else parts # Capitalize each word return ' '.join(word.capitalize() for word in name_parts) return player_id def calculate_consensus(props): """Calculate consensus line from all bookmakers""" lines = [float(p['line']) for p in props if p['line'] is not None] if not lines: return None return { 'mean': round(mean(lines), 1), 'median': median(lines), 'min': min(lines), 'max': max(lines), 'books': len(lines) } def find_outliers(props, consensus): """Find bookmakers with outlier lines""" if not consensus: return [] mean_line = consensus['mean'] outliers = [] for prop in props: if prop['line'] is None: continue line_value = float(prop['line']) diff = abs(line_value - mean_line) # Outlier if differs by 1+ from consensus if diff >= 1.0: outliers.append({ 'bookmaker': prop['bookmaker'], 'line': line_value, 'diff': diff, 'direction': 'higher' if line_value > mean_line else 'lower' }) return sorted(outliers, key=lambda x: x['diff'], reverse=True) def analyze_player_props(event): """Analyze all player props for an event""" away_name = event['teams']['away']['names']['long'] home_name = event['teams']['home']['names']['long'] matchup = f"{away_name} @ {home_name}" print(f'\n{matchup}') start_time = event.get('status', {}).get('startsAt', 'TBD') print(f"Start: {start_time}") print('=' * 80) props_by_player = extract_player_props(event.get('odds', {})) if not props_by_player: print('No player props found for this game') print('\nNote: Player props are typically available 12-24 hours before game time.') return print(f"\nFound props for {len(props_by_player)} players\n") for player, prop_types in sorted(props_by_player.items()): display_name = format_player_name(player) print(f'\n{display_name}') print('-' * 80) for prop_type, props in sorted(prop_types.items()): # Combine over/under entries by bookmaker combined_props = {} for prop in props: bookmaker = prop['bookmaker'] if bookmaker not in combined_props: combined_props[bookmaker] = {'bookmaker': bookmaker, 'line': prop['line']} if prop['over_price']: combined_props[bookmaker]['over'] = prop['over_price'] if prop['under_price']: combined_props[bookmaker]['under'] = prop['under_price'] props_list = list(combined_props.values()) if not props_list: continue # Calculate consensus consensus = calculate_consensus(props_list) # Find outliers outliers = find_outliers(props_list, consensus) # Format prop name for display prop_name = prop_type.replace('_', ' ').title() print(f'\n {prop_name}') if consensus: print(f" Consensus: {consensus['mean']} (from {consensus['books']} books)") print(f" Range: {consensus['min']} - {consensus['max']}") print(f'\n {"Bookmaker":<15} {"Line":<8} {"Over":<10} {"Under":<10} {"Notes"}') print(f' {"-" * 70}') for prop in sorted(props_list, key=lambda x: float(x['line']) if x['line'] else 0): line = f"{prop['line']}" if prop['line'] else 'N/A' over = f"{prop.get('over')}" if prop.get('over') else '-' under = f"{prop.get('under')}" if prop.get('under') else '-' # Check if outlier outlier = next((o for o in outliers if o['bookmaker'] == prop['bookmaker']), None) notes = '' if outlier: notes = f"{outlier['diff']:.1f}pts {outlier['direction']}" print(f" {prop['bookmaker']:<15} {line:<8} {over:<10} {under:<10} {notes}") # Highlight best value if outliers: print(f'\n Value Opportunities:') for outlier in outliers[:2]: # Top 2 if outlier['direction'] == 'lower': print(f" - {outlier['bookmaker']}: Line {outlier['diff']:.1f}pts lower (consider OVER)") else: print(f" - {outlier['bookmaker']}: Line {outlier['diff']:.1f}pts higher (consider UNDER)") def main(): print('SportsGameOdds Player Props Analyzer\n') # Option 1: Analyze specific event event_id = os.environ.get('EVENT_ID') if event_id: event = fetch_game_props(event_id) if event: analyze_player_props(event) return # Option 2: Show upcoming games print('Upcoming NBA Games:\n') games = find_upcoming_nba_games() if not games: print('No upcoming NBA games found') print('Tip: Check during NBA season (October-June)') return for i, game in enumerate(games, 1): away_name = game['teams']['away']['names']['long'] home_name = game['teams']['home']['names']['long'] matchup = f"{away_name} @ {home_name}" start_time = game.get('status', {}).get('startsAt', 'TBD') # Count player props (statEntityID not in team-level values) prop_count = sum( 1 for odd in game.get('odds', {}).values() if odd.get('statEntityID') not in ['all', 'home', 'away', None] ) print(f"{i}. {matchup}") print(f" Event ID: {game['eventID']}") print(f" Start: {start_time}") print(f" Player Props Available: {prop_count}") print() print('\nTo analyze a specific game:') print('export EVENT_ID=') print('python analyzer.py') if __name__ == '__main__': main() ``` Step 3: Run It [#step-3-run-it] **List upcoming games:** ```bash export SPORTSGAMEODDS_KEY=your_api_key_here python analyzer.py ``` **Analyze specific game:** ```bash export SPORTSGAMEODDS_KEY=your_api_key_here export EVENT_ID=abc123 python analyzer.py ``` Expected Output [#expected-output] ``` SportsGameOdds Player Props Analyzer Boston Celtics @ Los Angeles Lakers Start: 2024-11-14T19:00:00Z ================================================================================ Found props for 15 players LeBron James -------------------------------------------------------------------------------- Points Ou Consensus: 25.5 (from 8 books) Range: 24.5 - 26.5 Bookmaker Line Over Under Notes ---------------------------------------------------------------------- betmgm 24.5 -105 -115 1.0pts lower draftkings 25.5 -110 -110 fanduel 25.5 -108 -112 caesars 25.5 -110 -110 pointsbet 26.0 -105 -115 barstool 26.5 -110 -110 1.0pts higher Value Opportunities: - betmgm: Line 1.0pts lower (consider OVER) - barstool: Line 1.0pts higher (consider UNDER) Rebounds Ou Consensus: 7.5 (from 7 books) Range: 7.5 - 8.5 Bookmaker Line Over Under Notes ---------------------------------------------------------------------- draftkings 7.5 -110 -110 fanduel 7.5 -115 -105 betmgm 7.5 -110 -110 caesars 8.5 -120 +100 1.0pts higher Value Opportunities: - caesars: Line 1.0pts higher (consider UNDER) Assists Ou Consensus: 6.5 (from 6 books) Range: 6.5 - 6.5 Bookmaker Line Over Under Notes ---------------------------------------------------------------------- draftkings 6.5 -120 +100 fanduel 6.5 -110 -110 betmgm 6.5 -105 -115 Anthony Davis -------------------------------------------------------------------------------- Points Ou Consensus: 23.5 (from 7 books) Range: 22.5 - 24.5 ... ``` How It Works [#how-it-works] 1. Identify Player Props [#1-identify-player-props] Player props are identified by their `statEntityID`. Team-level odds have values like `'all'`, `'home'`, or `'away'`, while player props have player identifiers: ```python stat_entity = odd.get('statEntityID', 'all') # Skip team-level odds if stat_entity in ['all', 'home', 'away']: continue # This is a player prop - statEntityID is the player (e.g., 'LEBRON_JAMES_1_NBA') player = stat_entity ``` 2. Process Bookmaker Odds [#2-process-bookmaker-odds] Each odd contains prices from multiple bookmakers in the `byBookmaker` object: ```python for bookmaker_id, bookmaker_data in odd.get('byBookmaker', {}).items(): if not bookmaker_data.get('available', True): continue line = bookmaker_data.get('overUnder') # e.g., "25.5" price = bookmaker_data.get('odds') # e.g., "-110" ``` 3. Calculate Consensus [#3-calculate-consensus] ```python lines = [float(p['line']) for p in props if p['line'] is not None] consensus = { 'mean': round(mean(lines), 1), # Average line 'median': median(lines), # Middle value 'min': min(lines), # Lowest line 'max': max(lines) # Highest line } ``` **Example:** * DraftKings: 25.5 * FanDuel: 25.5 * BetMGM: 24.5 * Caesars: 26.5 **Consensus:** 25.5 (mean), Range: 24.5 - 26.5 4. Find Outliers [#4-find-outliers] ```python diff = abs(float(prop['line']) - mean_line) if diff >= 1.0: # 1+ points from consensus outliers.append({ 'bookmaker': prop['bookmaker'], 'diff': diff, 'direction': 'higher' if prop['line'] > mean_line else 'lower' }) ``` **Why outliers matter:** * Lower line = easier to hit OVER * Higher line = easier to hit UNDER * Potential value if one book is off-market 5. Identify Value [#5-identify-value] ```python if outlier['direction'] == 'lower': print(f"Consider OVER (easier to beat {outlier['line']})") else: print(f"Consider UNDER (easier to stay under {outlier['line']})") ``` Real-World Example [#real-world-example] **LeBron James Points:** | Bookmaker | Line | Over | Under | | ---------- | -------- | -------- | -------- | | DraftKings | 25.5 | -110 | -110 | | FanDuel | 25.5 | -108 | -112 | | **BetMGM** | **24.5** | **-105** | **-115** | | Caesars | 25.5 | -110 | -110 | | Barstool | 26.5 | -110 | -110 | **Consensus:** 25.5 points **Analysis:** * **BetMGM outlier:** 24.5 (1 point lower) * **Value:** OVER 24.5 @ -105 * **Why:** Easier to hit than market consensus of 25.5 * If LeBron scores 25 points, you win at BetMGM but lose everywhere else Enhancements [#enhancements] Track Props Over Time [#track-props-over-time] ```python from datetime import datetime def save_props_snapshot(event, props): """Save props for historical tracking""" snapshot = { 'timestamp': datetime.now().isoformat(), 'event_id': event['eventID'], 'props': props } with open(f"snapshots/{event['eventID']}.jsonl", 'a') as f: f.write(json.dumps(snapshot) + '\n') def detect_line_movement(event_id): """Compare current props to historical snapshots""" snapshots = [] try: with open(f"snapshots/{event_id}.jsonl", 'r') as f: for line in f: snapshots.append(json.loads(line)) except FileNotFoundError: return [] movements = [] for i in range(1, len(snapshots)): prev = snapshots[i-1] curr = snapshots[i] # Compare lines # ... (implementation) return movements ``` Filter by Prop Type [#filter-by-prop-type] ```python def main(): prop_filter = input('Filter by prop type (points/rebounds/assists/all): ') # In analyze function for prop_type, props in sorted(prop_types.items()): if prop_filter != 'all' and prop_filter not in prop_type.lower(): continue # Skip non-matching props ``` Export to Spreadsheet [#export-to-spreadsheet] ```python def export_props_to_csv(props_by_player, filename='props.csv'): rows = [] for player, prop_types in props_by_player.items(): for prop_type, props in prop_types.items(): for prop in props: rows.append({ 'Player': format_player_name(player), 'Prop': prop_type, 'Bookmaker': prop['bookmaker'], 'Line': prop['line'], 'Over': prop.get('over_price'), 'Under': prop.get('under_price') }) with open(filename, 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows) print(f'Exported to {filename}') ``` Add EV Calculations [#add-ev-calculations] ```python def american_to_decimal(american): """Convert American odds to decimal""" odds = int(american) if odds > 0: return (odds / 100) + 1 return (100 / abs(odds)) + 1 def calculate_ev(line, consensus_line, price, hit_rate=0.5): """ Calculate expected value Assumes hit_rate based on distance from consensus """ # Adjust hit rate based on line difference diff = float(line) - consensus_line if diff < 0: # Lower line (easier to hit over) adjusted_hit_rate = hit_rate + (abs(diff) * 0.05) else: adjusted_hit_rate = hit_rate - (diff * 0.05) adjusted_hit_rate = max(0.3, min(0.7, adjusted_hit_rate)) # Calculate EV decimal_odds = american_to_decimal(price) ev = (adjusted_hit_rate * (decimal_odds - 1)) - (1 - adjusted_hit_rate) return ev * 100 # As percentage ``` Troubleshooting [#troubleshooting] "No player props found" [#no-player-props-found] **Causes:** 1. Props not posted yet (too early before game) 2. Event has no props available 3. Wrong event ID **Solution:** Check when props are typically posted: * NBA: 12-24 hours before tip-off * NFL: 48+ hours before kickoff Props missing for some players [#props-missing-for-some-players] **Cause:** Bookmakers don't offer props for all players (usually starters + key bench players only). **Expected:** Props for 10-15 players per NBA game. Player names showing as IDs [#player-names-showing-as-ids] **Cause:** The API returns player IDs like `LEBRON_JAMES_1_NBA`. **Solution:** Use the `format_player_name` function to convert: ```python def format_player_name(player_id): parts = player_id.split('_') if len(parts) >= 2: name_parts = parts[:-2] if len(parts) > 2 else parts return ' '.join(word.capitalize() for word in name_parts) return player_id ``` Next Steps [#next-steps] Combine with Other Examples [#combine-with-other-examples] * **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Monitor prop line movement * **[Arbitrage Calculator](/docs/examples/arbitrage-calculator)** - Find prop arbitrage Learn More [#learn-more] * **[Understanding oddID](/v2/data-types/odds)** - Decode prop identifiers * **[Best Practices](/docs/info/best-practices)** - Optimize queries --- # Pagination Guide - Cursor-Based Batching URL: https://sportsgameodds.com/docs/guides/data-batches Pagination Guide - Cursor-Based Batching [#pagination-guide---cursor-based-batching] This following information only applies to the `/events/`, `/teams`, and `/players` endpoints. Other endpoints will always return all of the results which match your query. However, there may be hundreds or even thousands of results to these endpoints, so they must be fetched in batches. How It Works [#how-it-works] 1. Make a request. Each request has a limit on the number of items returned. 2. If there are more items to show, you'll get a `nextCursor` in the response. 3. Repeat the query but put the value from last request's `nextCursor` in the `cursor` parameter. 4. Repeat this until you no longer receive a `nextCursor` in the response in order to get all of the results. The cursor Parameter [#the-cursor-parameter] This tells the API where to pick up from. If you received a response from the API and there are more items to show, you'll get a `nextCursor` at the top level of the response. Use this value in the `cursor` parameter of your next request to pick up where you left off. Notes on using `cursor`: * Always use the value from the last response's `nextCursor` property. Don't try to reverse-engineer the cursor value yourself * Don't change any query parameters between cursor requests. This will cause the cursor to not function properly. * Don't try to reverse-engineer the cursor value yourself. Just use the value from the last response's `nextCursor` property instead. * In some cases, the API may return a `nextCursor` when in fact there are no more items to show. So if you include a `cursor` parameter and get 404 (no results) back, that just means you've reached the end of the results. The limit Parameter [#the-limit-parameter] The `limit` parameter determines the max number of items to return in each request. It has a default value which can be overridden. * If you don't specify a `limit` parameter, then a limit of `10` will be used. * If you specify a `limit` above `300`, then you'll receive an error. * Otherwise, the limit applied is the smaller value between the `limit` parameter you supplied and the max-limit for the endpoint. * If you're making a request to the `/players` or `/teams` endpoints, the max-limit is `250` * If you're making a request to the `/events` endpoint, the max-limit varies from 25-100 depending on the query. Factors affecting this include: * If the query filters for specific fields (ex: `oddIDs`) * If the query is for upcoming or past events * If the query includes alt lines Example [#example] Let's take the following example, where we want to grab all unfinalized NBA events: ```js const allEvents = []; let nextCursor = null; let hasMore = true; while (hasMore) { try { const response = await axios.get("https://api.sportsgameodds.com/v2/events", { params: { leagueID: "NBA", finalized: false, limit: 100, cursor: nextCursor, }, headers: { "x-api-key": YOUR_API_KEY, }, }); allEvents.push(...response.data.data); nextCursor = response.data.nextCursor; hasMore = Boolean(nextCursor); } catch (error) { hasMore = false; } } console.log(`Found ${allEvents.length} events`); allEvents.forEach((event) => console.log(event.eventID)); return allEvents; ``` ```python import requests all_events = [] next_cursor = None has_more = True while has_more: try: response = requests.get( "https://api.sportsgameodds.com/v2/events", params={ "leagueID": "NBA", "finalized": "false", "limit": 100, "cursor": next_cursor }, headers={"x-api-key": YOUR_API_KEY} ) response.raise_for_status() data = response.json() all_events.extend(data.get("data", [])) next_cursor = data.get("nextCursor") has_more = next_cursor is not None except requests.RequestException: has_more = False print(f"Found {len(all_events)} events") for event in all_events: print(event["eventID"]) return all_events ``` ```ruby require "net/http" require "json" require "uri" def fetch_all_events(api_key) all_events = [] next_cursor = nil has_more = true while has_more begin uri = URI("https://api.sportsgameodds.com/v2/events") params = { leagueID: "NBA", finalized: false, limit: 100, cursor: next_cursor }.compact uri.query = URI.encode_www_form(params) req = Net::HTTP::Get.new(uri) req["x-api-key"] = api_key res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(req) end data = JSON.parse(res.body) all_events.concat(data["data"] || []) next_cursor = data["nextCursor"] has_more = !next_cursor.nil? && !next_cursor.empty? rescue StandardError has_more = false end end puts "Found #{all_events.length} events" all_events.each { |event| puts event["eventID"] } all_events end ``` ```php $allEvents = []; $nextCursor = null; $hasMore = true; while ($hasMore) { try { $params = [ 'leagueID' => 'NBA', 'finalized' => 'false', 'limit' => 100, ]; if ($nextCursor !== null) { $params['cursor'] = $nextCursor; } $ch = curl_init("https://api.sportsgameodds.com/v2/events?" . http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "x-api-key: " . YOUR_API_KEY ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { throw new Exception("HTTP Error: " . $httpCode); } $data = json_decode($response, true); $events = $data['data'] ?? []; $allEvents = array_merge($allEvents, $events); $nextCursor = $data['nextCursor'] ?? null; $hasMore = $nextCursor !== null; } catch (Exception $e) { $hasMore = false; } } echo "Found " . count($allEvents) . " events\n"; foreach ($allEvents as $event) { echo $event['eventID'] . "\n"; } return $allEvents; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.List; import com.google.gson.JsonObject; import com.google.gson.JsonParser; List allEvents = new ArrayList<>(); String nextCursor = null; boolean hasMore = true; HttpClient client = HttpClient.newHttpClient(); while (hasMore) { try { String url = String.format( "https://api.sportsgameodds.com/v2/events?leagueID=NBA&finalized=false&limit=100%s", nextCursor != null ? "&cursor=" + nextCursor : "" ); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", YOUR_API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); JsonObject data = JsonParser.parseString(response.body()).getAsJsonObject(); data.getAsJsonArray("data").forEach(event -> allEvents.add(event.getAsJsonObject()) ); nextCursor = data.has("nextCursor") && !data.get("nextCursor").isJsonNull() ? data.get("nextCursor").getAsString() : null; hasMore = nextCursor != null; } catch (Exception e) { hasMore = false; } } System.out.println("Found " + allEvents.size() + " events"); allEvents.forEach(event -> System.out.println(event.get("eventID").getAsString()) ); return allEvents; ``` --- # Teams Guide - Fetch by ID, League, or Sport URL: https://sportsgameodds.com/docs/guides/fetching-teams Teams Guide - Fetch by ID, League, or Sport [#teams-guide---fetch-by-id-league-or-sport] Overview [#overview] The Sports Game Odds API provides the ability to fetch a list of teams or a specific team's details. You can use a `sportID` or `leagueID` to get a list of associated teams and their details. You can also pass a `teamID` to just get a single team's details. Note that when specifying a `teamID`, it will still return as an array, just with a single object in it. Fetching by Team [#fetching-by-team] Let's take the following example, where we want to fetch the details of a specific NBA team: ```js await axios.get("/v2/teams", { params: { teamID: "LOS_ANGELES_LAKERS_NBA", }, }); ``` ```python import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_TOKEN' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body) ``` ```php $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: YOUR_TOKEN' ]); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import org.json.JSONObject; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_TOKEN"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); JSONObject data = new JSONObject(content.toString()); System.out.println(data.toString(2)); } catch (Exception e) { e.printStackTrace(); } } } ``` This will return a response that looks something like this: ```json { "success": true, "data": [ { "sportID": "BASKETBALL", "names": { "short": "LAL", "medium": "Lakers", "long": "Los Angeles Lakers" }, "leagueID": "NBA", "teamID": "LOS_ANGELES_LAKERS_NBA" } ] } ``` Fetching by league [#fetching-by-league] If you wanted to fetch a list of all the teams in the NBA, your request would look something like this: ```js await axios.get("/v2/teams", { params: { leagueID: "NBA", }, }); ``` ```python import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?leagueID=NBA', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/teams?leagueID=NBA') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_TOKEN' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body) ``` ```php $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/teams?leagueID=NBA'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: YOUR_TOKEN' ]); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import org.json.JSONObject; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/teams?leagueID=NBA"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_TOKEN"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); JSONObject data = new JSONObject(content.toString()); System.out.println(data.toString(2)); } catch (Exception e) { e.printStackTrace(); } } } ``` This will return a response that looks something like this: ```json { "nextCursor": "MILWAUKEE_BUCKS_NBA", "success": true, "data": [ { "sportID": "BASKETBALL", "names": { "short": "LAL", "medium": "Lakers", "long": "Los Angeles Lakers" }, "leagueID": "NBA", "teamID": "LAKERS_NBA", "colors": { "secondary": "#FFFFFF", "primaryContrast": "#000000", "secondaryContrast": "#552583", "primary": "#552583" } }, { "sportID": "BASKETBALL", "names": { "short": "BOS", "medium": "Celtics", "long": "Boston Celtics" }, "leagueID": "NBA", "teamID": "CELTICS_NBA", "colors": { "secondary": "#FFFFFF", "primaryContrast": "#000000", "secondaryContrast": "#007A33", "primary": "#007A33" } }, // ... // Up to 30 objects may be returned in this object. If there are more available // then you'll see a nextCursor property you can use to fetch the next // page of related objects. ] } ``` Fetching by sport [#fetching-by-sport] If you wanted to fetch a list of all basketball teams across all of our supported leagues, your request would look something like this: ```js await axios.get("/v2/teams", { params: { sportID: "BASKETBALL", }, }); ``` ```python import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_TOKEN' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body) ``` ```php $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: YOUR_TOKEN' ]); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import org.json.JSONObject; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_TOKEN"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); JSONObject data = new JSONObject(content.toString()); System.out.println(data.toString(2)); } catch (Exception e) { e.printStackTrace(); } } } ``` This will return a response that looks something like this: ```json { "nextCursor": "BELMONT_NCAAB", "success": true, "data": [ { "sportID": "BASKETBALL", "names": { "short": "LAL", "medium": "Lakers", "long": "Los Angeles Lakers" }, "leagueID": "NBA", "teamID": "LAKERS_NBA" }, { "sportID": "BASKETBALL", "names": { "short": "BOS", "medium": "Celtics", "long": "Boston Celtics" }, "leagueID": "NBA", "teamID": "CELTICS_NBA" }, { "sportID": "BASKETBALL", "names": { "short": "GSW", "medium": "Warriors", "long": "Golden State Warriors" }, "leagueID": "NBA", "teamID": "WARRIORS_NBA" }, // ... // Up to 30 objects may be returned in this object. If there are more available // then you'll see a nextCursor property you can use to fetch the next // page of related objects. ] } ``` --- # Handling Odds - Parse Results and Grade Bets URL: https://sportsgameodds.com/docs/guides/handling-odds Handling Odds - Parse Results and Grade Bets [#handling-odds---parse-results-and-grade-bets] Overview [#overview] The Sports Game Odds API comes complete with odds and result data for every event. This guide will show you how you can easily fetch and parse the odds for a specific event or group of events! Example [#example] In our previous example you saw how you would fetch upcoming NBA events from the API using our cursor pattern, let's take that a step further! Now, assuming the NBA week has passed, we will fetch all finalized NBA events from that week, then parse the odds results for each event, so we can grade them. ```js let nextCursor = null; let eventData = []; do { try { const response = await axios.get('/v2/events', { params: { leagueID: 'NBA', startsAfter: '2024-04-01', startsBefore: '2024-04-08', finalized: true, cursor: nextCursor } }); const data = response.data; eventData = eventData.concat(data.data); nextCursor = data?.nextCursor; } catch (error) { console.error('Error fetching events:', error); break; } } while (nextCursor); // Now that we have the events, let's parse the odd results! // Based on the bet type, compare score to the odds and grade the odds, for this example assume the odds are over/under eventData.forEach((event) => { const odds = event.odds; Object.values(odds).forEach((oddObject) => { const oddID = oddObject.oddID; const score = parseFloat(oddObject.score); const closeOverUnder = parseFloat(oddObject.closeOverUnder); if (score > closeOverUnder) console.log(`Odd ID: ${oddID} - Over Wins`); else if (score === closeOverUnder) console.log(`Odd ID: ${oddID} - Push`); else console.log(`Odd ID: ${oddID} - Under Wins`); }); }); ``` --- # Real-Time Streaming API (WebSocket) URL: https://sportsgameodds.com/docs/guides/realtime-streaming-api Real-Time Streaming API (WebSocket) [#real-time-streaming-api-websocket] This API endpoint is only available to **AllStar** and **custom plan** subscribers. It is not included with basic subscription tiers. [Contact support](mailto:api@sportsgameodds.com) to get access. This streaming API is currently in **beta**. API call patterns, response formats, and functionality may change. Our Streaming API provides real-time updates for Event objects through WebSocket connections. Instead of polling our REST endpoints, you can maintain a persistent connection to receive instant notifications when events change. This is ideal for applications that need immediate updates with minimal delay. We use [Pusher Protocol](https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/) for WebSocket communication. While you can connect using any WebSocket library, we strongly recommend using any [Pusher Client Library](https://pusher.com/docs/channels/library_auth_reference/pusher-client-libraries) (ex: [Javascript](https://github.com/pusher/pusher-js), [Python](https://github.com/pusher/pusher-http-python)) How It Works [#how-it-works] The streaming process involves two steps: 1. **Get Connection Details**: Make a request to `/v2/stream/events` to receive: * WebSocket authentication credentials * WebSocket URL/channel info * Initial snapshot of current data 2. **Connect and Stream**: Use the provided details to connect via Pusher (or another WebSocket library) and receive real-time `eventID` notifications for changed events Your API key will have limits on concurrent streams. Available Feeds [#available-feeds] Subscribe to different feeds using the `feed` query parameter: | Feed | Description | Required Parameters | | ----------------- | ----------------------------------------------------------- | ------------------- | | `events:live` | All events currently in progress (started but not finished) | None | | `events:upcoming` | Upcoming events with available odds for a specific league | `leagueID` | | `events:byid` | Updates for a single specific event | `eventID` | The number of supported feeds will increase over time. Please reach out if you have a use case which can't be covered by these feeds. Quick Start Example [#quick-start-example] Here's the minimal code to connect to live events: ```js const axios = require("axios"); const Pusher = require("pusher-js"); const STREAM_FEED = "events:live"; // ex: events:upcoming, events:byid, events:live const API_BASE_URL = "https://api.sportsgameodds.com/v2"; const API_KEY = "YOUR API KEY"; const run = async () => { // Initialize a data structure where we'll save the event data const EVENTS = new Map(); // Call this endpoint to get initial data and connection parameters const streamInfo = await axios.get(`${API_BASE_URL}/stream/events`, { headers: { "x-api-key": API_KEY }, params: { feed: STREAM_FEED } }).then((r) => r?.data); // Seed initial data streamInfo.data.forEach((event) => EVENTS.set(event?.eventID, event)); // Connect to WebSocket server const pusher = new Pusher(streamInfo.pusherKey, streamInfo.pusherOptions); pusher.subscribe(streamInfo.channel).bind("data", async (changedEvents) => { // Get the eventIDs that changed const eventIDs = changedEvents.map(({ eventID }) => eventID).join(","); // Get the full event data for the changed events const eventDataResponse = await axios.get(`${API_BASE_URL}/events`, { headers: { "x-api-key": API_KEY }, params: { eventIDs } }).then((r) => r?.data); // Update our data with the full event data eventDataResponse.data.forEach((newEventData) => EVENTS.set(newEventData?.eventID, newEventData)); }); // Use pusher.disconnect to disconnect from the WebSocket server process.on("SIGINT", pusher.disconnect); }; run(); ``` ```python import requests import pusherclient import json API_KEY = 'YOUR_API_KEY' API_BASE_URL = 'https://api.sportsgameodds.com/v2' def handle_update(data): try: changed_events = json.loads(data) event_ids = ','.join([e['eventID'] for e in changed_events]) # Fetch updated event data response = requests.get( f"{API_BASE_URL}/events", headers={'x-api-key': API_KEY}, params={'eventIDs': event_ids} ) for event in response.json()['data']: print(f"Updated: {event['eventID']}") except Exception as e: print(f"Error processing update: {e}") def connect_to_live_events(): # Get connection details response = requests.get( f"{API_BASE_URL}/stream/events", headers={'x-api-key': API_KEY}, params={'feed': 'events:live'} ) stream_info = response.json() print(f"Connected with {len(stream_info['data'])} initial events") # Connect to Pusher (Note: private channels require custom auth in Python) pusher_key = stream_info['pusherKey'] channel_name = stream_info['channel'] pusher = pusherclient.Pusher(pusher_key) channel = pusher.subscribe(channel_name) channel.bind('data', handle_update) pusher.connection.bind('pusher:connection_established', lambda data: print("Connected to stream")) if __name__ == "__main__": connect_to_live_events() ``` ```ruby require 'net/http' require 'json' require 'pusher-client' require 'uri' STREAM_FEED = "events:live" # ex: events:upcoming, events:byid, events:live API_BASE_URL = "https://api.sportsgameodds.com/v2" API_KEY = "API_KEY_GOES_HERE" def convert_ruby_options(standard_options, api_key) ruby_client_options = {} ruby_client_options[:ws_host] = standard_options['wsHost'] if standard_options['wsHost'] ruby_client_options[:ws_port] = standard_options['wsPort'] if standard_options['wsPort'] ruby_client_options[:wss_port] = standard_options['wssPort'] if standard_options['wssPort'] ruby_client_options[:ws_path] = standard_options['wsPath'] if standard_options['wsPath'] ruby_client_options[:ws_host] = "ws-#{standard_options['cluster']}.pusher.com" if standard_options['cluster'] && !ruby_client_options[:ws_host] ruby_client_options[:secure] = true if (standard_options.dig('channelAuthorization', 'endpoint')) ruby_client_options[:auth_method] = ->(socket_id, channel) { headers = (standard_options.dig('channelAuthorization', 'headers') || {}).merge('Content-Type' => 'application/json') params = (standard_options.dig('channelAuthorization', 'params') || {}).merge('socket_id' => socket_id, 'channel_name' => channel.name) res = Net::HTTP.post(URI(standard_options.dig('channelAuthorization', 'endpoint')), params.to_json, headers) res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body)['auth'] : raise("Auth failed: HTTP #{res.code} - #{res.body}") } end ruby_client_options end def api_get(path, params = {}) uri = URI("#{API_BASE_URL}#{path}") uri.query = URI.encode_www_form(params) unless params.empty? res = Net::HTTP.get_response(uri, {'x-api-key' => API_KEY}) raise "HTTP #{res.code}: #{res.message}" unless res.is_a?(Net::HTTPSuccess) JSON.parse(res.body) end def run events = {} # Get initial data and connection parameters stream_info = api_get("/stream/events", feed: STREAM_FEED) stream_info['data'].each { |e| events[e['eventID']] = e } # Convert the standard options to Ruby SDK options connect_options = convert_ruby_options(stream_info['pusherOptions'], API_KEY) # Initialize the client client = PusherClient::Socket.new(stream_info['pusherKey'], connect_options) # Set up other event handlers as needed client.bind('pusher:connection_established') { puts "Connected" } client.bind('pusher:subscription_succeeded') { |_| puts "Subscribed to: #{stream_info['channel']}" } client.bind('pusher:subscription_error') { |data| puts "Error: #{data}" } # Subscribe to and handle data events in the channel channel = client.subscribe(stream_info['channel'], stream_info['user']) channel.bind('data') do |json| changed_events = JSON.parse(json) event_ids = changed_events.map { |e| e['eventID'] }.join(', ') puts "Changed Events: #{event_ids}" # Fetch and update full event data api_get("/events", eventIDs: event_ids)['data'].each { |e| events[e['eventID']] = e } rescue => e puts "Error processing data event: #{e.message}" end # Print initial event IDs puts "EVENTS: #{events.keys.join(',')}" # Graceful shutdown trap('INT') { puts "\nDisconnecting..."; client.disconnect; exit } # Connect to the WebSocket server client.connect # For async usage, you can instead call client.connect(true) after initializing the client end private # Runs the script run ``` API Response Format [#api-response-format] The `/v2/stream/events` endpoint returns: ```json { "success": true, "data": [ , // ... more events ], "pusherKey": , "pusherOptions": , "channel": } ``` Update Message Format [#update-message-format] Real-time updates contain only the `eventID` of changed events: ```json [ { "eventID": }, ] ``` You then fetch full event details using the `/v2/events` endpoint with the `eventIDs` parameter. Complete Example: NFL Game Tracker [#complete-example-nfl-game-tracker] This example demonstrates a production-ready implementation for tracking NFL games: ```js const axios = require("axios"); const Pusher = require("pusher-js"); const API_KEY = "PUT YOUR API KEY HERE"; const API_BASE_URL = "https://api.sportsgameodds.com/v2"; const LEAGUE_ID = "NFL"; // State variables let events = new Map(); let pusher = null; let channel = null; let connectionState = "disconnected"; let heartbeatInterval = null; const getEventTitle = (event) => { const awayTeam = event.teams?.away?.names?.medium || event.teams?.away?.names?.long || event.teams?.away?.names?.short; const homeTeam = event.teams?.home?.names?.medium || event.teams?.home?.names?.long || event.teams?.home?.names?.short; const gameTime = event.status.startsAt.toLocaleString(); return `${awayTeam} vs ${homeTeam} @ ${gameTime} (${event.eventID})`; }; const startMonitoring = () => { // Heartbeat heartbeatInterval = setInterval(() => { console.log(`💓 Heartbeat...connection: ${connectionState}, events tracked: ${events.size}`); }, 10000); // Connection state changes pusher.connection.bind("state_change", (states) => { connectionState = states.current; console.log(`🔌 Connection state: ${states.previous} → ${states.current}`); }); // Connection errors pusher.connection.bind("error", (error) => { console.error("🚨 Connection error:", error); }); // Connection established pusher.connection.bind("connected", () => { console.log("🎉 Connection established successfully"); }); // Connection disconnected pusher.connection.bind("disconnected", () => { console.log("👋 Connection disconnected"); }); // Failed connection pusher.connection.bind("failed", () => { console.log("💥 Connection failed permanently"); }); }; const connect = async () => { console.log(`🔄 Connecting upcoming events for: ${LEAGUE_ID}...`); connectionState = "connecting"; try { // Get stream metadata console.log("📡 Fetching stream configuration..."); const response = await axios.get(`${API_BASE_URL}/stream/events`, { headers: { "x-api-key": API_KEY }, params: { feed: "events:upcoming", leagueID: LEAGUE_ID, }, }); console.log("✅ Stream configuration received"); const { data: initialEvents, pusherKey, pusherOptions, channel: channelName } = response.data; console.log(`📊 Stream config...channel: ${channelName}, pusherKey: ${pusherKey}, initialEvents: ${initialEvents.length}`); initialEvents.forEach((event) => { events.set(event.eventID, event); console.log(`⏳ Initial Event: ${getEventTitle(event)}`); }); console.log("🔌 Initializing WebSocket connection..."); pusher = new Pusher(pusherKey, pusherOptions); startMonitoring(); // Subscribe to channel with error handling console.log(`📺 Subscribing to channel: ${channelName}`); channel = pusher.subscribe(channelName); channel.bind("pusher:subscription_succeeded", () => { console.log(`✅ Successfully subscribed to channel: ${channelName}`); connectionState = "subscribed"; }); channel.bind("data", async (changedEvents) => { console.log(`🔔 Received change notification for ${changedEvents.length} event(s)`); const eventIDs = changedEvents.map((e) => e.eventID).join(","); if (!eventIDs) return; console.log(`🔍 Fetching full data for events: ${eventIDs}`); const response = await axios .get(`${API_BASE_URL}/events`, { headers: { "x-api-key": API_KEY }, params: { eventIDs }, }) .catch(({ data }) => data); if (!response?.data?.length) return; console.log(`📦 Received ${response.data.length} updated events`); response.data.forEach((current) => { const prev = events.get(current.eventID); if (!prev) { console.log(`🆕 New Event: ${getEventTitle(current)}`); return; } else if (!prev.status.started && current.status.started) { console.log(`🏈 Event started: ${getEventTitle(current)}`); return; } else { console.log(`🔄 Event updated: ${getEventTitle(current)}`); } events.set(current.eventID, current); }); }); console.log("🎯 Setup complete, waiting for events..."); } catch (error) { console.log(error); console.error("❌ Failed to connect:", error.message); connectionState = "failed"; } }; const disconnect = () => { console.log("🔌 Disconnecting from stream..."); if (heartbeatInterval) clearInterval(heartbeatInterval); if (channel?.name) pusher.unsubscribe(channel.name); if (pusher) pusher.disconnect(); }; console.log(`🚀 Starting upcoming event tracker for: ${LEAGUE_ID}...`); connect(); process.on("SIGINT", () => { console.log("\n🛑 Shutdown signal received..."); disconnect(); }); ``` Feed-Specific Examples [#feed-specific-examples] Live Events [#live-events] Monitor all currently live games across all sports: ```js axios.get("https://api.sportsgameodds.com/v2/stream/events", { headers: { 'x-api-key': API_KEY }, params: { feed: 'events:live' } }); ``` Upcoming Events by League [#upcoming-events-by-league] Monitor upcoming games for a specific league: ```js axios.get("https://api.sportsgameodds.com/v2/stream/events", { headers: { 'x-api-key': API_KEY }, params: { feed: 'events:upcoming', leagueID: 'NFL' } }); ``` Single Event Tracking [#single-event-tracking] Track updates for one specific event: ```js axios.get("https://api.sportsgameodds.com/v2/stream/events", { headers: { 'x-api-key': API_KEY }, params: { feed: 'events:byid', eventID: 'DENVER_NUGGETS_VS_MIAMI_HEAT_2024-05-20T00:30:00Z_NBA' } }); ``` Connection Management [#connection-management] You'll want to be mindful of the connection state and handle errors and disconnections properly in order to maintain a reliable connection. ```js const pusher = new Pusher(pusherKey, pusherOptions); // Monitor connection state pusher.connection.bind('state_change', (states) => { console.log('Connection state:', states.current); // States: connecting, connected, unavailable, failed, disconnected }); // Handle connection errors pusher.connection.bind('error', (error) => { console.error('Connection error:', error); // Implement reconnection logic if needed }); // Clean disconnect pusher.disconnect(); ``` Troubleshooting [#troubleshooting] **Connection Issues** * Verify your API key has streaming permissions * Check that you haven't exceeded your concurrent stream limit * Ensure proper Pusher client library installation **No Updates Received** * Confirm you're subscribed to the correct channel * Verify the feed parameters match available events * Check network connectivity and firewall settings **High Memory Usage** * Implement data cleanup for old events * Store only necessary event properties * Use efficient data structures (Map vs Object) For additional support, contact our support team with your API key and specific error messages. --- # Optimize Response Speed and Latency URL: https://sportsgameodds.com/docs/guides/response-speed Optimize Response Speed and Latency [#optimize-response-speed-and-latency] We're in the process of adding additional request params which are designed to allow you to more efficiently fetch only the data you need. We also plan on releasing a GraphQL API in the future. Check back here for updates. We're committed to finding balance between making as much data available to you as possible while also ensuring that you can fetch that data quickly and efficiently. This guide is designed to help you understand how to optimize your requests to reduce response times/latency. Use the oddIDs parameter [#use-the-oddids-parameter] * The most common cause of high response times is fetching a large number of odds at once. This can be especially problematic when fetching odds for a large number of Events. * To reduce this, you can use the `oddIDs` parameter to fetch only the odds you need. * The `oddIDs` parameter can be included in the `/events` * It accepts a comma-separated list of oddID values (See the [Markets](/v2/data-types/markets) guide for a list of supported oddID values) * You can also set the parameter `includeOpposingOddIDs` to `true` to also include the opposing side of all oddIDs provided * You can also replace the playerID portion of any oddID with `PLAYER_ID` to fetch that oddID across all players * Example * Consider the oddID `batting_strikeouts-CODY_BELLINGER_1_MLB-game-ou-under` which represents the under odds for Cody Bellinger's strikeouts in a game * If you wanted to fetch all player strikeouts odds for this game you would set the following params * `oddIDs=batting_strikeouts-PLAYER_ID-game-ou-under` * `includeOpposingOddIDs=true` * That would give you both over and under odds for all player strikeouts odds markets for all Events/Odds returned --- # AI-Assisted Development - MCP Server and Context URL: https://sportsgameodds.com/docs/info/ai-vibe-coding AI-Assisted Development - MCP Server and Context [#ai-assisted-development---mcp-server-and-context] AI tools can significantly speed up your development process. However, these tools need proper context in order to generate accurate, production-ready code. The information below is designed to help you do just that. Copy-Paste Context [#copy-paste-context] **We highly recommend you either paste the [AI Context](#ai-context) below into your prompt or add it to an applicable context/rules file.** AI-Friendly Docs [#ai-friendly-docs] Indexed Documentation [#indexed-documentation] [View llms.txt](/llms.txt) — Includes URLs and descriptions of each of our documentation pages. AI tools can use this to identify specific documentation pages to fetch/read when needed. Length: \~2k tokens Full Documentation [#full-documentation] [View llms-full.txt](/llms-full.txt) — Includes our entire documentation in a single file. This is helpful for AI tools that have a large context window. Length: \~85k tokens OpenAPI Specification [#openapi-specification] [View OpenAPI Spec](/SportsGameOdds_OpenAPI_Spec.json) — A full machine-readable API definition with exact schemas for all endpoints/requests and responses. Length: \~600k tokens MCP Server [#mcp-server] Our [MCP](https://modelcontextprotocol.io/) server enables AI tools to directly make calls to the API. You can find the server here: [`sports-odds-api-mcp`](https://www.npmjs.com/package/sports-odds-api-mcp). It provides the following resources/tools: * **List endpoints** - Discover available API operations * **Fetch live data** - Get events, odds, teams, players, leagues, etc. * **Search documentation** - Find relevant info from the documentation * **Monitor usage** - Check your API quota and rate limits * **Stream events** - Get notified when Events change (AllStar plan only) Below are installation instructions. Be sure to replace `your-api-key-here` with your actual API key. Claude Code [#claude-code] ```bash code --add-mcp '{"name":"sports-game-odds","command":"npx","args":["-y","sports-odds-api-mcp@latest"],"env":{"SPORTS_ODDS_API_KEY_HEADER":"your-api-key-here"}}' ``` ```json { "name": "sports-game-odds", "command": "npx", "args": ["-y", "sports-odds-api-mcp@latest"], "env": { "SPORTS_ODDS_API_KEY_HEADER": "your-api-key-here" } } ``` AI Context [#ai-context] Paste the following into your prompt or add it to an applicable rules file: ``` # SportsGameOdds API Reference Real-time and historical sports and odds data. ## Primary Documentation Resources If your environment can fetch external URLs, use these as your primary sources of truth. **Do not guess or make up information.** | Resource | URL | Use Case | | ------------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------ | | Documentation Index (~2k tokens) | https://sportsgameodds.com/docs/llms.txt | Quick overview of all documentation pages with descriptions | | Full Documentation (~85k tokens) | https://sportsgameodds.com/docs/llms-full.txt | Detailed explanations of fields, parameters, and examples | | OpenAPI Specification (~600k tokens) | https://sportsgameodds.com/docs/SportsGameOdds_OpenAPI_Spec.json | Exact request/response schemas, parameter definitions, example responses | > **Note:** If you cannot access URLs directly, ask the user to paste in the relevant resource instead of guessing. ## Authentication - **API Key Required:** Users can obtain one at https://sportsgameodds.com/pricing - Free tier available - Paid tiers include free trials - API key is emailed after signup - **Security:** Treat the API key as secret. Never invent an API key. - **Usage:** Include the API key in all requests using one of these methods: - Query parameter: `?apiKey=API_KEY` - Header: `x-api-key: API_KEY` ## Response Format - All responses are JSON - Main response data is returned in the `data` field ## Endpoints ### Events Endpoint (Most Common) **URL:** `GET https://api.sportsgameodds.com/v2/events` **Full Documentation:** https://sportsgameodds.com/docs/endpoints/getEvents #### Common Query Parameters | Parameter | Example | Description | | ----------------- | -------------------------- | ---------------------------------------------------- | | `oddsAvailable` | `true` | Only return live/upcoming events with odds available | | `leagueID` | `NBA,NFL,MLB` | Filter by leagues (comma-separated) | | `oddID` | `points-home-game-ml-home` | Filter by odds markets (comma-separated) | | `includeAltLines` | `true` | Include alternate spread/over/under lines | | `cursor` | `` | Pagination cursor for next page | | `limit` | `10` | Max events to return (default: 10, max: variable) | #### Event Object Key Fields | Field | Type | Description | | -------------------- | ------- | ------------------------------------------- | | `eventID` | string | Unique identifier for the event | | `sportID` | string | ID of the sport | | `leagueID` | string | ID of the league | | `teams.home.teamID` | string | ID of the home team (if applicable) | | `teams.away.teamID` | string | ID of the away team (if applicable) | | `status.startsAt` | date | Start time of the event | | `status.started` | boolean | Whether the event has started | | `status.ended` | boolean | Whether the event has ended | | `status.finalized` | boolean | Whether the event's data has been finalized | | `players.` | object | Information about a participating player | | `odds` | object | Odds data for the event | > **Tip:** When the user has a specific use case (e.g., "get NFL games today with moneyline odds"), help them choose appropriate filters and construct the full request URL or HTTP client code. ## OddID Format Each `oddID` uniquely identifies a specific side/outcome on a betting market. **Format:** `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}` **Examples:** | oddID | Description | | ----------------------------------------- | --------------------------------------------------- | | `points-home-game-ml-home` | Moneyline bet on the home team to win the full game | | `points-away-1h-sp-away` | Spread bet on the away team to win the first half | | `points-all-game-ou-over` | Over bet on total points for the full game | | `assists-LEBRON_JAMES_1_NBA-game-ou-over` | Over bet on LeBron James assists for the full game | ## Bookmaker Odds Structure **Path:** `odds..byBookmaker.` | Field | Type | Description | | ----------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `odds` | string | Current odds in American format | | `available` | boolean | Whether this market is currently available | | `spread` | string (optional) | Current spread/line (when `betTypeID === "sp"`) | | `overUnder` | string (optional) | Current over/under line (when `betTypeID === "ou"`) | | `deeplink` | string (optional) | Direct URL to the market on bookmaker's website | | `altLines` | array (optional) | Alternate lines (only if `includeAltLines=true`). Each object may contain: `odds`, `available`, `spread`, `overUnder`, `lastUpdatedAt` | > **Note:** Use the docs and/or OpenAPI spec to confirm additional or optional fields. ## Reference: Common Identifiers ### sportID | ID | Sport | | ------------ | ---------- | | `BASKETBALL` | Basketball | | `FOOTBALL` | Football | | `SOCCER` | Soccer | | `HOCKEY` | Hockey | | `TENNIS` | Tennis | | `GOLF` | Golf | | `BASEBALL` | Baseball | ### leagueID | ID | League | | ----------------------- | ------------------------ | | `NBA` | NBA | | `NFL` | NFL | | `MLB` | MLB | | `NHL` | NHL | | `EPL` | Premier League | | `UEFA_CHAMPIONS_LEAGUE` | Champions League | | `NCAAB` | Men's College Basketball | | `NCAAF` | Men's College Football | ### bookmakerID | ID | Bookmaker | | ------------ | ---------- | | `draftkings` | DraftKings | | `fanduel` | FanDuel | | `bet365` | Bet365 | | `circa` | Circa | | `caesars` | Caesars | | `betmgm` | BetMGM | | `betonline` | BetOnline | | `prizepicks` | PrizePicks | | `pinnacle` | Pinnacle | ### betTypeID & sideID | betTypeID | Description | Valid sideIDs | | --------- | --------------- | ------------------------------------------------------------ | | `ml` | Moneyline | `home`, `away` | | `sp` | Spread | `home`, `away` | | `ou` | Over/Under | `over`, `under` | | `eo` | Even/Odd | `even`, `odd` | | `yn` | Yes/No | `yes`, `no` | | `ml3way` | 3-Way Moneyline | `home`, `away`, `draw`, `away+draw`, `home+draw`, `not_draw` | ### periodID | ID | Period | | ------ | -------------- | | `game` | Full Game | | `1h` | First Half | | `2h` | Second Half | | `1q` | First Quarter | | `2q` | Second Quarter | | `3q` | Third Quarter | | `4q` | Fourth Quarter | ### statID (varies by sport) | ID | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `points` | Stats that determine the winner (points in Baseball/Football, goals in Soccer/Hockey, sets in Tennis, strokes against par in Golf, fight winner in MMA/Boxing) | | `rebounds` | Rebounds | | `assists` | Assists | | `steals` | Steals | | `receptions` | Receptions | | `passing_yards` | Passing yards | | `rushing_yards` | Rushing yards | | `receiving_yards` | Receiving yards | ``` --- # Best Practices and Common Mistakes URL: https://sportsgameodds.com/docs/info/best-practices Best Practices and Common Mistakes [#best-practices-and-common-mistakes] This guide covers our recommended patterns as well as some common mistakes to avoid when working with the SportsGameOdds API. Patterns to Follow [#patterns-to-follow] Set up a server-side process to sync data from the API into your database [#set-up-a-server-side-process-to-sync-data-from-the-api-into-your-database] * This is the most secure and scalable way to ingest API data * A simple cron job can handle this well. * Since you're syncing the data, you're in full control of how up-to-date your data is. * If your app starts doing well and you get 10x more traffic, your API calls remain consistent and predictable. Calculate your expected usage [#calculate-your-expected-usage] * Use these factors to determine how many requests/objects you'll need: * How many leagues you're tracking data for * How frequently you want to refresh your data * How many games you want track/update at a time for each league * All games? Then determine how many that will roughly be at each time of year based on active sports seasons. * All games in the next 24 hours? 48 hours? week? month? If so, what do those numbers look like for each tracked league? * Only games with active odds markets? If so, then what do the average numbers look like for each tracked league? Specify oddIDs in /events requests when you only need specific odds markets [#specify-oddids-in-events-requests-when-you-only-need-specific-odds-markets] * Including this parameter can significantly reduce the respoonse payload size and improve response times. Implement retry logic on 500-level errors [#implement-retry-logic-on-500-level-errors] * If you ever receive a 500-level error, you should wait a short period, then retry a single time. * In most cases, you'll get a successful response on the second attempt. * If you don't then stop retrying and log the error. Use query params to filter data [#use-query-params-to-filter-data] * Filtering at the API level means less irrelevant/unused data is returned to you. * This improves response times and reduces your costs. Use the limit and cursor parameters together [#use-the-limit-and-cursor-parameters-together] * Before you start using the `cursor` parameter, increase the `limit` parameter so you get more results per request. Keep your API key secure [#keep-your-api-key-secure] * Never expose it in your frontend code. * Never commit it to version control. Monitor your usage [#monitor-your-usage] * Check the `/account/usage` endpoint to see how you're doing against your rate limits Handle missing fields defensively [#handle-missing-fields-defensively] * Some fields may not always be available across all of our data. You should handle such cases gracefully. * Typically, critical fields will always have a value (ex: Events will always have an `eventID`, `sportID`, `leagueID`), but less critical fields won't. * For example, use `Event.teams.home.names.long || Event.teams.home.names.medium || Event.teams.home.names.short` rather than assuming `Event.teams.home.names.long` is always available. Vary Your Polling/Caching Intervals [#vary-your-pollingcaching-intervals] * Odds change very infrequently when a game is far in the future * Many markets aren't even offered until a game is less than 24-48 hours away * You can save on API calls polling less frequently for games in the far future and more frequently for games in the near future. Learn the oddID Structure [#learn-the-oddid-structure] * Knowing this structure and what the individual values mean will make working with the API much faster and easier * `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}-{bookmakerID}` Anti-Patterns to Avoid [#anti-patterns-to-avoid] Making API Requests from a Frontend/Browser [#making-api-requests-from-a-frontendbrowser] * Frontend code (ex: React, Vue, etc.) should never make API requests directly. * This exposes your API key to the public. * In general, to avoid this you have two options: * Set up a server-side proxy that makes the API requests and returns the data to the frontend. * Set up a server-side process to sync data from the API into your database. Query your database from your frontend. Polling on an Interval on the Free/Amateur plan [#polling-on-an-interval-on-the-freeamateur-plan] * If you set up a system that makes a number of API requests continuously on an interval, you'll quickly hit your limits on the Amateur tier * We recomend only running your API requests manually and upgrading to a higher-limit plan when you're ready to set something like this up. Polling Too Frequently [#polling-too-frequently] * Based on your plan, your data may not update more than every 30 seconds to 1 minute * Polling more frequently than this wastes your rate limit quota Always Polling All Upcoming Games [#always-polling-all-upcoming-games] * Instead, cache data for longer when a game is happening far in the future * Such games likely won't have frequent odds movements * This can help you save on API calls and reduce your costs Not Considering Response Codes [#not-considering-response-codes] * For example, if you get a 429 (Rate Limit Exceeded) error, you should wait before retrying * Not waiting is just going to cause you to burn through your rate limit quota faster * You'll also want to consider whether you've hit a minute-level, month-level, or other rate limit. Use the `/account/usage` endpoint to check your usage. Not Considering Error Messages [#not-considering-error-messages] * Whenever you get an error response, there will be an `error` field at the top-level of the response body * This field will tell you what went wrong and why * In most cases, you should log this info and use it to help debug any problems you're encountering Always Including includeAltLines=true in /events requests [#always-including-includealtlinestrue-in-events-requests] * Don't include alt lines unless you actually need them * They can significantly increase the response payload size and slow down your response times Using startsAfter/startsBefore when unneeded [#using-startsafterstartsbefore-when-unneeded] * The startsAfter and startsBefore parameters can impede query performance in some cases * Performance suffers the most when it's combined with other less-performant parameters such as: * `playerID`, `bookmakerID`, `teamID`, `includeAltLines` * Try to re-evaluate whether you can accomplish the same thing without these parameters (sometimes you can, sometimes you can't) Using too many query parameters [#using-too-many-query-parameters] * Queries are best optimized for requests that use 1-3 parameters (excluding `limit`, `cursor`, `apiKey`) * Using more than this can make your queries slower in some cases Filtering Results on the Client [#filtering-results-on-the-client] * Our API can return very large payloads. * Therefore, your goal should be to filter as much as possible at the API level and not in your code * That way, you don't have to download and parse unnecessary data. * This is best accomplished with query parameters Using Wrong/Invalid Query Parameters [#using-wronginvalid-query-parameters] * See our [Reference Docs](/docs/reference/) for more information on the valid query parameters for each endpoint Not Monitoring Usage [#not-monitoring-usage] * You want to keep track of your usage in order to optimize your usage patterns and avoid hitting your rate limits. * Use the `/account/usage` endpoint to check your usage. --- # Consensus Odds - Fair and Book Calculations URL: https://sportsgameodds.com/docs/info/consensus-odds Consensus Odds - Fair and Book Calculations [#consensus-odds---fair-and-book-calculations] "Lines" [#lines] * The word "line" is used in this guide to refer to the spread or over-under value associated with a given set of odds * For bet types without a line (ex: moneyline or yes/no bets), all odds are treated as if they have a line of 0 * Both sides of the bet will have mirrored line values * ex: On a spread bet, if the fairSpread for the home side is 3, then the fairSpread for the away side will be -3 * ex: On an over-under bet, if the fairOverUnder for the over side is 5, then the fairOverUnder for the under side will also be 5 Fair Odds [#fair-odds] Key Fields [#key-fields] * `fairSpread` or `fairOverUnder`: The most fair spread or over/under line (if applicable) * `fairOdds`: The most fair odds for the given bet (associated with the line if applicable) * `fairOddsAvailable`: Indicates if sufficient data was available to calculate fairOdds (and fairSpread/fairOverUnder) Summary [#summary] We group odds by line, perform linear regression to find the most balanced line, calculate median odds across bookmakers for that line, and remove the juice to determine fair odds. Calculation Process [#calculation-process] 1. **Group odds data** for each Event + oddID by `bookmakerID` + `line` 2. **Include related bet types** into the groupings if possible * ex: moneyline bets are spread bets with a line of 0, so moneyline odds are included as such in spread calculations) 3. **Get the latest odds data** from each group in cases where we have multiple values 4. **Combine these values** into a single dataset * We first attempt to do this only with odds that are available/open for betting at the given bookmaker * If that doesn't yield sufficient data, then all values will be considered (and `fairOddsAvailable` will be set to false) 5. **Perform linear regression** to estimate the line with the most even odds 6. **Choose the fair line** based on: * Separate regressions for each side of the bet determine the line's upper and lower bounds for the fair line * Regression on the entire dataset gives us a target line value * Only positive lines are considered for over-unders and non-zero lines are considered for spreads * Line closest to target value with most data points is chosen 7. **Calculate median odds** for each side across all bookmakers for the chosen line * If no bookmakers offer odds at the fair line, then we use the odds returned by our regression calculation instead 8. **Remove the juice** to get the fair odds value by: * Calculating implied probability for each side * Recalculating odds based on the combined implied probabilities of both sides Book Odds [#book-odds] Key Fields [#key-fields-1] * `bookSpread` or `bookOverUnder`: Consensus/average spread or over/under line across bookmakers * `bookOdds`: Consensus/average odds for the associated line * `bookOddsAvailable`: Indicates if odds are available/open at at least one bookmaker Summary [#summary-1] We identify the main line for each bookmaker, select the most common main line across all bookmakers, and calculate the median odds across bookmakers for that line. Full Calculation Process [#full-calculation-process] 1. **Group odds data** for each Event + oddID by `bookmakerID` + `line` 2. **Identify the main line** for each group. * If a bookmaker doesn't distinguish between main and alt lines (and we can't easily infer that based on data attributes which can vary by bookmaker), then... * We identify the main line used most frequently by other bookmakers * And identify this bookmaker's main line as the line closest to that one 3. **Get the latest odds data** from each group in cases where we have multiple values 4. **Combine these values** into a single dataset * We first attempt to do this only with odds that are available/open for betting at the given bookmaker * If that doesn't yield sufficient data, then all values will be considered 5. **Select the consensus book line** by applying the following criteria: * The line which has the most data points (ie: the largest number of bookmakers have identified this as their main line) * If two have the same number of data points, then the line closest to the consensus fair line is selected 6. **Calculate consensus book odds** as the median odds across all bookmakers for the selected line --- # Error Codes and Troubleshooting URL: https://sportsgameodds.com/docs/info/errors Error Codes and Troubleshooting [#error-codes-and-troubleshooting] All API errors return an applicable HTTP status code (400-level or 500-level) along with the following JSON body: ```json { "success": false, "error": "Human-readable error description" } ``` TLDR [#tldr] | Code | Meaning | Your Action | | ---- | ------------------- | ------------------------------------------------------ | | 200 | Success | Use the data | | 400 | Bad Request | Fix parameters - check validation errors | | 401 | Unauthorized | Check API key header/parameter | | 403 | Forbidden | Check subscription status or feature access | | 404 | Not Found | Verify endpoint path | | 429 | Rate Limited | Wait and then retry. Upgrade subscriptionif persistent | | 500 | Server Error | Retry once after delay, simplify query if persistent | | 503 | Service Unavailable | Wait for service to reconnect | | 504 | Gateway Timeout | Reduce query complexity, retry once after delay | Types of Errors [#types-of-errors] Non-Standard Response Format [#non-standard-response-format] In rare cases you may receive: * An empty response body * A non-JSON response body (ex: text or html) * A response body which isn't parseable into JSON * A response body without a success field **Meaning:** These are typically due to transient networking issues or server errors. **Recommendation:** * Treat these the same as a 500-level error. * Retry the request once after a short delay * If you see these repeatedly and the request still fails after a retry, please contact support. 400 Bad Request [#400-bad-request] **Meaning:** Your request has invalid or missing parameters. **Troubleshooting**: * Typically the `error` field in the response will be helpful to determine what the issue is * Check the [reference](/v2/reference) docs and the [data-types](/docs/data-types/) docs to help ensure you're using the correct values. * Make sure you haven't included the same parameter multiple times. * If it's a boolean parameter, make sure it's either `true` or `false` (as a string) * If it's a date parameter, make sure it's a valid ISO-formatted date or Unix timestamp. Ex: `2026-01-01T00:00:00.000Z` or `1736899200000` * In some cases, you may be using an invalid combination of parameters. These are combinations of query parameters which can't logically go together such as `live=true` with `ended=true`. 401 Unauthorized [#401-unauthorized] **Meaning:** Authentication failed or API key is missing. **Troubleshooting**: * Make sure you've included your API key either in the header as `x-api-key` (case-insensitive) or as a query parameter as `apiKey`. * Ensure that the API key doesn't contain any leading or trailing whitespace. * Make sure you copied your API key correctly. Check your email and ensure you're using the same key which was sent to you. * Make sure your API key hasn't been re-generated or revoked. 403 Forbidden [#403-forbidden] **Meaning:** Your API key doesn't have permission to access this data **Troubleshooting**: * Make sure you haven't cancelled your subscription. [Check your subscription here](/account). * If you're on a paid plan, make sure you didn't have a failed payment/billing issue * Make sure you're using the latest API version. By default API keys are not granted access to old/legacy API versions. * Certain endpoints may be restricted by tier. For example, the /stream/events endpoint is only available on the AllStar plan. Contact support to upgrade. 404 Not Found [#404-not-found] **Meaning:** The requested resource doesn't exist. **Troubleshooting**: * Make sure you're using the correct URL to query the API. Check for typos. * The version of your API request was invalid. Make sure your URL path starts with `/v2/` or `/v1/` or another valid version. 429 Too Many Requests [#429-too-many-requests] **Meaning:** You've exceeded your rate limits **Troubleshooting**: * Try waiting up to a minute and then try again * Check your rate limit usage using the `/account/usage` endpoint * If you ran out of "objects per month" then you may need to upgrade your plan 500 Internal Server Error [#500-internal-server-error] **Meaning:** Something went wrong on our end. **Troubleshooting**: * Wait a few seconds and then retry the request once. * If it still fails, then contact support * Consider trying a different endpoint or different set of query parameters. Sometimes active problems are isolated to only specific cases of these * In general, these errors are not your fault and should be exceedingly rare * We ask that you do not continuously retry the request as this tends to overload our servers and may result in additional errors 503 Service Unavailable [#503-service-unavailable] **Meaning:** API is temporarily unavailable or our server is offline **Troubleshooting**: * Wait a few seconds and then retry the request once. * If it still fails, then contact support * In some cases, these types of errors are isolated to a single server instance. If that's what's happening, your retry may work. Otherwise, it's likely a more widespread issue with our servers. 504 Gateway Timeout [#504-gateway-timeout] **Meaning:** Request took too long to complete. **Troubleshooting**: * Your query may be too complex. The following can help reduce query complexity: * Remove these parameters: `includeAltLines`, `startsBefore`, `startsAfter`, `playerID`/`playerIDs`, `bookmakerID`/`bookmakerIDs`, `teamID`/`teamIDs` * Reduce the overall number of query parameters you're using * Our servers may be overloaded. Wait a few seconds and then retry the request once. * If it still fails, then contact support Error Handling Example [#error-handling-example] --- # Sports Betting Glossary - Terms and Definitions URL: https://sportsgameodds.com/docs/info/glossary Sports Betting Glossary - Terms and Definitions [#sports-betting-glossary---terms-and-definitions] A guide to sports betting terminology and how it relates to the SportsGameOdds API. Alternate Lines (Alt Lines) [#alternate-lines-alt-lines] Non-standard point spreads or totals offered at different prices. In the API, alternate lines appear as multiple entries for the same market with different `line` values. American Odds [#american-odds] Standard US format using positive/negative numbers. Negative odds (e.g., -110) indicate how much to risk to win $100. Positive odds (e.g., +150) indicate how much you win on a $100 bet. API Key [#api-key] A unique authentication token required to access the SportsGameOdds API. Passed via the `x-api-key` header in all API requests. Arbitrage (Arb) [#arbitrage-arb] Placing bets on all possible outcomes of an event at different bookmakers to guarantee profit by exploiting odds discrepancies. See our [Arbitrage Calculator recipe](/docs/examples/arbitrage-calculator). ATS [#ats] Against the Spread - betting on a team to cover the point spread. Bad Beat [#bad-beat] Losing a bet in an unlikely or devastating way after appearing to have won, such as a last-second score changing the cover. betTypeID [#bettypeid] Identifier for the type of bet: `ml` (moneyline), `sp` (spread), `ou` (over/under), `yn` (yes/no), `eo` (even/odd), or `prop` (proposition). Part of the `oddID` format. See [Bet Types](/docs/data-types/bet-types). Book Odds [#book-odds] Consensus odds across all bookmakers including juice. Fields include `bookOdds`, `bookSpread`, and `bookOverUnder`. Represents the average market price with vig included. Bookmaker ID [#bookmaker-id] Unique identifier for each sportsbook in the API (e.g., `draftkings`, `fanduel`, `betmgm`). See [Bookmakers reference](/docs/data-types/bookmakers). BTTS [#btts] Both Teams To Score - a soccer bet on whether both teams will score in the match. Buying Points [#buying-points] Paying additional juice to move the spread or total in your favor. For example, buying a half-point to move from -3 to -2.5. Not commonly available in API data. Closing Line [#closing-line] The final odds before an event starts. The closing line reflects all available information and is considered the most efficient, accurate price. In the API, closing lines are available via the `closing` field. CLV [#clv] Closing Line Value - the difference between the odds you received and the closing line. Consistently beating the closing line indicates sharp betting. Consensus Line [#consensus-line] The average or median odds across all bookmakers, useful for identifying the fair market price and spotting outlier opportunities. See our [Consensus Odds guide](/docs/info/consensus-odds). Cover [#cover] When a team beats the point spread. For example, if the Lakers are -5.5 and win by 6 or more points, they "covered" the spread. Cursor [#cursor] A pagination token used to fetch the next batch of results from the API. Provided in the `nextCursor` field of responses. See [Data Batches guide](/docs/guides/data-batches). Decimal Odds [#decimal-odds] Common in Europe and Australia. Represents total payout per $1 wagered, including your original stake. Example: 1.91 means a $1 bet returns $1.91 total ($0.91 profit). DNB [#dnb] Draw No Bet - a soccer bet where your stake is refunded if the match ends in a draw. EV [#ev] Expected Value - the average amount you can expect to win or lose per bet over the long term, based on probability and payout. Fair Odds [#fair-odds] Consensus odds with the juice (vig) removed, representing a more accurate probability. Fields include `fairOdds`, `fairSpread`, and `fairOverUnder`. See [Consensus Odds guide](/docs/info/consensus-odds). Favorite [#favorite] The team expected to win, indicated by negative odds in American format (e.g., -150). Fractional Odds [#fractional-odds] Traditional UK format showing profit relative to stake. Example: 10/11 means you win $10 profit for every $11 wagered. Futures [#futures] Long-term bets on season outcomes like championship winners, MVP awards, or team win totals. Handle [#handle] The total amount of money wagered on an event or market. Hedge [#hedge] Placing a bet on the opposite side of an existing bet to guarantee profit or reduce potential losses. Commonly used when the first bet is in a favorable position. Hook [#hook] A half-point in the spread or total (e.g., the .5 in -7.5). Prevents pushes by ensuring one side always wins. Implied Probability [#implied-probability] The probability of an outcome implied by the odds, calculated as `(1 / decimalOdds) * 100`. The sum of both sides' implied probabilities exceeds 100% due to the vig (bookmaker's commission). Juice / Vig (Vigorish) [#juice--vig-vigorish] The bookmaker's commission built into the odds. For example, when both sides are -110, the combined implied probability is \~104.76%, with the extra \~4.76% being the juice. League ID [#league-id] Unique identifier for each league or competition in the API (e.g., `nba`, `nfl`, `epl`). See [Leagues reference](/docs/data-types/leagues). Line Movement [#line-movement] Changes to odds or point spreads over time. Line movement is driven by sharp money, public betting patterns, injury news, weather conditions, and other market factors. Live Betting (In-Play) [#live-betting-in-play] Bets placed after an event has started, with odds updating in real-time as the game progresses. Use `live=true` in event queries to filter for live events. Middle [#middle] Betting both sides of an event at different lines, creating a scenario where both bets win if the result lands between the two lines. For example, betting Team A -3 and Team B +7. ML [#ml] Moneyline - a straight up bet on which team wins. Moneyline [#moneyline] A straight up bet on which team wins the game outright. Negative odds indicate the favorite (amount you risk to win $100), positive odds indicate the underdog (amount you win on a $100 bet). In the API, moneylines use `betTypeID: "ml"`. O/U [#ou] Over/Under - betting on whether the total score will be over or under a set number. Object [#object] The API's main pricing unit. An object is a single item returned in an API response. This can be an event (game), a league, a team, a player, etc. A single API query might return multiple objects, so if a query returned 10 events, then it would cost 10 objects. It's worth noting that we don't charge per odds market or bookmaker value returned, we only charge per object returned. This makes the SportsGameOdds API 60-80% cheaper than other APIs. oddID [#oddid] Unique identifier for each odd in the API, formatted as `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}-{bookmakerID}`. See our [Odds guide](/docs/data-types/odds). Opening Line [#opening-line] The first odds posted by a bookmaker for an event. In the API, opening lines are available via the `opening` field within `byBookmaker`. Parlay [#parlay] A single bet combining multiple selections where all must win for the bet to pay out. Odds multiply together for higher risk and higher reward. Period ID [#period-id] Identifier for the time period of a bet in the API: `game` for full game, `h1` for first half, `q1` for first quarter, `reg` for regulation only, etc. See [Periods reference](/docs/data-types/periods). PL [#pl] Puck Line - hockey's version of the point spread, typically set at ±1.5 goals. Positive EV (Expected Value) [#positive-ev-expected-value] A bet with a positive expected return, meaning it's mathematically profitable. Props (Proposition Bets) [#props-proposition-bets] Bets on specific events or player performances within a game, such as "LeBron James Over 28.5 Points" or "First Team to Score." In the API, props typically use `betTypeID: "ou"`, `"yn"`, or `"prop"`. Public Money [#public-money] Bets from recreational bettors. Public money tends to favor favorites and overs, often following popular teams and narratives. Push [#push] When the result lands exactly on the line and the bet is refunded with no win or loss. Half-point lines (e.g., -5.5) prevent pushes. RL [#rl] Run Line - baseball's version of the point spread, typically set at ±1.5 runs. ROI [#roi] Return on Investment - the percentage return on your betting bankroll over time. SGP [#sgp] Same Game Parlay - a parlay that combines multiple bets from the same game, such as a team to win plus a player prop. Sharp Money [#sharp-money] Bets placed by professional, well-informed bettors. Sharp action often causes significant line movement as bookmakers respect and adjust to these bets. sideID [#sideid] Identifier for which side of a bet: `home`, `away`, `over`, `under`, `yes`, `no`, `draw`, etc. Part of the `oddID` format. See [Bet Types](/docs/data-types/bet-types). Spread (Point Spread) [#spread-point-spread] A handicap applied to the favorite to level the playing field. If Lakers are -5.5, they must win by 6+ points to cover the spread. In the API, spreads use `betTypeID: "sp"`. Stat Entity ID [#stat-entity-id] Identifies who the stat applies to: `home` (home team), `away` (away team), `all` (both teams combined), or a specific `playerID`. Part of the `oddID` format. See [Stat Entity reference](/docs/data-types/stat-entity). Stat ID [#stat-id] Identifier for the statistic being bet on in the API (e.g., `points`, `rebounds`, `assists`, `touchdowns`). See [Stats reference](/docs/data-types/stats). Steam Move [#steam-move] Sudden, significant line movement across multiple bookmakers simultaneously. Steam moves typically indicate sharp money or important new information entering the market. Total (Over/Under) [#total-overunder] A bet on whether the combined score of both teams will be over or under a specified number. In the API, totals use `betTypeID: "ou"` with `sideID: "over"` or `"under"`. Underdog [#underdog] The team expected to lose, indicated by positive odds in American format (e.g., +150). --- # Rate Limits by Plan - Requests and Objects URL: https://sportsgameodds.com/docs/info/rate-limiting Rate Limits by Plan - Requests and Objects [#rate-limits-by-plan---requests-and-objects] Based on the package you chose, your API key will be limited to a certain number of requests made and/or objects returned during a given time interval. If you exceed this limit, you will receive a `429` status code and will be unable to make further requests until the limit resets. While we do make changes to our standard rate limits, these changes usually won't affect existing subscribers, so unless you receive an email, your rate limit will remain the same as it was when you signed up. Therefore, the limits posted in this guide may be different from those your API key is subject to. Request Limits [#request-limits] Each request you make to the Sports Game Odds API server will count towards your request rate limit. * Amateur plan: 10 requests per minute * Rookie plan: 50 requests per minute * Pro plan: 300 requests per minute * All-Star plan: Unlimited requests per minute Object Limits [#object-limits] Each request you make to the Sports Game Odds API server may return multiple objects in the response. Each object returned will count towards your object rate limit over a given time interval. Each response counts as a minimum of 1 object. * Amateur plan: 2,500 objects per month * Rookie plan: 100,000 objects per month * Pro plan: Unlimited objects per month * All-Star plan: Unlimited objects per month Default Limits [#default-limits] In order to protect our servers and ensure maximum uptime, the following default limits apply to all API keys. In general you shouldn't ever hit these limits regardless of your plan, but if you do, feel free to reach out to us and we can find a solution for removing or increasing these default limits * 50k requests per hour * 300k objects per hour * 7M objects per day Strategies to Avoid Rate Limiting [#strategies-to-avoid-rate-limiting] To protect your application from rate limiting, you can use the following strategies: 1. Avoid a high frequency of calls to endpoints serving data that doesn't change frequently (e.g., Teams, Players, Stats). 2. Cache this data locally to avoid making unnecessary requests. 3. Make use of available query params at each endpoint to focus on only the data you need. 4. You can always fetch data about your current rate limit usage using the `/account/usage` endpoint. Response Filtering Notice [#response-filtering-notice] When your API key has limitations that cause data to be filtered from responses (such as limited bookmakers or restricted data types), you may receive a `notice` field in the API response. This notice informs you about what data has been filtered and can be accessed at higher API tiers. The notice field appears in responses when: * Events are filtered due to sport or league restrictions * Bookmaker odds are filtered due to bookmaker access limitations * Data types are filtered due to subscription tier restrictions Example response with notice: ```json { "success": true, "data": [...], "nextCursor": "n.1720564800000.DCtqsAt8d0GIFAvMmfzD", "notice": "Response is missing 3 events and 15 bookmaker odds. Upgrade your API key to access all data from this query." } ``` This notice is designed to help you understand when your current subscription might be limiting your access to data, allowing you to make informed decisions about upgrading your plan. Checking Your Rate Limit Usage [#checking-your-rate-limit-usage] You can use the `/account/usage` endpoint to get information on your rate limits. This endpoint provides details about your current usage and remaining limits. A sample API call to the endpoint is below: ```javascript fetch('https://api.sportsgameodds.com/v2/account/usage', { headers: { 'X-Api-Key': 'YOUR_TOKEN' } }) ``` A sample response is below ```json { "success": true, "data": { "keyID": "abc123xyz456", "customerID": "cus_987xyz321abc", "isActive": true, "rateLimits": { "per-second": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": "n/a", "currentIntervalEntities": "n/a", "currentIntervalEndTime": "n/a" }, "per-minute": { "maxRequestsPerInterval": 1000, "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": 1, "currentIntervalEntities": "n/a", "currentIntervalEndTime": "2024-01-01T00:01:00.000Z" }, "per-hour": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": "n/a", "currentIntervalEntities": "n/a", "currentIntervalEndTime": "n/a" }, "per-day": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": "n/a", "currentIntervalEntities": "n/a", "currentIntervalEndTime": "n/a" }, "per-month": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": 1000000, "currentIntervalRequests": "n/a", "currentIntervalEntities": 100, "currentIntervalEndTime": "2024-01-31T00:01:00.000Z" } } } } ``` --- # Migration Guide - V1 to V2 API URL: https://sportsgameodds.com/docs/info/v1-to-v2 Migration Guide - V1 to V2 API [#migration-guide---v1-to-v2-api] Overview of Changes [#overview-of-changes] * **Odds can update significantly more frequently** - We overhauled our system which aggregates and syncs odds data to the API, allowing us to significantly speed up the polling frequency of odds data and offer data at less than 50% the delay of the previous system. Effects on your data depend on tier. * **Faster API Response Times** - When fetching data from the API, you should receive data significantly faster. This change affects both v1 and v2 endpoints. Note that v2 responses often contain more data than v2 which may affect response times. * **Combined /odds and /events endpoints** - You no longer need to query one endpoint to get odds breakdowns by sportsbook and another endpoint to get event metadata (status, teams, players, results, etc.). All of this data has now been combined into a single `/v2/events` endpoint. * **Added deeplinks to odds data** - We've added a deeplink field to the bookmaker-specific odds data. Only major US sportsbooks were included at launch (FanDuel, Draftkings, BetMGM) with more to come soon after. * **Bookmaker-specific odds persist when unavailable/inactive** - Previously, odds for a specific bookmaker (under the byBookmaker field) would only show when those odds were actively being offered (open for wagers) by that bookmaker and would disappear when no longer offered. In v2, all odds (both for main lines and alt lines) will show along with an `available` field which tells you whether those odds are actively being offered (open for wagers). * **Improved Player System** - Players are now unique across all teams in a league. This means a player will have the same playerID regardless of which team they are on. This new player system also allows us to return a higher number of player props odds and results/stats data when querying Events. * **More request options added to Events (/Odds) endpoint** We added 10 additional parameters to the /events endpoint to help you zero-in on the data you need. Even more options are coming soon. Upgrade Guide [#upgrade-guide] Some of the changes made to the v2 API cause the /events endpoint it to return a lot of data. We highly recomend you make use of the new params we've added to the /events endpoint in order ensure you're only requesting the data you need. This helps keep the API running fast for both yourself as well as everyone else. Thanks! Change the URLs you use to access the API to use the new v2 endpoints by simply swapping v1 for v2. [#change-the-urls-you-use-to-access-the-api-to-use-the-new-v2-endpoints-by-simply-swapping-v1-for-v2] ```text https://api.sportsgameodds.com/v1/... // [!code --] https://api.sportsgameodds.com/v2/... // [!code ++] ``` Change any request to either v1/events or v1/odds endpoints to use the new combined v2/events endpoint (there is no v2/odds endpoint) [#change-any-request-to-either-v1events-or-v1odds-endpoints-to-use-the-new-combined-v2events-endpoint-there-is-no-v2odds-endpoint] ```text https://api.sportsgameodds.com/v1/events... // [!code --] https://api.sportsgameodds.com/v1/odds... // [!code --] https://api.sportsgameodds.com/v2/events... // [!code ++] ``` The v2/events endpoint now returns everything found in the v1/events + v1/odds endpoints [#the-v2events-endpoint-now-returns-everything-found-in-the-v1events--v1odds-endpoints] If you were parsing data from the **v1/events endpoint's odds object**, (not the v1/odds endpoint's odds object) then you will need to make some changes: * `Event.odds..odds` -> `Event.odds..fairOdds` * `Event.odds..spread` -> `Event.odds..fairSpread` * `Event.odds..overUnder` -> `Event.odds..fairOverUnder` * `Event.odds..closeOdds` -> `Event.odds..closeFairOdds` * `Event.odds..closeSpread` -> `Event.odds..closeFairSpread` * `Event.odds..closeOverUnder` -> `Event.odds..closeFairOverUnder` * `Event.odds..isFallbackOdds` -> `REMOVED` * We previously showed placeholder even money odds marked with `isFallbackOdds = true` when odds were not yet available. This is no longer the case in v2. Some event status fields changes [#some-event-status-fields-changes] * `Event.status.hasMarketOdds` -> `Event.status.oddsPresent` * `Event.status.hasAnyOdds` -> `Event.status.oddsPresent` * `Event.status.anyOddsAvailable` -> `Event.status.oddsAvailable` * `Event.status.marketOddsAvailable` -> `Event.status.oddsAvailable` * `Event.status.nextUpdateAt` -> `REMOVED` Event players changed in certain cases [#event-players-changed-in-certain-cases] The Event.players object returned by the /v2/events endpoint will include each player's new playerID. On v1 of the API, any players already attached to an event (meaning there were odds or results data on the event for that player) by Jan 31, 2025 will remain unaffected. However, players attached after this date will carry the new/V2 playerID on the Event. Reach out to support if this may pose a problem to you. Add the includeAltLines parameter to /v2/events requests if you need alt lines by bookmaker. [#add-the-includealtlines-parameter-to-v2events-requests-if-you-need-alt-lines-by-bookmaker] By default, altLines are not included in v2 responses. To also include altLines add the parameter `includeAltLines=true` to your request. Please note that this can significantly increase the amount of data returned which in turn will increase latency of response times. Filter out unavailable byBookmaker odds if needed [#filter-out-unavailable-bybookmaker-odds-if-needed] Odds returned in the byBookmaker now include both available and unavailable odds. Each item in byBookmaker has an `available` field to tell you whether those odds are currently available. Therefore, if you only care about bookmaker-specific odds which are available and open for wagers, you'll need to ignore/filter any byBookmaker odds where `available == false`. You'll find the available field at the following path: ```text Event.odds..byBookmaker..available // [!code ++] Event.odds..byBookmaker..altLines.[i].available // [!code ++] ``` Parse player names data from different path on Player objects [#parse-player-names-data-from-different-path-on-player-objects] Player names are now wrapped in a name object and the variation `name` has been renamed to `display` * `Player.firstName` -> `Player.names.firstName` * `Player.lastName` -> `Player.names.lastName` * `Player.name` -> `Player.names.display` **NOTE:** This does not affect the player data found on Events (at `Event.players.{playerID}`) Update saved/stored playerIDs [#update-savedstored-playerids] If you haven't saved playerIDs anywhere in your system or wouldn't run into issues if a player's playerID changed, then you can skip this section. All playerIDs have changed.. If you have saved a list of playerID values somewhere in your system which you use to identify certain players, you will likely need to update those playerIDs to use the player's new value. The format of playerIDs changed like so: ```text PLAYER_NAME_TEAM_ID_LEAGUE_ID // [!code --] PLAYER_NAME_NUMBER_LEAGUE_ID // [!code ++] ``` And since players with the exact same name are rare, the vast majority of players were renamed like so: ```text PATRICK_MAHOMES_KANSAS_CITY_CHIEFS_NFL // [!code --] PATRICK_MAHOMES_1_NFL // [!code ++] ``` However, while this covers most cases, be warned that there are a number of discrepancies. Therefore, migrating your saved playerIDs using the above pattern is NOT RECOMMENDED. Instead we recommend the following approaches to convert between old and new playerID formats in the more accurate possible way: v1 playerID -> v2 playerID [#v1-playerid---v2-playerid] Here we recommend calling the v2/players endpoint and supplying an `alias` param. This will return a single Player object with the new playerID ```mermaid flowchart TD A["PATRICK_MAHOMES_KANSAS_CITY_CHIEFS_NFL"] --> B B["api.sportsgameodds.com/v2/players?alias=PATRICK_MAHOMES_KANSAS_CITY_CHIEFS_NFL"] --> C C["Response.data.[0].playerID equals PATRICK_MAHOMES_1_NFL"] ``` v2 playerID -> v1 playerID [#v2-playerid---v1-playerid] Here we recommend calling the v2/players endpoint providing the v2 playerID as the playerID param and then finding the v1 playerID you're looking for in the response's aliases field ```mermaid flowchart TD A["PATRICK_MAHOMES_1_NFL"] --> B B["api.sportsgameodds.com/v2/players?playerID=PATRICK_MAHOMES_1_NFL"] --> C C["Response.data.[0].aliases includes PATRICK_MAHOMES_KANSAS_CITY_CHIEFS_NFL"] ``` New Features & Non-Breaking Changes [#new-features--non-breaking-changes] The API key can now be included in the URL query params [#the-api-key-can-now-be-included-in-the-url-query-params] You no longer have to include your API key in the headers. You can also include it in the request query params instead. `https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY_CAN_GO_HERE` New params for v2/events endpoint [#new-params-for-v2events-endpoint] * `type` - Only include Events of the specified type which can be `match`, `prop`, or `tournament` * `oddsPresent` - Whether you want only Events which do (true) or do not (false) have any associated odds markets regardless of whether those odds markets are currently available (open for wagering) * `includeOpposingOdds` - Whether to include opposing odds for each included oddID. This was renamed from `includeOpposingOddIDs` in v1 but the old name will still work. * `includeAltLines` - Whether to include alternate lines in the odds byBookmaker data * `bookmakerID` - A bookmakerID or comma-separated list of bookmakerIDs to include odds for * `teamID` - A teamID or comma-separated list of teamIDs to include Events (and associated odds) for * `playerID` - A single playerID or comma-separated list of playerIDs to include Events (and associated odds) for * `live` - Only include live Events (true), only non-live Events (false) or all Events (omit) * `started` - Only include Events which have have previously started (true), only Events which have not previously started (false) or all Events (omit) * `ended` - Only include Events which have have ended (true), only Events which have not ended (false) or all Events (omit) * `cancelled` - Only include cancelled Events (true), only non-cancelled Events (false) or all Events (omit) * `includeOpenCloseOdds` - Whether to include open and close odds values (openOdds, closeOdds, openSpread, closeSpread, openOverUnder, closeOverUnder) in the odds byBookmaker data. Defaults to false. New deeplink fields [#new-deeplink-fields] You'll be able to find deeplink information on a per-bookmaker basis at the following path on Event objects: `Event.odds..byBookmaker..deeplink` Deeplinks will also appear on altLines but only if they are different from the deeplink value on the main line `Event.odds..byBookmaker..altLines.[i].deeplink` Market name field added to odds [#market-name-field-added-to-odds] Odds items now contain a human-readable `marketName` field. Standardized weight, height and salary fields [#standardized-weight-height-and-salary-fields] These should now return a more consistent string-based value including units. Added nickname and suffix to player names [#added-nickname-and-suffix-to-player-names] These fields will show only when applicable Renamed Parameters with Backwards Compatibility [#renamed-parameters-with-backwards-compatibility] Certain API request options/parameters have been renamed. However all of the previous names will still work. For example, we've renamed `includeOpposingOddIDs` to `includeOpposingOdds` but the old param will also still work Per-request default and max limits have changed [#per-request-default-and-max-limits-have-changed] * For /events * default limit: 30 -> 10 * max limit: 300 -> 100 * For teams and players * default limit: 30 -> 50 * max limit: 300 -> 250 --- # Need Help URL: https://sportsgameodds.com/docs/snippets/need-help [FAQ](/docs/faq) · [Email](mailto:api@sportsgameodds.com) · [Chat](/) · [Discord](https://discord.gg/HhP9E7ZZaE)