--- url: /docs/v2/info/ai-vibe-coding.md description: >- Use Claude Code, Cursor, or Copilot with SportsGameOdds API. Install MCP server, access llms.txt docs, and copy-paste AI context for accurate code generation. --- # AI-Assisted Development 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](#ai-context) **We highly recommend you either paste this into your prompt or add it to an applicable context/rules file.** ## AI-Friendly Docs ### [Indexed Documentation](/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](/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-v2.json) A full machine-readable API definition with exact schemas for all endpoints/requests and responses. Length: ~600k tokens ## 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 ::: code-group ```bash [command] claude mcp add sports-game-odds --env SPORTS_ODDS_API_KEY_HEADER="your-api-key-here" -- npx -y sports-odds-api-mcp ``` ```json [json] { "mcpServers": { "sports-game-odds": { "command": "npx", "args": ["-y", "sports-odds-api-mcp@latest"], "env": { "SPORTS_ODDS_API_KEY_HEADER": "your-api-key-here" } } } } ``` ::: ### Cursor [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=sports-game-odds\&config=eyJlbnYiOnsiU1BPUlRTX09ERFNfQVBJX0tFWV9IRUFERVIiOiJ5b3VyLWFwaS1rZXktaGVyZSJ9LCJjb21tYW5kIjoibnB4IC15IHNwb3J0cy1vZGRzLWFwaS1tY3BAbGF0ZXN0In0%3D) ::: code-group ```bash [json] { "mcpServers": { "sports-game-odds": { "command": "npx", "args": ["-y", "sports-odds-api-mcp@latest"], "env": { "SPORTS_ODDS_API_KEY_HEADER": "your-api-key-here" } } } } ``` ::: ### VS Code [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_SportsGameOdds_MCP-0098FF?style=for-the-badge\&logo=visualstudiocode\&logoColor=ffffff)](vscode:mcp/install?%7B%22name%22%3A%22sports-game-odds%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22sports-odds-api-mcp%40latest%22%5D%2C%22env%22%3A%7B%22SPORTS_ODDS_API_KEY_HEADER%22%3A%22%24%7Binput%3Aapi-key%7D%22%7D%7D) ::: code-group ```bash [command] 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 [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 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/openAPI-v2.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 | ``` --- --- url: /docs/v2/basics/cheat-sheet.md description: >- SportsGameOdds API cheat sheet with endpoints, authentication, oddID format, and response structure. Copy-paste examples for common requests. --- # Cheat Sheet ::: tip TLDR * Get an API key [here](https://sportsgameodds.com/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](/reference). * Responses are in JSON format. The `data` field contains the data you queried for. * Use our [Data Explorer](/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 A full reference of all endpoints and their parameters can be found [here](/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`](/reference#tag/events/GET/events/) endpoint. ## Authentication All requests require an [API key](https://sportsgameodds.com/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 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](/guides/data-batches). ## 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 The best way to get a sense of how our data is structured is to play around with our [Data Explorer](/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? --- --- url: /docs/v2/reference.md description: >- Complete SportsGameOdds API reference. All endpoints, query parameters, request/response schemas with interactive testing. OpenAPI specification. --- * --- --- url: /docs/v2/examples/arbitrage-calculator.md description: >- Build an arbitrage finder with the SportsGameOdds API. Scan bookmakers for odds discrepancies, calculate guaranteed profit. Full Python code. --- # Build an Arbitrage Calculator Find guaranteed profit opportunities by identifying odds discrepancies across bookmakers. ## What You'll 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? **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 * Python 3.8+ * SportsGameOdds API key ([Get one free](https://sportsgameodds.com/pricing)) * Basic Python knowledge ## Complete Code ### 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 ```python # arb_calculator.py import requests import os 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 ```bash export SPORTSGAMEODDS_KEY=your_api_key_here python arb_calculator.py ``` ## 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 ### 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 ```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 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 ```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 ```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 ```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 **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 ### 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 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 ```python import time 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 ```python import requests 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 ### 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 Arbitrage opportunities disappear quickly (seconds to minutes). **Solutions:** * Use our [Real-time Streaming](/guides/realtime-streaming-api) (AllStar plan) * Automate bet placement (requires bookmaker APIs) * Set up price alerts ### 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 Remember to account for: * Withdrawal fees * Currency conversion * Deposit bonuses with rollover requirements ## Troubleshooting ### "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 **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']}" ``` --- --- url: /docs/v2/info/best-practices.md description: >- API best practices for security, performance, and cost optimization. Avoid common mistakes like frontend API calls, over-polling, and missing filters. --- # Best Practices & 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 ### 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 * 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 * Including this parameter can significantly reduce the respoonse payload size and improve response times. ### 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 * 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 * Before you start using the `cursor` parameter, increase the `limit` parameter so you get more results per request. ### Keep your API key secure * Never expose it in your frontend code. * Never commit it to version control. ### Monitor your usage * Check the `/account/usage` endpoint to see how you're doing against your rate limits ### 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 * 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 * 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 ### Making API Requests from a Frontend/Browser * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * See our [Reference Docs](/reference) for more information on the valid query parameters for each endpoint ### 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. --- --- url: /docs/v2/data-types/bet-types.md description: >- All betTypeID and sideID values in the SportsGameOdds API. Moneyline (ml), spread (sp), over/under (ou), 3-way, and prop bet definitions. --- # Bet Types and Sides 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 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. ## Bet Types ## Sides --- --- url: /docs/v2/data-types/bookmakers.md description: >- Complete list of bookmakerID values. DraftKings, FanDuel, BetMGM, Caesars, PrizePicks, Pinnacle, and 80+ more sportsbooks and DFS platforms. --- # Bookmakers Each `bookmakerID` corresponds to a sportbook, daily fantasy site, or betting platform. > \[!Note] > 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](https://sportsgameodds.com/contact-us) to discuss your needs. --- --- url: /docs/v2/info/consensus-odds.md description: >- How fairOdds and bookOdds are calculated. Linear regression finds fair lines, median odds across bookmakers, juice removal for true probability. --- # Consensus Odds Calculations ## "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 ### 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 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 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 ### Key Fields * `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 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 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 --- --- url: /docs/v2/explorer.md description: >- Interactive tool to browse SportsGameOdds API data. Explore sports, leagues, teams, events, and odds markets in real-time without writing code. --- * --- --- url: /docs/v2/data-types.md description: >- How SportsGameOdds API data types relate. Hierarchy of sports, leagues, teams, events, odds, and bookmakers with identifier formats explained. --- # Data Types 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` | --- --- url: /docs/v2/info/errors.md description: >- Full reference of all error codes, what they mean and how to troubleshoot them. Includes code examples with best practices for error handling. --- # Error Reference 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 | 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 ### 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 **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](/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 **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 **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](https://billing.stripe.com/p/login/5kA3eWePU7affkIdQQ). * 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 **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 **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 **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 **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 **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 ::: code-group ```js [Javascript] async function fetchSGO(url, options = {}, canRetry = true) { let response, payload, error; try { response = await fetch(url, options); payload = await response.json(); } catch (e) { error = e; } const success = payload?.success; const status = response?.status; if (success === true) return payload.data; const isClientError = success === false && status >= 400 && status < 500 && status !== 429; if (canRetry && !isClientError) { const backoffMs = 2000 + Math.floor(Math.random() * 3000); await new Promise((resolve) => setTimeout(resolve, backoffMs)); return fetchSGO(url, options, false); } console.error(`SGO Request Failed: ${status} ${payload?.error || error?.message}`, { url, options, payload, error }); return null; } ``` ```python [Python] import requests import random import time def fetch_sgo(url, options=None, can_retry=True): options = options or {} response = None payload = None try: response = requests.request( method=options.get('method', 'GET'), url=url, headers=options.get('headers'), json=options.get('body') ) payload = response.json() except Exception as e: error = e else: error = None if payload and payload.get('success') is True: return payload.get('data') status = response.status_code if response else None is_client_error = ( payload and payload.get('success') is False and status and 400 <= status < 500 and status != 429 ) if can_retry and not is_client_error: time.sleep(2 + random.random() * 3) return fetch_sgo(url, options, can_retry=False) error_msg = (payload or {}).get('error') or (str(error) if error else 'Unknown') print(f"SGO Request Failed: {status} {error_msg}") return None ``` ```java [Java] import java.net.URI; import java.net.http.*; import java.time.Duration; import java.util.Map; import java.util.Random; import com.google.gson.*; public class SgoClient { private static final HttpClient client = HttpClient.newHttpClient(); private static final Gson gson = new Gson(); private static final Random random = new Random(); public static JsonElement fetchSgo(String url, Map headers, String body, boolean canRetry) { HttpResponse response = null; JsonObject payload = null; Exception error = null; try { var builder = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(30)); if (headers != null) { headers.forEach(builder::header); } if (body != null) { builder.POST(HttpRequest.BodyPublishers.ofString(body)); } response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString()); payload = gson.fromJson(response.body(), JsonObject.class); } catch (Exception e) { error = e; } if (payload != null && payload.has("success") && payload.get("success").getAsBoolean()) { return payload.get("data"); } int status = response != null ? response.statusCode() : 0; boolean isClientError = payload != null && payload.has("success") && !payload.get("success").getAsBoolean() && status >= 400 && status < 500 && status != 429; if (canRetry && !isClientError) { try { Thread.sleep(2000 + random.nextInt(3000)); } catch (InterruptedException ignored) {} return fetchSgo(url, headers, body, false); } String errorMsg = payload != null && payload.has("error") ? payload.get("error").getAsString() : (error != null ? error.getMessage() : "Unknown"); System.err.println("SGO Request Failed: " + status + " " + errorMsg); return null; } } ``` ```ruby [Ruby] require 'net/http' require 'json' require 'uri' def fetch_sgo(url, options = {}, can_retry: true) response = nil payload = nil error = nil begin uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' request = Net::HTTP::Get.new(uri) options[:headers]&.each { |k, v| request[k] = v } response = http.request(request) payload = JSON.parse(response.body) rescue => e error = e end return payload['data'] if payload&.dig('success') == true status = response&.code&.to_i is_client_error = payload&.dig('success') == false && status && status >= 400 && status < 500 && status != 429 if can_retry && !is_client_error sleep(2 + rand * 3) return fetch_sgo(url, options, can_retry: false) end error_msg = payload&.dig('error') || error&.message || 'Unknown' warn "SGO Request Failed: #{status} #{error_msg}" nil end ``` ```php [PHP] true, CURLOPT_HTTPHEADER => $options['headers'] ?? [], CURLOPT_TIMEOUT => 30, ]); $body = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($body !== false) { $payload = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { $payload = null; } } if (isset($payload['success']) && $payload['success'] === true) { return $payload['data'] ?? null; } $isClientError = isset($payload['success']) && $payload['success'] === false && $httpCode >= 400 && $httpCode < 500 && $httpCode !== 429; if ($canRetry && !$isClientError) { usleep((2 + lcg_value() * 3) * 1_000_000); return fetchSgo($url, $options, false); } $errorMsg = $payload['error'] ?? $error ?: 'Unknown'; error_log("SGO Request Failed: {$httpCode} {$errorMsg}"); return null; } ``` ::: *** ::: tip Need Help? [FAQ](/faq) · [Email](mailto:api@sportsgameodds.com) · Chat · [Discord](https://discord.gg/HhP9E7ZZaE) ::: --- --- url: /docs/v2/faq.md description: >- Answers to common SportsGameOdds API questions. Authentication, filtering, rate limits, error handling, oddID format, player props, historical data, and optimization tips. --- # Frequently Asked Questions Common questions about using the SportsGameOdds API. Don't see your question? [Contact us](https://sportsgameodds.com/contact-us/) or join our [Discord](https://discord.gg/HhP9E7ZZaE). ## Getting Started ::: details What is an odds API and how does it work? An odds API delivers betting odds in machine-readable format, aggregating data from multiple sportsbooks. It provides moneylines, spreads, totals, player props, alternate lines, and settlement data through REST or WebSocket endpoints. SportsGameOdds updates every 30-60 seconds across 25+ sports, 55+ leagues, and 80+ bookmakers. Instead of scraping bookmakers manually, an odds API aggregates odds from multiple sportsbooks and provides them through a simple REST interface. ::: ::: details How do I get an API key? 1. Sign up at [sportsgameodds.com/pricing](https://sportsgameodds.com/pricing) 2. Select a plan (free tier available) 3. Check your email for your API key 4. Start making requests immediately No credit card is required for the free (Amateur) tier. ::: ::: details Can I test the API for free? Yes! SportsGameOdds offers a free tier (Amateur plan) for testing and small projects with: * 10 requests per minute * 2,500 objects per month * Access to all sports, leagues, and bookmakers * No credit card required The free tier is perfect for prototyping and testing. [Sign up here](https://sportsgameodds.com/pricing/). ::: ::: details What's the fastest way to make my first API call? Paste this URL into your browser (replace `YOUR_API_KEY`): ``` https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true ``` Or use code: **JavaScript:** ```javascript const response = await fetch("https://api.sportsgameodds.com/v2/events?leagueID=NBA&oddsAvailable=true", { headers: { "x-api-key": "YOUR_API_KEY" } }); const { data } = await response.json(); console.log(data); ``` **Python:** ```python import requests response = requests.get( "https://api.sportsgameodds.com/v2/events", headers={"x-api-key": "YOUR_API_KEY"}, params={"leagueID": "NBA", "oddsAvailable": "true"} ) print(response.json()["data"]) ``` **Ruby:** ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/events?leagueID=NBA&oddsAvailable=true') response = Net::HTTP.get_response(uri, {'x-api-key' => 'YOUR_API_KEY'}) puts JSON.parse(response.body)['data'] ``` **PHP:** ```php $ch = curl_init('https://api.sportsgameodds.com/v2/events?leagueID=NBA&oddsAvailable=true'); curl_setopt($ch, CURLOPT_HTTPHEADER, ['x-api-key: YOUR_API_KEY']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); print_r($response['data']); ``` **Java:** ```java HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.sportsgameodds.com/v2/events?leagueID=NBA&oddsAvailable=true")) .header("x-api-key", "YOUR_API_KEY") .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ::: ::: details How do I authenticate my requests? Include your API key in one of two ways: **Option 1: Header (recommended)** ``` x-api-key: YOUR_API_KEY ``` **Option 2: Query parameter** ``` ?apiKey=YOUR_API_KEY ``` The header method is recommended because it keeps your API key out of URL logs. ::: ::: details Do you have an SDK I can use? Yes! Official SDKs are available for: * **TypeScript/JavaScript**: `npm install sports-odds-api` * **Python**: `pip install sports-odds-api` * **Ruby**: `gem install sports-odds-api` * **Go**: `go get github.com/SportsGameOdds/sports-odds-api-go` * **Java**: Maven/Gradle packages available SDKs include auto-pagination, type safety, retry logic, and error handling. See the [SDK Guide](/sdk) for details. ::: ## Data Coverage ::: details What sports and leagues do you support? We support **55+ leagues** across **25+ sports** including NFL, NBA, MLB, NHL, UFC, EPL, La Liga, Champions League, and many more. See [Sports Reference](/data-types/sports) for all sports and [Leagues Reference](/data-types/leagues) for the complete league list. Call the `/leagues` endpoint to see leagues available to your API key. ::: ::: details How many bookmakers do you cover? We cover **80+ bookmakers** including DraftKings, FanDuel, BetMGM, Caesars, Pinnacle, Bet365, PrizePicks, and many more US, international, and DFS platforms. See [Bookmakers Reference](/data-types/bookmakers) for the complete list. ::: ::: details Do you support player props? Yes! We provide extensive player prop coverage across all major sports. To see specific league + market + bookmaker coverage, check out the [Supported Markets](/data-types/markets) page. ::: ::: details Do you offer historical odds data? Yes, but the availability can vary. Depending on your plan, you may have access to up to 2 years of historical data. In general, you may see less specific data the further back you go. We're expanding historical data access in phases: 1. Event metadata + final team scores 2. Team & player stats + main market opening/closing odds 3. Prop markets + bookmaker-specific odds Use `startsAfter` and `startsBefore` to query historical events by date. ::: ::: details Do you provide live/in-play odds? Yes! We provide live odds for games currently in progress. Use `live=true` to filter: ``` /events?leagueID=NBA&live=true ``` ::: ::: details What betting markets are available? We support all major betting markets including moneyline, spread, over/under, 3-way moneyline (soccer), yes/no props, and more. Markets are available for full game, halves, quarters, and other periods. See [Supported Markets](/data-types/markets) for coverage by league and bookmaker, and [Bet Types Reference](/data-types/bet-types) for all `betTypeID` values. ::: ::: details Do you have deeplinks to bookmakers? Yes we do. However, we don't guarentee that every market for every bookmaker will include a deeplink. Each event includes `links.bookmakers` with event-level deeplinks. We also include a deeplink field at the top level of byBookmaker objects (`odds..byBookmaker..deeplink`). We're in the process of adding more deeplink coverage across bookmakers. ::: ## Understanding Odds Data ::: details What is an oddID? The `oddID` uniquely identifies a specific betting option. It allows us to uniquely identify the market (what you're betting on) and the side (which side of the market you're taking) for a given event. Here is the format: ``` {statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID} ``` **Examples:** * `points-home-game-ml-home` = Moneyline on home team (full game) * `points-all-game-ou-over` = Total points over (full game) * `assists-LEBRON_JAMES_NBA-game-ou-over` = LeBron assists over (player prop) See [Odds Reference](/data-types/odds) for complete details on each component. ::: ::: details What's the difference between fairOdds and bookOdds? * **`fairOdds`**: Consensus odds with juice/vig removed - represents true probability * **`bookOdds`**: Consensus odds across bookmakers with juice included Use `fairOdds` to calculate true implied probability and compare `byBookmaker` odds to find value bets. See [Consensus Odds](/info/consensus-odds) for calculation methodology. ::: ::: details What are the different bet types (betTypeID)? The main bet types are `ml` (moneyline), `sp` (spread), `ou` (over/under), `ml3way` (3-way moneyline for soccer), `yn` (yes/no), and `eo` (even/odd). See [Bet Types Reference](/data-types/bet-types) for all `betTypeID` and `sideID` values. ::: ::: details How do I convert American odds to decimal/fractional? **American to Decimal:** **JavaScript:** ```javascript function americanToDecimal(american) { if (american > 0) return american / 100 + 1; return 100 / Math.abs(american) + 1; } americanToDecimal(-110); // 1.909 americanToDecimal(150); // 2.5 ``` **Python:** ```python def american_to_decimal(american): if american > 0: return american / 100 + 1 return 100 / abs(american) + 1 american_to_decimal(-110) # 1.909 american_to_decimal(150) # 2.5 ``` **Ruby:** ```ruby def american_to_decimal(american) return american / 100.0 + 1 if american > 0 100 / american.abs.to_f + 1 end american_to_decimal(-110) # 1.909 american_to_decimal(150) # 2.5 ``` **PHP:** ```php function americanToDecimal($american) { if ($american > 0) return $american / 100 + 1; return 100 / abs($american) + 1; } americanToDecimal(-110); // 1.909 americanToDecimal(150); // 2.5 ``` **Decimal to Implied Probability:** ```javascript function decimalToImpliedProb(decimal) { return (1 / decimal) * 100; } decimalToImpliedProb(1.909); // ~52.4% decimalToImpliedProb(2.5); // 40% ``` See [Handling Odds Guide](/guides/handling-odds) for more conversions. ::: ::: details What does statEntityID mean? `statEntityID` identifies who the stat applies to: `home` (home team), `away` (away team), `all` (combined/total), or a specific `playerID` for player props. See [Stat Entity Reference](/data-types/stat-entity) for details. ::: ::: details What are the different periodIDs? `periodID` defines the time segment: `game` (full game), `reg` (regulation only), `1h`/`2h` (halves), `1q`-`4q` (quarters), `1p`-`3p` (hockey periods), `1i`-`9i` (innings), `1s`-`5s` (tennis sets), and `ot` (overtime). See [Periods Reference](/data-types/periods) for all values by sport. ::: ::: details Why does "points" mean different things in different sports? The `statID` of `points` represents the primary scoring stat used to determine the winner - actual points in basketball/football, runs in baseball, goals in hockey/soccer, and sets in tennis. This allows consistent querying across sports (e.g., `points-home-game-ml-home` works for any sport). Sport-specific stats like `goals` or `runs` are also available. See [Stats Reference](/data-types/stats). ::: ## Making API Requests ::: details How do I get odds data? All odds data comes from the `/events` endpoint. Each event includes an `odds` object keyed by `oddID`: **JavaScript:** ```javascript const response = await fetch("https://api.sportsgameodds.com/v2/events?leagueID=NBA&oddsAvailable=true", { headers: { "x-api-key": API_KEY } }); const { data } = await response.json(); // Access odds for an event const event = data[0]; const moneyline = event.odds["points-home-game-ml-home"]; console.log(`Home ML: ${moneyline.bookOdds}`); ``` **Python:** ```python import requests response = requests.get( "https://api.sportsgameodds.com/v2/events", headers={"x-api-key": API_KEY}, params={"leagueID": "NBA", "oddsAvailable": "true"} ) data = response.json()["data"] # Access odds for an event event = data[0] moneyline = event["odds"]["points-home-game-ml-home"] print(f"Home ML: {moneyline['bookOdds']}") ``` **Ruby:** ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/events?leagueID=NBA&oddsAvailable=true') response = Net::HTTP.get_response(uri, {'x-api-key' => API_KEY}) data = JSON.parse(response.body)['data'] # Access odds for an event event = data[0] moneyline = event['odds']['points-home-game-ml-home'] puts "Home ML: #{moneyline['bookOdds']}" ``` **PHP:** ```php $ch = curl_init('https://api.sportsgameodds.com/v2/events?leagueID=NBA&oddsAvailable=true'); curl_setopt($ch, CURLOPT_HTTPHEADER, ['x-api-key: ' . $API_KEY]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true)['data']; // Access odds for an event $event = $data[0]; $moneyline = $event['odds']['points-home-game-ml-home']; echo "Home ML: " . $moneyline['bookOdds']; ``` Use query parameters to filter which odds are returned. See [Odds Reference](/data-types/odds). ::: ::: details How do I filter by specific bookmaker(s)? Use the `bookmakerID` parameter: ```javascript // Single bookmaker /v2/events?leagueID=NBA&bookmakerID=draftkings // Multiple bookmakers (comma-separated) /v2/events?leagueID=NBA&bookmakerID=draftkings,fanduel,betmgm ``` This filters the `byBookmaker` object in each odd to only include specified bookmakers, reducing response size and improving performance. See [Bookmakers Reference](/data-types/bookmakers) for all available values. ::: ::: details How do I filter by specific team(s)? Use the `teamID` parameter: ```javascript // Single team /v2/events?leagueID=NBA&teamID=LOS_ANGELES_LAKERS_NBA // Multiple teams /v2/events?leagueID=NBA&teamID=LOS_ANGELES_LAKERS_NBA,GOLDEN_STATE_WARRIORS_NBA ``` This returns only events where the specified teams are playing. Team IDs follow the pattern `{TEAM_NAME}_{LEAGUE}`. ::: ::: details How do I get only player props? Filter using `statEntityID=player` with bet type `ou` (over/under): ```javascript // All player props for NBA /v2/events?leagueID=NBA&betTypeID=ou&statEntityID=player // Specific stat (points props only) /v2/events?leagueID=NBA&betTypeID=ou&statID=points&statEntityID=player // Specific player /v2/events?leagueID=NBA&playerID=LEBRON_JAMES_NBA ``` ::: ::: details What's the difference between live=true and finalized=false? * **`live=true`**: Only games currently in progress * **`finalized=false`**: Upcoming + live games (not yet settled) * **`started=false`**: Only upcoming games (not started yet) * **`finalized=true`**: Only completed games (historical results) ```javascript /v2/events?leagueID=NBA&live=true // Live games only /v2/events?leagueID=NBA&finalized=false // Upcoming + live /v2/events?leagueID=NBA&started=false // Upcoming only ``` ::: ::: details How do I use pagination (cursor)? Use cursor-based pagination for large result sets: ```javascript // First request const page1 = await fetch("/v2/events?leagueID=NBA&limit=50", { headers }); const data1 = await page1.json(); // data1.nextCursor = "n.1720564800000.abc123" // Next page const page2 = await fetch(`/v2/events?leagueID=NBA&limit=50&cursor=${data1.nextCursor}`, { headers }); ``` * Increase `limit` (default: 10, max varies by endpoint) to get more results per request * Continue until `nextCursor` is null * Cursors are opaque strings - don't parse or construct them See [Data Batches Guide](/guides/data-batches). ::: ::: details How do I optimize API response times? Follow these best practices: 1. **Use specific filters** - More parameters = smaller response ```javascript // Slow - gets everything /v2/events?leagueID=NBA // Fast - filtered /v2/events?leagueID=NBA&bookmakerID=draftkings&oddID=points-home-game-ml-home ``` 2. **Specify `oddID` when you only need specific markets** ```javascript /v2/events?leagueID=NBA&oddID=points-home-game-ml-home,points-all-game-ou-over ``` 3. **Avoid `includeAltLines=true` unless needed** - Alt lines dramatically increase response size 4. **Use appropriate date ranges** ```javascript /v2/events?leagueID=NBA&startsAfter=2025-01-15&startsBefore=2025-01-16 ``` 5. **Cache responses** - Pre-game odds don't change frequently See [Response Speed Guide](/guides/response-speed) for more tips. ::: ::: details How do I get alternate lines? Add `includeAltLines=true` to your request: ```javascript /v2/events?leagueID=NBA&includeAltLines=true ``` Alternate lines appear in the `byBookmaker` object with different spread/overUnder values: ```json { "byBookmaker": { "draftkings": { "odds": "-110", "spread": "-5.5", "altLines": [ { "spread": "-4.5", "odds": "-125" }, { "spread": "-6.5", "odds": "+105" } ] } } } ``` **Note:** Alt lines significantly increase response size. Only include when needed. ::: ## Pricing & Rate Limits ::: details What does "object" mean in pricing? An **object = 1 event (game)**, regardless of how many odds markets or bookmakers are included. You get ALL markets and ALL bookmakers for the same price - unlike competitors who charge per market or per bookmaker. This pricing model makes SportsGameOdds 60-80% cheaper than competitors. See [Rate Limiting](/info/rate-limiting) for details. ::: ::: details What are the rate limits for each plan? Rate limits vary by plan tier. The free Amateur plan includes 10 requests/min and 2,500 objects/month. Paid plans (Rookie, Pro, AllStar) offer higher limits up to unlimited. Check your current usage with `/account/usage`. See [Rate Limiting](/info/rate-limiting) for full details and [pricing page](https://sportsgameodds.com/pricing/) for plan comparison. ::: ::: details How often should I poll for odds updates? * **Pre-game (7+ days out)**: 15-30 minutes * **Pre-game (24-48 hours)**: 5-15 minutes * **Pre-game (game day)**: 2-5 minutes * **Live in-game**: 30-60 seconds * **Critical moments**: Use [streaming](/guides/realtime-streaming-api) (AllStar plan) Cache data longer for games far in the future. Many markets aren't offered until 24-48 hours before gametime. ::: ::: details How do I check my current usage? Call the `/account/usage` endpoint: ```javascript const response = await fetch("https://api.sportsgameodds.com/v2/account/usage", { headers: { "x-api-key": API_KEY }, }); const { data } = await response.json(); console.log(`Requests this minute: ${data.rateLimits["per-minute"].currentIntervalRequests}`); console.log(`Objects this month: ${data.rateLimits["per-month"].currentIntervalEntities}`); ``` This endpoint doesn't count against your rate limits. ::: ::: details What happens if I exceed my rate limit? You'll receive a `429 Too Many Requests` error. To handle this: 1. **Wait and retry** - Rate limits reset at the start of each interval 2. **Check usage** - Use `/account/usage` to see which limit you hit 3. **Optimize requests** - Use filters to reduce object count 4. **Upgrade plan** - If consistently hitting limits ```javascript if (response.status === 429) { const retryAfter = response.headers.get("Retry-After") || 60; await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000)); // Retry request } ``` ::: ::: details Why is SportsGameOdds cheaper than competitors? Our pricing model charges per **event**, not per market or bookmaker. A single NBA game with 100 markets × 20 bookmakers costs 1 unit with SportsGameOdds vs. potentially 2,000 units with per-market-per-bookmaker pricing. This means 60-80% lower costs, predictable billing, and no need to optimize which data you fetch. See [Rate Limiting](/info/rate-limiting) for details. ::: ## Real-Time & Streaming ::: details Do you offer webhooks? Real-time updates are available via our **Streaming API** on the AllStar plan. Instead of webhooks, we provide WebSocket connections via Pusher that notify you instantly when: * Odds change * Lines move * Events start/finish * Scores update For non-AllStar plans, we recommend polling at appropriate intervals. [Contact us](mailto:api@sportsgameodds.com) to upgrade. ::: ::: details How does the real-time streaming API work? The Streaming API uses WebSockets (via Pusher protocol) for instant updates: 1. **Get connection details** from `/v2/stream/events` 2. **Connect via WebSocket** using provided credentials 3. **Receive eventID notifications** when data changes 4. **Fetch updated data** via `/v2/events?eventIDs=...` ```javascript const axios = require("axios"); const Pusher = require("pusher-js"); // 1. Get stream info const streamInfo = await axios.get("/v2/stream/events", { headers: { "x-api-key": API_KEY }, params: { feed: "events:live" }, }); // 2. Connect to WebSocket const pusher = new Pusher(streamInfo.data.pusherKey, streamInfo.data.pusherOptions); // 3. Subscribe and listen pusher.subscribe(streamInfo.data.channel).bind("data", async (changedEvents) => { const eventIDs = changedEvents.map((e) => e.eventID).join(","); const updated = await axios.get(`/v2/events?eventIDs=${eventIDs}`, { headers }); // Process updated events }); ``` See [Real-Time Streaming Guide](/guides/realtime-streaming-api) for complete examples. ::: ::: details What feeds are available for streaming? Three feeds are available: `events:live` (all live games), `events:upcoming` (upcoming games by league), and `events:byid` (specific event updates). Streaming requires AllStar plan. See [Streaming Guide](/guides/realtime-streaming-api) for setup and examples. ::: ## Errors & Troubleshooting ::: details Why am I getting a 401 error? A `401 Unauthorized` error means authentication failed. Common causes: 1. **Missing API key header** ```javascript // Wrong - key in URL only fetch("/v2/events?apiKey=xxx"); // Correct - key in header fetch("/v2/events", { headers: { "x-api-key": "xxx" } }); ``` 2. **Whitespace in key** - Use `apiKey.trim()` 3. **Wrong header name** - Must be exactly `x-api-key` (lowercase) 4. **Old/revoked key** - Check your email or account dashboard ::: ::: details Why am I getting a 403 error? A `403 Forbidden` error means your key lacks permission. Causes: 1. **Cancelled subscription** - [Check subscription status](https://billing.stripe.com/p/login/5kA3eWePU7affkIdQQ) 2. **Failed payment** - Update billing info 3. **Feature not in plan** - e.g., streaming requires AllStar 4. **Wrong API version** - Keys default to v2; v1 requires special access Check the error `message` field for specifics. ::: ::: details Why am I getting a 429 error? A `429 Too Many Requests` error means you've exceeded rate limits: 1. **Wait** - Limits reset each minute/hour/month 2. **Check which limit** - Use `/account/usage` 3. **Optimize** - Use filters to reduce objects returned 4. **Upgrade** - If consistently hitting limits ```javascript // Check your limits const usage = await fetch("/v2/account/usage", { headers }); const data = await usage.json(); console.log(data.data.rateLimits); ``` See [Rate Limiting](/info/rate-limiting). ::: ::: details How should I handle errors in my code? Implement retry logic with exponential backoff: **JavaScript:** ```javascript async function fetchWithRetry(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch(url, options); if (response.ok) return response.json(); // Don't retry client errors (except 429) if (response.status >= 400 && response.status < 500 && response.status !== 429) { throw new Error(`Client error: ${response.status}`); } // Wait with exponential backoff const delay = Math.pow(2, i) * 1000 + Math.random() * 1000; await new Promise((resolve) => setTimeout(resolve, delay)); } throw new Error("Max retries exceeded"); } ``` **Python:** ```python import time import random def fetch_with_retry(url, headers, max_retries=3): for i in range(max_retries): response = requests.get(url, headers=headers) if response.ok: return response.json() # Don't retry client errors (except 429) if 400 <= response.status_code < 500 and response.status_code != 429: raise Exception(f"Client error: {response.status_code}") # Wait with exponential backoff delay = (2 ** i) + random.random() time.sleep(delay) raise Exception("Max retries exceeded") ``` See [Error Reference](/info/errors) for all error codes and handling examples. ::: ::: details How accurate is your data? We aggregate data from multiple premium providers with validation algorithms. Odds accuracy is 99.9%+ (direct bookmaker feeds), scores/stats are 99.5%+ (verified against official sources), and event metadata is 99.9%+ (verified against league sources). If you notice issues, [report them](https://sportsgameodds.com/contact-us/) with the `eventID`. ::: ## Advanced Topics ::: details Can I use the API in my frontend/browser? **Not recommended.** This exposes your API key publicly. Instead: **Option 1: Backend proxy (recommended)** ```javascript // Your backend (Node.js/Express) app.get("/api/odds", async (req, res) => { const response = await fetch("https://api.sportsgameodds.com/v2/events?leagueID=NBA", { headers: { "x-api-key": process.env.SGO_API_KEY }, }); res.json(await response.json()); }); // Your frontend fetch("/api/odds"); // Calls your backend, not SGO directly ``` **Option 2: Sync to database** * Server-side process syncs data to your database * Frontend queries your database instead See [Best Practices](/info/best-practices) for security guidelines. ::: ::: details How do I get a draw-no-bet line? A draw-no-bet line functions as a moneyline where a draw returns your stake. Look for: * `betTypeID`: `ml` * `sideID`: `home` or `away` * `periodID`: `reg` (regulation only, common for soccer) ```javascript /v2/events?leagueID=EPL&betTypeID=ml&periodID=reg ``` For soccer, if no `reg` period odds exist, use `game` period odds as fallback. ::: ::: details Why don't I see players (lineups) in Event data? Players appear in event data based on odds availability: 1. **Future events**: We add players when sportsbooks start offering props on them (more accurate than roster-based lineups) 2. **Smaller leagues**: Some leagues may not have player data. Contact us to add support. If you need lineups for a specific event, contact us with the `eventID`. ::: ::: details Where do I get season stats? We don't currently provide a dedicated season stats endpoint. However, you can: 1. **Calculate yourself**: Query `/events` for completed games and aggregate stats 2. **Use historical data**: Sum player/team stats across events ```javascript // Get completed games for a player /v2/events?leagueID=NBA&playerID=LEBRON_JAMES_NBA&finalized=true&startsAfter=2024-10-01 ``` Season stats endpoint is on our roadmap. [Contact us](https://sportsgameodds.com/contact-us/) for specific needs. ::: ::: details How do I get consensus/average odds? Consensus odds are included in every response via `fairOdds` (juice removed) and `bookOdds` (with juice). These are calculated automatically using median values across all bookmakers. See [Consensus Odds](/info/consensus-odds) for the calculation methodology. ::: ::: details What if I need a league or bookmaker you don't list? We can add additional leagues, regional bookmakers, and exchange data through custom plans. Most additions can be completed within 1-2 weeks. [Contact us](https://sportsgameodds.com/contact-us/) to discuss your needs. ::: ::: details How fast do you update odds compared to bookmaker sites? The speed at which you will receive updates can vary based on your plan as well as certain factors related to the event itself. For example, some events in the far future may receive updates less frequently than events that are happening right now. However, on average, we update odds data every 30-60 seconds. ::: *** ::: tip Need Help? [Email](mailto:api@sportsgameodds.com) · Chat · [Discord](https://discord.gg/HhP9E7ZZaE) ::: --- --- url: /docs/v2/endpoints/getUsage.md description: >- Check your API key rate limits and current usage. Returns requests per minute, objects per month, and remaining quota by time interval. --- --- --- url: /docs/v2/endpoints/getEvents.md description: >- Retrieve sports events with live odds, scores, player props, and results. Filter by league, team, date, or odds market. Includes all bookmaker lines. --- --- --- url: /docs/v2/endpoints/getLeagues.md description: >- Retrieve all supported leagues with leagueID values. Filter by sportID. Returns NFL, NBA, MLB, NHL, EPL, and 50+ other leagues. --- --- --- url: /docs/v2/endpoints/getPlayers.md description: >- Retrieve player information including names, team, position, and identifiers. Filter by playerID, teamID, or leagueID. Supports pagination. --- --- --- url: /docs/v2/endpoints/getSports.md description: >- Retrieve all supported sports with their sportID values. Returns basketball, football, baseball, hockey, soccer, tennis, golf, MMA, and more. --- --- --- url: /docs/v2/endpoints/getStats.md description: >- Retrieve all supported statistics with statID values. Filter by sportID. Returns points, rebounds, assists, touchdowns, and sport-specific stats. --- --- --- url: /docs/v2/endpoints/getTeams.md description: >- Retrieve team information including names, colors, and identifiers. Filter by teamID, leagueID, or sportID. Supports pagination. --- --- --- url: /docs/v2/guides/handling-odds.md description: >- Access odds data from events, grade over/under and spread bets using closeOverUnder and score fields. JavaScript example for processing results. --- # Handling Odds ## 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 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`); }); }); ``` --- --- url: /docs/v2/basics.md description: >- Sports betting odds API with 80+ bookmakers, 55+ leagues. Pay per event, not per market. Get real-time odds, scores, and player props in one request. --- # Introduction 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 * **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 **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 * **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 * **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 ### 1. [Sign Up & Get Your API Key](https://sportsgameodds.com/pricing/) 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 ```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 Use our [SDK](/sdk), [examples](/examples/live-odds-tracker), and [guides](/guides/data-batches) to build faster. *** ::: tip Need Help? [FAQ](/faq) · [Email](mailto:api@sportsgameodds.com) · Chat · [Discord](https://discord.gg/HhP9E7ZZaE) ::: --- --- url: /docs/v2/data-types/leagues.md description: >- Complete list of leagueID values. NFL, NBA, MLB, NHL, EPL, La Liga, Champions League, NCAA, UFC, and 55+ leagues across all major sports. --- # Leagues 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`. > \[!TIP] > Not all leagues may be available to you depending on your subscription plan. Make an API request to the `/leagues` endpoint for the most up-to-date list of leagues available to your API key. 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](https://sportsgameodds.com/contact-us) to discuss your needs. --- --- url: /docs/v2/examples/live-odds-tracker.md description: >- Build a live odds tracker with the SportsGameOdds API. Detect line movement and price changes across bookmakers every 30 seconds. Full Node.js code. --- # Build a Live Odds Tracker Complete working example that tracks NBA odds every 30 seconds and detects line movement. ## What You'll 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 * Node.js 18+ * SportsGameOdds API key ([Get one free](https://sportsgameodds.com/pricing)) * Basic JavaScript knowledge ## Complete Code ### 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 ```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 ```bash export SPORTSGAMEODDS_KEY=your_api_key_here node tracker.js ``` ## 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 ### 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 ```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 ```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 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 ```javascript previousOdds[key] = { spread: currentBookmakerOdds.spread, overUnder: currentBookmakerOdds.overUnder, odds: currentBookmakerOdds.odds, lastUpdatedAt: currentBookmakerOdds.lastUpdatedAt, }; ``` ## Enhancements ### 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 ```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 ```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 ```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) 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](/guides/realtime-streaming-api) ## Troubleshooting ### "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) **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](/info/rate-limiting) ### 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 ### Combine with Other Examples * **[Arbitrage Calculator](/examples/arbitrage-calculator)** - Detect arb opportunities from line movements * **[Odds Comparison Dashboard](/examples/odds-comparison-dashboard)** - Visualize movements in real-time * **[Player Props Analyzer](/examples/player-props-analyzer)** - Track prop line movements ### Learn More * **[Understanding oddID](/v2/data-types/odds)** - Deep dive into odds structure * **[Best Practices](/info/best-practices)** - Optimize your polling strategy * **[SDK Guide](/sdk)** - Use our TypeScript/Python SDK for easier development --- --- url: /docs/v2/info/v1-to-v2.md description: >- Upgrade from API v1 to v2. Combined /events endpoint, new playerID format, deeplinks, altLines parameter, and field name changes explained. --- # API V1 to V2 Guide ## 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 ::: warning Important 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`. ```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) ```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 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 * `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 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. 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 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 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 ::: info Notice 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** 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** 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 ### 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 * `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) ### 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 Odds items now contain a human-readable `marketName` field. ### 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 These fields will show only when applicable ### 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 * For /events * default limit: 30 -> 10 * max limit: 300 -> 100 * For teams and players * default limit: 30 -> 50 * max limit: 300 -> 250 --- --- url: /docs/v2/examples/odds-comparison-dashboard.md description: >- Build an odds comparison dashboard with the SportsGameOdds API. Compare lines across bookmakers in real-time. Full Next.js TypeScript code with Tailwind. --- # Build an Odds Comparison Dashboard Create a React/Next.js dashboard that compares odds across multiple sportsbooks and highlights the best lines. ## What You'll 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 * Node.js 18+ * Basic React/Next.js knowledge * SportsGameOdds API key ([Get one free](https://sportsgameodds.com/pricing)) ## Complete Code ### Step 1: Create Next.js 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 Create `app/api/odds/route.ts`: ```typescript // app/api/odds/route.ts import { NextResponse } from "next/server"; 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 Create `components/OddsDashboard.tsx`: ```typescript // components/OddsDashboard.tsx 'use client'; import { useState, useEffect } from 'react'; 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 (
{/* Header */}

Odds Comparison

{/* League Selector */}
{['NBA', 'NFL', 'NHL', 'MLB'].map(l => ( ))}
{/* Last Update */} {lastUpdate && (
Last updated: {lastUpdate.toLocaleTimeString()}
)}
{/* Games */}
{events.map(event => { const spreadOdds = groupOddsByMarket(event.odds, 'sp'); const bestHomeSpread = findBestOdds(event.odds, 'sp', 'home'); const bestAwaySpread = findBestOdds(event.odds, 'sp', 'away'); return (
{/* Matchup */}
{event.teams.away.names.long} @ {event.teams.home.names.long}
{event.status?.startsAt ? new Date(event.status.startsAt).toLocaleString() : 'TBD'}
{/* Spread Odds Table */}
{Object.entries(spreadOdds).map(([bookmaker, odds]) => ( {/* Away Spread */} {/* Home Spread */} ))}
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 Summary */}
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 Update `app/page.tsx`: ```typescript // app/page.tsx import OddsDashboard from '@/components/OddsDashboard'; export default function Home() { return ; } ``` ### Step 5: Add Environment Variable Create `.env.local`: ```bash SPORTSGAMEODDS_KEY=your_api_key_here ``` ### Step 6: Run It ```bash npm run dev ``` Open `http://localhost:3000` in your browser ## 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 ### 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 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 ```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 ```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 ```typescript useEffect(() => { fetchOdds(); const interval = setInterval(fetchOdds, 30000); // Every 30s return () => clearInterval(interval); }, [league]); ``` ## Enhancements ### 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 ```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 ```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 ```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 ### Deploy to Vercel ```bash npm install -g vercel vercel ``` Add environment variable in Vercel dashboard: * `SPORTSGAMEODDS_KEY=your_api_key` ### Deploy to Netlify ```bash npm run build netlify deploy --prod --dir=.next ``` Add environment variable in Netlify settings. ## Troubleshooting ### "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 **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 **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 ### Combine with Other Examples * **[Live Odds Tracker](/examples/live-odds-tracker)** - Add real-time movement alerts * **[Arbitrage Calculator](/examples/arbitrage-calculator)** - Highlight arb opportunities in UI ### Learn More * **[Understanding oddID](/v2/data-types/odds)** - Decode odds structure * **[SDK Guide](/sdk)** - Use TypeScript SDK for type safety * **[Best Practices](/info/best-practices)** - Optimize performance --- --- url: /docs/v2/data-types/odds.md description: >- How oddID works in the SportsGameOdds API. Format is statID-statEntityID-periodID-betTypeID-sideID. Includes examples and field reference. --- # Odds 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 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 | 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 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 | 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) | ## 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. --- --- url: /docs/v2/guides/response-speed.md description: >- Reduce API response times with oddIDs filtering. Fetch only needed markets using PLAYER_ID wildcard and includeOpposingOddIDs parameter. --- # Improving Response Speed ::: info Note 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 * 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 --- --- url: /docs/v2/guides/data-batches.md description: >- Fetch large datasets with cursor pagination. Use nextCursor and limit parameters to iterate through events, teams, and players efficiently. --- # Getting Data in Batches 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 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 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 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 Let's take the following example, where we want to grab all unfinalized NBA events: ::: code-group ```js [Javascript] 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 [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 [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 [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 [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; ``` ::: --- --- url: /docs/v2/examples/parlay-builder.md description: >- Build a parlay calculator with the SportsGameOdds API. Calculate combined odds, payouts, and implied probability. Full Node.js code with round robin support. --- # Build a Parlay Calculator Build a parlay calculator that combines multiple bets and calculates total odds and payout. ## What You'll 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? 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 * Node.js 18+ * SportsGameOdds API key ([Get one free](https://sportsgameodds.com/pricing)) * Basic JavaScript knowledge ## Complete Code ### 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 ```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 ```bash export SPORTSGAMEODDS_KEY=your_api_key_here node calculator.js ``` ## 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 ### 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 ```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 ```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 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 ### 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 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 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 ```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 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 ### "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 **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 **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 ### 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 **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 **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 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 ### Combine with Other Examples * **[Live Odds Tracker](/examples/live-odds-tracker)** - Monitor parlay legs in real-time * **[Odds Comparison Dashboard](/examples/odds-comparison-dashboard)** - Find best odds for each leg ### Learn More * **[Understanding oddID](/v2/data-types/odds)** - Decode bet identifiers * **[Best Practices](/info/best-practices)** - Optimize your betting strategy * **[Glossary](/info/glossary)** - Learn all terminology --- --- url: /docs/v2/data-types/periods.md description: >- All periodID values for game segments. Full game, halves (1h, 2h), quarters (1q-4q), innings, periods, sets, rounds by sport. --- # Periods 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. ::: warning Deprecation Notice The `1ix5` periodID (1st 5 Innings) is being deprecated in favor of `1h` (1st Half) for Baseball. Planned deprecation date: September 30. Please migrate any usage of `1ix5` to `1h`. ::: --- --- url: /docs/v2/examples/player-props-analyzer.md description: >- Build a player props analyzer with the SportsGameOdds API. Compare bookmaker lines, find outliers, and identify value bets. Full Python code. --- # Build a Player Props Analyzer Analyze player prop bets by fetching props for a specific game and comparing lines across bookmakers. ## What You'll 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 * Python 3.8+ * SportsGameOdds API key ([Get one free](https://sportsgameodds.com/pricing)) * Basic Python knowledge ## Complete Code ### 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 ```python # analyzer.py import requests import os 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 **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 ``` 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 ### 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 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 ```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 ```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 ```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 **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 ### Track Props Over Time ```python import json 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 ```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 ```python import csv 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 ```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 ### "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 **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 **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 ### Combine with Other Examples * **[Live Odds Tracker](/examples/live-odds-tracker)** - Monitor prop line movement * **[Arbitrage Calculator](/examples/arbitrage-calculator)** - Find prop arbitrage ### Learn More * **[Understanding oddID](/v2/data-types/odds)** - Decode prop identifiers * **[Best Practices](/info/best-practices)** - Optimize queries --- --- url: /docs/v2/basics/quickstart.md description: >- Make your first SportsGameOdds API call with copy-paste code examples. Fetch live NFL, NBA, and MLB odds in JavaScript, Python, Ruby, PHP, or Java. --- # Get Your First Odds in 5 Minutes A complete, copy-paste tutorial to fetch live odds ## Step 1: Get Your API Key 1. Sign up at [sportsgameodds.com/pricing](https://sportsgameodds.com/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 1. Copy one of the examples below into a file 2. Replace `YOUR_API_KEY` with your actual API key 3. Run it! ::: code-group ```text [Browser URL] https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true ``` ```bash [cURL] curl -X GET "https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true" ``` ```javascript [JavaScript] (async () => { const response = await fetch("https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true"); const output = await response.json(); console.log(JSON.stringify(output.data, null, 2)); })(); ``` ```python [Python] import requests import json response = requests.get( 'https://api.sportsgameodds.com/v2/events', params={'leagueID': 'NBA,NFL,MLB', 'oddsAvailable': 'true', 'apiKey': 'YOUR_API_KEY'} ) output = response.json() print(json.dumps(output['data'], indent=2)) ``` ```ruby [Ruby] require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/events') uri.query = URI.encode_www_form({ leagueID: 'NFL,NBA,MLB', oddsAvailable: 'true', apiKey: 'YOUR_API_KEY' }) response = Net::HTTP.get_response(uri) output = JSON.parse(response.body) puts JSON.pretty_generate(output['data']) ``` ```php [PHP] ``` ```java [Java] import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class SportsGameOddsAPI { public static void main(String[] args) throws Exception { String url = "https://api.sportsgameodds.com/v2/events?" + "leagueID=NFL,NBA,MLB" + "&oddsAvailable=true" + "&apiKey=YOUR_API_KEY"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); } } ``` ::: > \[!TIP] If you don't want to write code, paste the "Browser URL" example into your browser ## Step 3: See Live Odds You'll get a response like this: ```json { "nextCursor": "n.1763687700000.4lronEc61vA9pcyqoRIa", "success": true, "data": [ { "eventID": "4lronEc61vA9pcyqoRIa", "sportID": "FOOTBALL", "leagueID": "NFL", "type": "match", "teams": { "home": { "teamID": "HOUSTON_TEXANS_NFL", "names": { "long": "Houston Texans", "medium": "Texans", "short": "HOU" } //.. more team details }, "away": { "teamID": "BUFFALO_BILLS_NFL", "names": { "long": "Buffalo Bills", "medium": "Bills", "short": "BUF" } //... more team details } }, "status": { "started": false, "completed": false, "cancelled": false, "ended": false, "live": false, "delayed": false, "currentPeriodID": "", "previousPeriodID": "", "displayShort": "", "displayLong": "Upcoming", "inBreak": false, "hardStart": true, "periods": { "started": [], "ended": [] }, "oddsPresent": true, "oddsAvailable": true, "finalized": false, "startsAt": "2025-11-21T01:15:00.000Z", "previousStartsAt": [] }, "info": { "seasonWeek": "NFL 25/26" }, "odds": { "passing_yards-JOSH_ALLEN_1_NFL-game-ou-over": { "oddID": "passing_yards-JOSH_ALLEN_1_NFL-game-ou-over", "opposingOddID": "passing_yards-JOSH_ALLEN_1_NFL-game-ou-under", "statID": "passing_yards", "statEntityID": "JOSH_ALLEN_1_NFL", "periodID": "game", "betTypeID": "ou", "sideID": "over", "playerID": "JOSH_ALLEN_1_NFL", "started": false, "ended": false, "cancelled": false, "bookOddsAvailable": true, "fairOddsAvailable": true, "fairOdds": "+100", "bookOdds": "-114", "fairOverUnder": "231", "bookOverUnder": "224.5", "openFairOdds": "+100", "openBookOdds": "-114", "openFairOverUnder": "231", "openBookOverUnder": "224.5", "scoringSupported": true, "byBookmaker": { "betmgm": { "odds": "-118", "overUnder": "224.5", "lastUpdatedAt": "2025-11-19T21:12:36.000Z", "available": true, "deeplink": "https://sports.nj.betmgm.com/en/sports?options=18503696-1407860184--325476953" }, "prizepicks": { "odds": "+100", "overUnder": "224.5", "lastUpdatedAt": "2025-11-19T21:09:53.000Z", "available": true, "deeplink": "https://app.prizepicks.com/?projections=7878799-o-224.5" } //... more bookmakers }, "marketName": "Josh Allen Passing Yards Over/Under" } //... more odds markets }, "links": { "bookmakers": { "espnbet": "https://espnbet.com/sport/football/organization/united-states/competition/nfl/event/2c83a542-357e-4e78-8efe-58129cb8cd37", "draftkings": "https://sportsbook.draftkings.com/event/32225621", "nordicbet": "https://www.nordicbet.com/en/sportsbook/american-football/nfl/nfl/houston-texans-buffalo-bills" // ...more deeplinks } }, "players": { "JOSH_ALLEN_1_NFL": { "playerID": "JOSH_ALLEN_1_NFL", "teamID": "BUFFALO_BILLS_NFL", "firstName": "Josh", "lastName": "Allen", "name": "Josh Allen", "nickname": "J.Allen" } // ... more players } } //... more events ] } ``` ### Understand the Response Let's break down what you just got: **Event Details:** * `eventID` - Unique identifier for this game * `teams.home.name` - Home team: Los Angeles Lakers * `teams.away.name` - Away team: Boston Celtics * `startTime` - When the game starts (UTC) **Odds Data:** Each odd is keyed by `oddID` which follows this pattern: ``` {statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID} ``` Note: The oddID does NOT include bookmakerID. Each oddID contains odds from ALL available bookmakers nested under the `byBookmaker` object. **Example:** `points-home-game-sp-home` means: * `points` - Stat being bet on (team points/score) * `home` - Entity (home team) * `game` - For the full game (not just a quarter/half) * `sp` - This is a point spread bet (betTypeID) * `home` - Side of the bet (home team) **The odds show:** * Each oddID contains odds from multiple bookmakers under `byBookmaker` * Lakers -5.5 @ -110 from DraftKings, -5.5 @ -112 from FanDuel * Celtics +5.5 @ -110 from DraftKings, +5.5 @ -108 from FanDuel * Lakers moneyline -220 from DraftKings, -225 from FanDuel * All odds values (spread, odds) are **strings**, not numbers ## Done! **Congratulations!** You just fetched live odds from the SportsGameOdds API. ## Next Steps Now that you've made your first successful call, explore what else you can do: ### 🚀 Build Something Real Start with these complete, working examples: * **[Live Odds Tracker](/examples/live-odds-tracker)** - Track line movement in real-time * **[Arbitrage Calculator](/examples/arbitrage-calculator)** - Find guaranteed profit opportunities * **[Odds Comparison Dashboard](/examples/odds-comparison-dashboard)** - Compare odds across bookmakers * **[Player Props Analyzer](/examples/player-props-analyzer)** - Analyze player prop bets * **[Parlay Builder](/examples/parlay-builder)** - Build parlay bet calculators ### 📖 Learn the Basics * **[Understanding Odds Data](/data-types/odds)** - Deep dive into the oddID structure * **[Sports Betting Glossary](/info/glossary)** - Learn all the terminology * **[Setup](/basics/setup)** - More about how to get started with the API * **[Rate Limiting](/info/rate-limiting)** - Understand request limits ### 🛠️ Use Our SDK Instead of raw HTTP requests, use our official SDKs with built-in pagination, retries, and type safety: ```bash # TypeScript/JavaScript npm install sports-odds-api yarn add sports-odds-api pnpm add sports-odds-api # Python pip install sports-odds-api poetry add sports-odds-api # Ruby gem install sports-odds-api bundle add sports-odds-api # Go go get github.com/SportsGameOdds/sports-odds-api-go ``` [Full SDK Guide →](/sdk) ### 🔍 Explore All Endpoints * **[API Reference](/reference)** - Interactive documentation for all endpoints * **[Data Explorer](/explorer)** - Browse available leagues, bookmakers, and markets ## Common Next Questions ::: details How do I get odds from multiple bookmakers? By default, the API returns odds from all available bookmakers. Each bookmaker appears in the oddID: ```javascript // Filter to specific bookmaker fetch("/v2/events?leagueID=NBA&bookmakerID=draftkings"); // Or get all and filter in code Object.entries(event.odds).forEach(([oddID, odd]) => { if (odd.bookmakerID === "draftkings") { console.log("DraftKings odds:", odd); } }); ``` See [all available bookmakers →](/data-types/bookmakers) ::: ::: details How do I get live scores? Scores are included automatically in the same response for live games: ```javascript const response = await fetch("/v2/events?leagueID=NBA&live=true", { headers: { "x-api-key": API_KEY } }); const data = await response.json(); data.data.forEach((event) => { console.log(`${event.teams.away.name}: ${event.scores.away}`); console.log(`${event.teams.home.name}: ${event.scores.home}`); }); ``` No separate API needed - odds, scores, and stats all in one response! ::: ::: details How do I get historical data? Use the `startsAfter` and `startsBefore` parameters: ```javascript // Get all NBA games from December 2024 fetch("/v2/events?leagueID=NBA&startsAfter=2024-12-01&startsBefore=2024-12-31"); ``` Historical data from 2024+ is included in all plans. Extended historical data (2020+) available on Pro/AllStar plans. ::: ::: details How often should I poll for live odds? **Recommended intervals:** * **Pre-match:** Every 5-15 minutes (odds change slowly) * **Live:** Every 30-60 seconds (good balance) * **Critical moments:** Use real-time streaming (AllStar plan) Polling faster than 30 seconds wastes rate limits without meaningful updates. See [Best Practices Guide →](/info/best-practices) ::: ::: details What counts as an "object" in pricing? An **object** = 1 event (game). Each event includes ALL odds markets and ALL bookmakers by default. **Key difference from other APIs:** You pay per event, not per market or per bookmaker. **Example:** * 1 NBA game with 100 odds markets across 20 bookmakers = **1 object** * 10 NBA games = **10 objects** * Same 10 games polled twice = **20 objects** (charged per request) This makes SportsGameOdds 60-80% cheaper than competitors who charge per market or per bookmaker. Learn more: [Rate Limiting →](/info/rate-limiting) ::: *** ::: tip Need Help? [FAQ](/faq) · [Email](mailto:api@sportsgameodds.com) · Chat · [Discord](https://discord.gg/HhP9E7ZZaE) ::: **Ready to build something amazing?** Pick a recipe and start coding! 🚀 --- --- url: /docs/v2/info/rate-limiting.md description: >- Rate limits for Amateur, Rookie, Pro, and AllStar plans. Check usage via /account/usage endpoint. Strategies to avoid 429 errors and optimize requests. --- # Rate Limiting 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 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 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 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 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 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 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" } } } } ``` --- --- url: /docs/v2/guides/realtime-streaming-api.md description: >- WebSocket streaming for live odds updates via Pusher. Connect to events:live, events:upcoming, or events:byid feeds. AllStar plan required. --- # Real-Time Event Streaming API ::: warning Access Required 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. ::: ::: warning Beta Feature 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 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 ::: tip Rate Limits Your API key will have limits on concurrent streams. ::: ## 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` | ::: info Feeds 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 Here's the minimal code to connect to live events: ::: code-group ```js [JavaScript/Node.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 [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 [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 The `/v2/stream/events` endpoint returns: ```json { "success": true, "data": [ , // ... more events ], "pusherKey": , "pusherOptions": , "channel": } ``` ## 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 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 ### 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 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 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 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 **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. --- --- url: /docs/v2/sdk.md description: >- Official SportsGameOdds SDKs with auto-pagination, type safety, and error handling. Install guides and code examples for all supported languages. --- # SportsGameOdds SDK Guide 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 Install the SDK using your language's package manager: ::: code-group ```bash [TypeScript] # Using npm npm install sports-odds-api # Using yarn yarn add sports-odds-api # Using pnpm pnpm add sports-odds-api ``` ```bash [Python] # Using pip pip install sports-odds-api # Using poetry poetry add sports-odds-api ``` ```bash [Ruby] # Using bundler bundle add sports-odds-api # Or add to Gemfile gem "sports-odds-api", "~> 1.0" ``` ```bash [Go] go get github.com/SportsGameOdds/sports-odds-api-go ``` ```groovy [Java] // 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' ``` ::: ## Quick Start Get up and running in under 60 seconds: ::: code-group ```ts [TypeScript] import SportsGameOdds from "sports-odds-api"; const client = new SportsGameOdds({ apiKeyHeader: process.env.SPORTS_ODDS_API_KEY_HEADER, }); // Fetch first page of events const page = await client.events.get({ limit: 5 }); console.log(`Found ${page.data.length} events`); // Access event data const event = page.data[0]; console.log(`Event: ${event.teams.away.name} @ ${event.teams.home.name}`); ``` ```python [Python] import os from sports_odds_api import SportsGameOdds client = SportsGameOdds( api_key_param=os.environ.get("SPORTS_ODDS_API_KEY_HEADER") ) # Fetch first page of events page = client.events.get(limit=5) print(f"Found {len(page.data)} events") # Access event data event = page.data[0] print(f"Event: {event.teams.away.name} @ {event.teams.home.name}") ``` ```ruby [Ruby] require "sports_odds_api" client = SportsOddsAPI::Client.new( api_key_param: ENV["SPORTS_ODDS_API_KEY_HEADER"] ) # Fetch first page of events page = client.events.get(limit: 5) puts "Found #{page.data.length} events" # Access event data event = page.data[0] puts "Event: #{event.teams.away.name} @ #{event.teams.home.name}" ``` ```go [Go] package main import ( "context" "fmt" "os" sportsoddsapi "github.com/SportsGameOdds/sports-odds-api-go" "github.com/SportsGameOdds/sports-odds-api-go/option" ) func main() { client := sportsoddsapi.NewClient( option.WithAPIKeyParam(os.Getenv("SPORTS_ODDS_API_KEY_HEADER")), ) // Fetch first page of events ctx := context.Background() page, _ := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ Limit: sportsoddsapi.Float(5), }) fmt.Printf("Found %d events\n", len(page.Data)) // Access event data event := page.Data[0] fmt.Printf("Event: %s @ %s\n", event.Teams.Away.Name, event.Teams.Home.Name) } ``` ```java [Java] import com.sportsgameodds.api.client.SportsGameOddsClient; import com.sportsgameodds.api.client.okhttp.SportsGameOddsOkHttpClient; import com.sportsgameodds.api.models.events.EventGetPage; import com.sportsgameodds.api.models.events.EventGetParams; public class QuickStart { public static void main(String[] args) { SportsGameOddsClient client = SportsGameOddsOkHttpClient.builder() .apiKeyHeader(System.getenv("SPORTS_ODDS_API_KEY_HEADER")) .build(); // Fetch first page of events EventGetParams request = EventGetParams.builder() .limit(5.0) .build(); EventGetPage page = client.events().get(request); System.out.println("Found " + page.items().size() + " events"); // Access event data page.items().get(0).teams().ifPresent(teams -> { System.out.println("Event: " + teams.away().name().orElse("N/A") + " @ " + teams.home().name().orElse("N/A")); }); } } ``` ::: ::: tip Complete Examples View complete working examples in our GitHub repositories: * [TypeScript Examples](https://github.com/SportsGameOdds/sports-odds-api-typescript/tree/main/examples) * [Python Examples](https://github.com/SportsGameOdds/sports-odds-api-python/tree/main/examples) * [Ruby Examples](https://github.com/SportsGameOdds/sports-odds-api-ruby/tree/main/examples) * [Go Examples](https://github.com/SportsGameOdds/sports-odds-api-go/tree/main/examples) * [Java Examples](https://github.com/SportsGameOdds/sports-odds-api-java/tree/main/examples) ::: ## Pagination The SportsGameOdds API returns data in pages to ensure optimal performance. Our SDKs provide both automatic and manual pagination options. ### Auto-Pagination The easiest way to iterate through all results is using auto-pagination, which automatically fetches subsequent pages as you iterate: ::: code-group ```ts [TypeScript] // Iterate through all events using async iteration for await (const event of client.events.get({ limit: 100 })) { console.log(`${event.eventID}: ${event.activity}`); // Process each event // SDK automatically fetches next page when needed } ``` ```python [Python] # Sync auto-pagination for event in client.events.get(limit=100).auto_paging_iter(): print(f"{event.eventID}: {event.activity}") # SDK automatically fetches next page when needed # Async auto-pagination async for event in client.events.get(limit=100).auto_paging_iter(): print(f"{event.eventID}: {event.activity}") ``` ```ruby [Ruby] # Auto-pagination with block page = client.events.get(limit: 100) page.auto_paging_each do |event| puts "#{event.event_id}: #{event.activity}" # SDK automatically fetches next page when needed end ``` ```go [Go] // Auto-pagination with iterator iter := client.Events.GetAutoPaging(ctx, sportsoddsapi.EventGetParams{ Limit: sportsoddsapi.Float(100), }) for iter.Next() { event := iter.Current() fmt.Printf("%s: %s\n", event.EventID, event.Activity) // SDK automatically fetches next page when needed } if err := iter.Err(); err != nil { // Handle error } ``` ```java [Java] // Note: Java SDK currently supports manual pagination // See manual pagination example below EventGetParams request = EventGetParams.builder() .limit(100.0) .build(); EventGetPage page = client.events().get(request); for (Event event : page.items()) { System.out.println(event.eventId().orElse("") + ": " + event.activity().orElse("")); } ``` ::: ### Manual Pagination For more control over pagination, you can manually navigate through pages: ::: code-group ```ts [TypeScript] let page = await client.events.get({ limit: 100 }); // Process first page console.log(`Page 1: ${page.data.length} events`); // Check if there are more pages while (page.hasNextPage()) { page = await page.getNextPage(); console.log(`Next page: ${page.data.length} events`); } ``` ```python [Python] page = client.events.get(limit=100) # Process first page print(f"Page 1: {len(page.data)} events") # Check if there are more pages while page.has_next_page(): page = page.get_next_page() print(f"Next page: {len(page.data)} events") ``` ```ruby [Ruby] page = client.events.get(limit: 100) # Process first page puts "Page 1: #{page.data.length} events" # Check if there are more pages while page.next_page? page = page.next_page puts "Next page: #{page.data.length} events" end ``` ```go [Go] page, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ Limit: sportsoddsapi.Float(100), }) if err != nil { // Handle error } // Process first page fmt.Printf("Page 1: %d events\n", len(page.Data)) // Navigate through pages manually for { nextPage, err := page.GetNextPage() if err != nil || nextPage == nil { break } page = nextPage fmt.Printf("Next page: %d events\n", len(page.Data)) } ``` ```java [Java] EventGetParams request = EventGetParams.builder() .limit(100.0) .build(); EventGetPage page = client.events().get(request); // Process first page System.out.println("Page 1: " + page.items().size() + " events"); // Navigate through pages using cursor while (page.nextCursor().isPresent()) { EventGetParams nextRequest = EventGetParams.builder() .limit(100.0) .cursor(page.nextCursor().get()) .build(); page = client.events().get(nextRequest); System.out.println("Next page: " + page.items().size() + " events"); } ``` ::: ## Filtering and Query Parameters Filter events by league, date, odds availability, and more using query parameters: ::: code-group ```ts [TypeScript] // Filter NFL events with available odds const nflEvents = await client.events.get({ leagueID: "NFL", oddsAvailable: true, finalized: false, limit: 50, }); console.log(`Found ${nflEvents.data.length} NFL events with odds`); // Filter events by specific event IDs const specificEvents = await client.events.get({ eventIDs: "1234567890,0987654321", }); ``` ```python [Python] # Filter NFL events with available odds nfl_events = client.events.get( leagueID='NFL', oddsAvailable=True, finalized=False, limit=50 ) print(f"Found {len(nfl_events.data)} NFL events with odds") # Filter events by specific event IDs specific_events = client.events.get( eventIDs='1234567890,0987654321' ) ``` ```ruby [Ruby] # Filter NFL events with available odds nfl_events = client.events.get( leagueID: 'NFL', oddsAvailable: true, finalized: false, limit: 50 ) puts "Found #{nfl_events.data.length} NFL events with odds" # Filter events by specific event IDs specific_events = client.events.get( eventIDs: '1234567890,0987654321' ) ``` ```go [Go] // Filter NFL events with available odds nflEvents, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ LeagueID: sportsoddsapi.String("NFL"), OddsAvailable: sportsoddsapi.Bool(true), Finalized: sportsoddsapi.Bool(false), Limit: sportsoddsapi.Float(50), }) if err == nil { fmt.Printf("Found %d NFL events with odds\n", len(nflEvents.Data)) } // Filter events by specific event IDs specificEvents, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ EventIDs: sportsoddsapi.String("1234567890,0987654321"), }) ``` ```java [Java] // Filter NFL events with available odds EventGetParams request = EventGetParams.builder() .leagueId("NFL") .oddsAvailable(true) .finalized(false) .limit(50.0) .build(); EventGetPage nflEvents = client.events().get(request); System.out.println("Found " + nflEvents.items().size() + " NFL events with odds"); // Filter events by specific event IDs EventGetParams specificRequest = EventGetParams.builder() .eventIds("1234567890,0987654321") .build(); EventGetPage specificEvents = client.events().get(specificRequest); ``` ::: ::: tip Available Filters Common query parameters include: * `leagueID` - Filter by league (e.g., 'NFL', 'NBA', 'MLB') * `eventIDs` - Comma-separated list of specific event IDs * `oddsAvailable` - Only return events with odds data * `live` - Filter by live status * `finalized` - Filter by finalized status * `limit` - Number of results per page (default: 100, max: 500) See the [API Reference](/v2/reference) for a complete list of available parameters. ::: ## Error Handling Handle API errors gracefully with proper exception handling: ::: code-group ```ts [TypeScript] import SportsGameOdds from "sports-odds-api"; try { const page = await client.events.get(); console.log(`Successfully fetched ${page.data.length} events`); } catch (err) { if (err instanceof SportsGameOdds.APIError) { console.error("API Error:", { status: err.status, message: err.message, headers: err.headers, }); } else { console.error("Unexpected error:", err); } } ``` ```python [Python] from sports_odds_api import SportsGameOdds from sports_odds_api.errors import ( APIError, AuthenticationError, APIStatusError, RateLimitError, ) try: page = client.events.get() print(f"Successfully fetched {len(page.data)} events") except AuthenticationError as e: print(f"Authentication failed: {e}") except RateLimitError as e: print(f"Rate limit exceeded: {e}") except APIStatusError as e: print(f"API error {e.status_code}: {e.message}") except APIError as e: print(f"API error: {e}") ``` ```ruby [Ruby] require "sports_odds_api" begin page = client.events.get puts "Successfully fetched #{page.data.length} events" rescue SportsOddsAPI::Errors::AuthenticationError => e puts "Authentication failed: #{e.message}" rescue SportsOddsAPI::Errors::RateLimitError => e puts "Rate limit exceeded: #{e.message}" rescue SportsOddsAPI::Errors::NotFoundError => e puts "Resource not found: #{e.message}" rescue SportsOddsAPI::Errors::APIError => e puts "API error: #{e.message}" end ``` ```go [Go] import ( "errors" "fmt" sportsoddsapi "github.com/SportsGameOdds/sports-odds-api-go" ) page, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{}) if err != nil { // Check for specific error types var apiErr *sportsoddsapi.Error if errors.As(err, &apiErr) { fmt.Printf("API Error %d: %s\n", apiErr.StatusCode, apiErr.Message) } else { fmt.Printf("Unexpected error: %v\n", err) } return } fmt.Printf("Successfully fetched %d events\n", len(page.Data)) ``` ```java [Java] import com.sportsgameodds.api.errors.SportsGameOddsException; try { EventGetPage page = client.events().get(); System.out.println("Successfully fetched " + page.items().size() + " events"); } catch (SportsGameOddsException e) { System.err.println("API Error: " + e.getMessage()); // Access additional error details if available if (e.statusCode().isPresent()) { System.err.println("Status Code: " + e.statusCode().get()); } } catch (Exception e) { System.err.println("Unexpected error: " + e.getMessage()); } ``` ::: ::: warning Common Error Types * **AuthenticationError (401)** - Invalid or missing API key * **PermissionDeniedError (403)** - Feature not available on your plan * **NotFoundError (404)** - Resource not found * **RateLimitError (429)** - Rate limit exceeded * **APIStatusError (5xx)** - Server error ::: ## Timeout and Retry Configuration Configure timeouts and retry behavior for robust applications: ::: code-group ```ts [TypeScript] // Global configuration (applies to all requests) const client = new SportsGameOdds({ apiKeyHeader: process.env.SPORTS_ODDS_API_KEY_HEADER, timeout: 20 * 1000, // 20 seconds maxRetries: 3, // Retry up to 3 times }); // Per-request configuration (overrides global settings) const page = await client.events.get({ limit: 100, timeout: 5 * 1000, // 5 seconds for this request maxRetries: 1, // Retry once for this request }); ``` ```python [Python] import httpx # Global configuration (applies to all requests) client = SportsGameOdds( api_key_param=os.environ.get("SPORTS_ODDS_API_KEY_HEADER"), timeout=httpx.Timeout(20.0), # 20 seconds max_retries=3, # Retry up to 3 times ) # Per-request configuration page = client.events.get( limit=100, timeout=5.0, # 5 seconds for this request ) ``` ```ruby [Ruby] # Global configuration (applies to all requests) client = SportsOddsAPI::Client.new( api_key_param: ENV["SPORTS_ODDS_API_KEY_HEADER"], timeout: 20, # 20 seconds max_retries: 3, # Retry up to 3 times ) # Configuration applies to all requests from this client page = client.events.get(limit: 100) ``` ```go [Go] import ( "context" "time" "github.com/SportsGameOdds/sports-odds-api-go/option" ) // Global configuration client := sportsoddsapi.NewClient( option.WithAPIKeyParam(os.Getenv("SPORTS_ODDS_API_KEY_HEADER")), option.WithMaxRetries(3), ) // Per-request timeout using context ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() page, err := client.Events.Get( ctx, sportsoddsapi.EventGetParams{Limit: sportsoddsapi.Float(100)}, option.WithRequestTimeout(5*time.Second), ) ``` ```java [Java] import java.time.Duration; // Global configuration SportsGameOddsClient client = SportsGameOddsOkHttpClient.builder() .apiKeyHeader(System.getenv("SPORTS_ODDS_API_KEY_HEADER")) .timeout(Duration.ofSeconds(20)) .maxRetries(3) .build(); // Configuration applies to all requests EventGetParams request = EventGetParams.builder() .limit(100.0) .build(); EventGetPage page = client.events().get(request); ``` ::: ::: tip Best Practices * Set reasonable timeouts to avoid hanging requests * Configure retries for transient network issues * Use shorter timeouts for time-sensitive operations * Monitor retry counts in production ::: ## Working with Odds Data Access and parse odds data efficiently: ::: code-group ```ts [TypeScript] // Fetch events with odds const events = await client.events.get({ leagueID: "NFL", oddsAvailable: true, limit: 10, }); // Parse odds data for (const event of events.data) { console.log(`\nEvent: ${event.teams.away.name} @ ${event.teams.home.name}`); if (event.odds) { // Odds is an object keyed by oddID for (const [oddID, odd] of Object.entries(event.odds)) { console.log(` ${odd.betTypeID}: ${odd.bookmakerID}`); // Access line and price data if (odd.line) console.log(` Line: ${odd.line}`); if (odd.price) console.log(` Price: ${odd.price}`); } } } ``` ```python [Python] # Fetch events with odds events = client.events.get( leagueID='NFL', oddsAvailable=True, limit=10 ) # Parse odds data for event in events.data: print(f"\nEvent: {event.teams.away.name} @ {event.teams.home.name}") if event.odds: # Odds is a dict keyed by oddID for odd_id, odd in event.odds.items(): print(f" {odd.betTypeID}: {odd.bookmakerID}") # Access line and price data if odd.line: print(f" Line: {odd.line}") if odd.price: print(f" Price: {odd.price}") ``` ```ruby [Ruby] # Fetch events with odds events = client.events.get( leagueID: 'NFL', oddsAvailable: true, limit: 10 ) # Group odds by betTypeID for easier analysis events.data.each do |event| puts "\nEvent: #{event.teams.away.name} @ #{event.teams.home.name}" next unless event.odds # Group by bet type odds_by_type = {} event.odds.each do |odd_id, odd| bet_type_id = odd.bet_type_id odds_by_type[bet_type_id] ||= [] odds_by_type[bet_type_id] << odd end puts " Available bet types: #{odds_by_type.keys.join(', ')}" puts " Total markets: #{event.odds.length}" end ``` ```go [Go] // Fetch events with odds events, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ LeagueID: sportsoddsapi.String("NFL"), OddsAvailable: sportsoddsapi.Bool(true), Limit: sportsoddsapi.Float(10), }) if err != nil { // Handle error } // Parse odds data for _, event := range events.Data { fmt.Printf("\nEvent: %s @ %s\n", event.Teams.Away.Name, event.Teams.Home.Name) if event.Odds != nil { // Odds is a map keyed by oddID for oddID, odd := range event.Odds { fmt.Printf(" %s: %s\n", odd.BetTypeID, odd.BookmakerID) // Access line and price data if odd.Line != nil { fmt.Printf(" Line: %f\n", *odd.Line) } if odd.Price != nil { fmt.Printf(" Price: %d\n", *odd.Price) } } } } ``` ```java [Java] // Fetch events with odds EventGetParams request = EventGetParams.builder() .leagueId("NFL") .oddsAvailable(true) .limit(10.0) .build(); EventGetPage events = client.events().get(request); // Parse odds data for (Event event : events.items()) { event.teams().ifPresent(teams -> { System.out.println("\nEvent: " + teams.away().name().orElse("N/A") + " @ " + teams.home().name().orElse("N/A")); }); event.odds().ifPresent(odds -> { // Odds contains additional properties as a Map Map oddsData = odds._additionalProperties(); System.out.println(" Total markets: " + oddsData.size()); // Parse each odd oddsData.forEach((oddID, oddValue) -> { // Access odd properties System.out.println(" Market: " + oddID); }); }); } ``` ::: ## Real-Time Streaming (AllStar Plan) Subscribe to real-time updates using our streaming API (requires AllStar plan): ::: code-group ```ts [TypeScript] // Get stream connection info const streamInfo = await client.stream.events({ feed: "events:live", }); console.log("Stream channel:", streamInfo.channel); console.log("Initial events:", streamInfo.data.length); // Use Pusher client to connect to WebSocket import Pusher from "pusher-js"; const pusher = new Pusher(streamInfo.pusherKey, { cluster: streamInfo.pusherOptions.cluster, }); const channel = pusher.subscribe(streamInfo.channel); channel.bind("update", (data) => { console.log("Event update:", data); }); ``` ```python [Python] # Get stream connection info stream_info = client.stream.events(feed='events:live') print(f"Stream channel: {stream_info.channel}") print(f"Initial events: {len(stream_info.data)}") # Use pusher client to connect to WebSocket import pysher pusher = pysher.Pusher( stream_info.pusher_key, cluster=stream_info.pusher_options['cluster'] ) def update_handler(data): print(f"Event update: {data}") pusher.connection.bind('pusher:connection_established', lambda data: None) pusher.subscribe(stream_info.channel).bind('update', update_handler) pusher.connect() ``` ```ruby [Ruby] # Get stream connection info stream_info = client.stream.events(feed: 'events:live') puts "Stream channel: #{stream_info.channel}" puts "Initial events: #{stream_info.data.length}" # Use pusher client to connect to WebSocket require 'pusher-client' pusher = PusherClient::Socket.new(stream_info.pusher_key, { cluster: stream_info.pusher_options['cluster'] }) pusher.subscribe(stream_info.channel) pusher[stream_info.channel].bind('update') do |data| puts "Event update: #{data}" end pusher.connect(true) ``` ```go [Go] // Get stream connection info streamInfo, err := client.Stream.Events(ctx, sportsoddsapi.StreamEventsParams{ Feed: sportsoddsapi.String("events:live"), }) if err != nil { // Handle error (may be permission error if not on AllStar plan) } fmt.Printf("Stream channel: %s\n", streamInfo.Channel) fmt.Printf("Initial events: %d\n", len(streamInfo.Data)) // Use Pusher client library to connect to WebSocket // Implementation depends on chosen Go Pusher client library ``` ```java [Java] // Get stream connection info StreamEventsParams request = StreamEventsParams.builder() .feed("events:live") .build(); StreamEventsResponse streamInfo = client.stream().events(request); streamInfo.channel().ifPresent(channel -> System.out.println("Stream channel: " + channel)); streamInfo.data().ifPresent(data -> System.out.println("Initial events: " + data.size())); // Use Pusher client library to connect to WebSocket // Implementation depends on chosen Java Pusher client library ``` ::: ::: warning AllStar Plan Required Real-time streaming is only available on the AllStar plan. Requests will return a `403 Permission Denied` error if not subscribed to the AllStar plan. ::: ## Type Safety and IDE Support All SDKs provide full type definitions for excellent IDE autocomplete and type checking: ::: code-group ```ts [TypeScript] import SportsGameOdds from "sports-odds-api"; // Full TypeScript types available const client = new SportsGameOdds({ apiKeyHeader: process.env.SPORTS_ODDS_API_KEY_HEADER, }); // Type-safe event access with autocomplete const page = await client.events.get(); const event: SportsGameOdds.Event = page.data[0]; // IDE autocomplete for all properties console.log(event.eventID); console.log(event.leagueID); console.log(event.teams.away.name); console.log(event.status.startsAt); ``` ```python [Python] from sports_odds_api import SportsGameOdds from sports_odds_api.types import Event client = SportsGameOdds( api_key_param=os.environ.get("SPORTS_ODDS_API_KEY_HEADER") ) # Type hints with Pydantic models page = client.events.get() event: Event = page.data[0] # Convert to dict or JSON event_dict = event.to_dict() event_json = event.to_json() # IDE autocomplete for all properties print(event.eventID) print(event.leagueID) print(event.teams.away.name) ``` ```ruby [Ruby] require "sports_odds_api" client = SportsOddsAPI::Client.new( api_key_param: ENV["SPORTS_ODDS_API_KEY_HEADER"] ) # Ruby objects with attribute accessors page = client.events.get event = page.data[0] # Access properties with Ruby conventions puts event.event_id puts event.league_id puts event.teams.away.name puts event.status.starts_at ``` ```go [Go] import sportsoddsapi "github.com/SportsGameOdds/sports-odds-api-go" // Strongly typed structs page, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{}) if err != nil { // Handle error } event := page.Data[0] // Compile-time type safety fmt.Println(event.EventID) fmt.Println(event.LeagueID) fmt.Println(event.Teams.Away.Name) fmt.Println(event.Status.StartsAt) ``` ```java [Java] import com.sportsgameodds.api.models.events.Event; // Strongly typed with Optional support EventGetPage page = client.events().get(); Event event = page.items().get(0); // Type-safe access with Optional chaining event.eventId().ifPresent(id -> System.out.println(id)); event.leagueId().ifPresent(league -> System.out.println(league)); event.teams().ifPresent(teams -> { teams.away().name().ifPresent(name -> System.out.println(name)); }); ``` ::: ## Advanced Usage ### Accessing Raw Response Data Sometimes you need access to the raw HTTP response: ::: code-group ```ts [TypeScript] // Get only the raw response const response = await client.events.get().asResponse(); console.log("Status:", response.status); console.log("Headers:", response.headers); // Get both parsed data and raw response const { data: page, response: raw } = await client.events.get().withResponse(); console.log(`Fetched ${page.data.length} events`); console.log("Response status:", raw.status); ``` ```python [Python] # Python SDK returns parsed data by default # Access response metadata through the client if needed page = client.events.get() print(f"Fetched {len(page.data)} events") ``` ```ruby [Ruby] # Ruby SDK returns parsed data by default # Access response through the page object if available page = client.events.get puts "Fetched #{page.data.length} events" ``` ```go [Go] // Go SDK returns both data and error page, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{}) if err != nil { // Error contains HTTP status information if applicable var apiErr *sportsoddsapi.Error if errors.As(err, &apiErr) { fmt.Printf("Status Code: %d\n", apiErr.StatusCode) } return } fmt.Printf("Fetched %d events\n", len(page.Data)) ``` ```java [Java] // Java SDK returns parsed data // Access response metadata through exceptions if needed try { EventGetPage page = client.events().get(); System.out.println("Fetched " + page.items().size() + " events"); } catch (SportsGameOddsException e) { e.statusCode().ifPresent(code -> System.out.println("Status Code: " + code)); } ``` ::: ### Concurrent Requests Make multiple requests concurrently for better performance: ::: code-group ```ts [TypeScript] // Fetch multiple leagues concurrently const [nflEvents, nbaEvents, mlbEvents] = await Promise.all([ client.events.get({ leagueID: "NFL", limit: 50 }), client.events.get({ leagueID: "NBA", limit: 50 }), client.events.get({ leagueID: "MLB", limit: 50 }), ]); console.log(`NFL: ${nflEvents.data.length} events`); console.log(`NBA: ${nbaEvents.data.length} events`); console.log(`MLB: ${mlbEvents.data.length} events`); ``` ```python [Python] import asyncio # Async concurrent requests async def fetch_all_leagues(): async with SportsGameOdds( api_key_param=os.environ.get("SPORTS_ODDS_API_KEY_HEADER") ) as client: nfl, nba, mlb = await asyncio.gather( client.events.get(leagueID='NFL', limit=50), client.events.get(leagueID='NBA', limit=50), client.events.get(leagueID='MLB', limit=50), ) print(f"NFL: {len(nfl.data)} events") print(f"NBA: {len(nba.data)} events") print(f"MLB: {len(mlb.data)} events") asyncio.run(fetch_all_leagues()) ``` ```ruby [Ruby] require 'concurrent' # Create concurrent requests using threads futures = [ Concurrent::Future.execute { client.events.get(leagueID: 'NFL', limit: 50) }, Concurrent::Future.execute { client.events.get(leagueID: 'NBA', limit: 50) }, Concurrent::Future.execute { client.events.get(leagueID: 'MLB', limit: 50) } ] # Wait for all to complete nfl_events, nba_events, mlb_events = futures.map(&:value) puts "NFL: #{nfl_events.data.length} events" puts "NBA: #{nba_events.data.length} events" puts "MLB: #{mlb_events.data.length} events" ``` ```go [Go] import ( "golang.org/x/sync/errgroup" ) // Concurrent requests with errgroup g, ctx := errgroup.WithContext(context.Background()) var nflEvents, nbaEvents, mlbEvents *sportsoddsapi.EventGetPage g.Go(func() error { page, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ LeagueID: sportsoddsapi.String("NFL"), Limit: sportsoddsapi.Float(50), }) nflEvents = page return err }) g.Go(func() error { page, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ LeagueID: sportsoddsapi.String("NBA"), Limit: sportsoddsapi.Float(50), }) nbaEvents = page return err }) g.Go(func() error { page, err := client.Events.Get(ctx, sportsoddsapi.EventGetParams{ LeagueID: sportsoddsapi.String("MLB"), Limit: sportsoddsapi.Float(50), }) mlbEvents = page return err }) if err := g.Wait(); err != nil { // Handle error } fmt.Printf("NFL: %d events\n", len(nflEvents.Data)) fmt.Printf("NBA: %d events\n", len(nbaEvents.Data)) fmt.Printf("MLB: %d events\n", len(mlbEvents.Data)) ``` ```java [Java] import java.util.concurrent.*; // Concurrent requests with ExecutorService ExecutorService executor = Executors.newFixedThreadPool(3); Future nflFuture = executor.submit(() -> client.events().get(EventGetParams.builder() .leagueId("NFL").limit(50.0).build())); Future nbaFuture = executor.submit(() -> client.events().get(EventGetParams.builder() .leagueId("NBA").limit(50.0).build())); Future mlbFuture = executor.submit(() -> client.events().get(EventGetParams.builder() .leagueId("MLB").limit(50.0).build())); // Wait for all to complete EventGetPage nflEvents = nflFuture.get(); EventGetPage nbaEvents = nbaFuture.get(); EventGetPage mlbEvents = mlbFuture.get(); System.out.println("NFL: " + nflEvents.items().size() + " events"); System.out.println("NBA: " + nbaEvents.items().size() + " events"); System.out.println("MLB: " + mlbEvents.items().size() + " events"); executor.shutdown(); ``` ::: ## Example Projects All examples shown in this guide are available as complete, runnable projects in our GitHub repositories: ### TypeScript * **Repository**: [sports-odds-api-typescript](https://github.com/SportsGameOdds/sports-odds-api-typescript) * **Examples**: [View all TypeScript examples](https://github.com/SportsGameOdds/sports-odds-api-typescript/tree/main/examples) ### Python * **Repository**: [sports-odds-api-python](https://github.com/SportsGameOdds/sports-odds-api-python) * **Examples**: [View all Python examples](https://github.com/SportsGameOdds/sports-odds-api-python/tree/main/examples) ### Ruby * **Repository**: [sports-odds-api-ruby](https://github.com/SportsGameOdds/sports-odds-api-ruby) * **Examples**: [View all Ruby examples](https://github.com/SportsGameOdds/sports-odds-api-ruby/tree/main/examples) ### Go * **Repository**: [sports-odds-api-go](https://github.com/SportsGameOdds/sports-odds-api-go) * **Examples**: [View all Go examples](https://github.com/SportsGameOdds/sports-odds-api-go/tree/main/examples) ### Java * **Repository**: [sports-odds-api-java](https://github.com/SportsGameOdds/sports-odds-api-java) * **Examples**: [View all Java examples](https://github.com/SportsGameOdds/sports-odds-api-java/tree/main/examples) ## Next Steps Now that you understand how to use the SDK, explore these resources: * **[API Reference](/reference)** - Complete API endpoint documentation * **[Getting Started Guide](/basics/cheat-sheet)** - Reference back to core concepts * **[Rate Limits](/info/rate-limiting)** - Understand rate limiting and best practices *** ::: tip Need Help? [FAQ](/faq) · [Email](mailto:api@sportsgameodds.com) · Chat · [Discord](https://discord.gg/HhP9E7ZZaE) ::: --- --- url: /docs/v2/basics/setup.md description: >- Get your SportsGameOdds API key and authenticate requests. Use x-api-key header or apiKey query param. Includes SDK install and code examples. --- # Getting Started There are only 2 things you need to get started - an API key and a way to make requests. ## API Key * **[Get an API key here](https://sportsgameodds.com/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 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 ### Reference Docs Tool You can make requests directly from our [API Reference](https://sportsgameodds.com/docs/reference) page: 1. Navigate to [sportsgameodds.com/docs/reference](https://sportsgameodds.com/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**. ::: tip The reference docs page can also give you additional ready-to-use code snippets in multiple languages ::: ### 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 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. ::: warning 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 ### With HTTP Libraries Replace `YOUR_API_KEY` with your actual API key in the examples below: ::: code-group ```js [Javascript] // 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 [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 [Ruby] # This example uses the net/http library require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/sports/') # [!code focus] request = Net::HTTP::Get.new(uri) # [!code focus] request['X-Api-Key'] = 'YOUR_API_KEY' # [!code focus] response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| # [!code focus] http.request(request) # [!code focus] end # [!code focus] parsed_response = JSON.parse(response.body) # [!code focus] puts parsed_response ``` ```php [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 [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 First, install the SDK: ::: code-group ```bash [JavaScript (TypeScript)] 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 [Python] pip install sports-odds-api ``` ```bash [Ruby] gem install sports-odds-api ``` ```bash [Go] go get github.com/SportsGameOdds/sports-odds-api-go ``` ```groovy [Java] // 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: ::: code-group ```js [TypeScript] 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 [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 [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 [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 [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] } } ``` ::: ::: tip Learn More About the SDK Check out our [SDK Guide](/v2/sdk) for comprehensive documentation including pagination, error handling, filtering, and advanced features. ::: --- --- url: /docs/v2/info/glossary.md description: >- Betting terminology explained with API field mappings. Moneyline, spread, over/under, juice, arbitrage, closing line value, and more. --- # Sports Betting Glossary A guide to sports betting terminology and how it relates to the SportsGameOdds API. ## 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 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 A unique authentication token required to access the SportsGameOdds API. Passed via the `x-api-key` header in all API requests. ## 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](/examples/arbitrage-calculator). ## ATS Against the Spread - betting on a team to cover the point spread. ## 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 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](/data-types/bet-types). ## 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 Unique identifier for each sportsbook in the API (e.g., `draftkings`, `fanduel`, `betmgm`). See [Bookmakers reference](/data-types/bookmakers). ## BTTS Both Teams To Score - a soccer bet on whether both teams will score in the match. ## 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 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 Closing Line Value - the difference between the odds you received and the closing line. Consistently beating the closing line indicates sharp betting. ## 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](/info/consensus-odds). ## 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 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](/guides/data-batches). ## 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 Draw No Bet - a soccer bet where your stake is refunded if the match ends in a draw. ## 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 Consensus odds with the juice (vig) removed, representing a more accurate probability. Fields include `fairOdds`, `fairSpread`, and `fairOverUnder`. See [Consensus Odds guide](/info/consensus-odds). ## Favorite The team expected to win, indicated by negative odds in American format (e.g., -150). ## Fractional Odds Traditional UK format showing profit relative to stake. Example: 10/11 means you win $10 profit for every $11 wagered. ## Futures Long-term bets on season outcomes like championship winners, MVP awards, or team win totals. ## Handle The total amount of money wagered on an event or market. ## 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 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 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) 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 Unique identifier for each league or competition in the API (e.g., `nba`, `nfl`, `epl`). See [Leagues reference](/data-types/leagues). ## 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) 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 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 Moneyline - a straight up bet on which team wins. ## 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 Over/Under - betting on whether the total score will be over or under a set number. ## 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 Unique identifier for each odd in the API, formatted as `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}-{bookmakerID}`. See our [Odds guide](/data-types/odds). ## 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 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 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](/data-types/periods). ## PL Puck Line - hockey's version of the point spread, typically set at ±1.5 goals. ## Positive EV (Expected Value) A bet with a positive expected return, meaning it's mathematically profitable. ## 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 Bets from recreational bettors. Public money tends to favor favorites and overs, often following popular teams and narratives. ## 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 Run Line - baseball's version of the point spread, typically set at ±1.5 runs. ## ROI Return on Investment - the percentage return on your betting bankroll over time. ## 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 Bets placed by professional, well-informed bettors. Sharp action often causes significant line movement as bookmakers respect and adjust to these bets. ## sideID Identifier for which side of a bet: `home`, `away`, `over`, `under`, `yes`, `no`, `draw`, etc. Part of the `oddID` format. See [Bet Types](/data-types/bet-types). ## 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 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](/data-types/stat-entity). ## Stat ID Identifier for the statistic being bet on in the API (e.g., `points`, `rebounds`, `assists`, `touchdowns`). See [Stats reference](/data-types/stats). ## 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) 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 The team expected to lose, indicated by positive odds in American format (e.g., +150). --- --- url: /docs/v2/data-types/sports.md description: >- Complete list of sportID values. Basketball, football, baseball, hockey, soccer, tennis, golf, MMA, and more supported sports. --- # Sports > \[!NOTE] > The data on this page may become outdated or may include Sports which are not included in your subscription plan. For the most up-to-date list of `sportID` values available to your API key call the `/sports` endpoint. Each `sportID` corresponds to a specific sport. Sports can have more than one league. Below is a list of sportIDs. More sports (including ones not shown here) can be added upon request through a custom plan. --- --- url: /docs/v2.md description: >- Real-time sports betting odds API with 80+ bookmakers, 55+ leagues. Get moneylines, spreads, player props, and live scores. Free tier available. --- --- --- url: /docs/v2/data-types/stats.md description: >- All statID values organized by sport. Points, rebounds, assists, touchdowns, passing yards, goals, and player props for every supported sport. --- # Stats 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 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. sportID: `{{ sport.sportID }}` > \[!TIP] > Make an API request to `/stats?sportID={{ sport.sportID }}` for the most up-to-date data. --- --- url: /docs/v2/data-types/stat-entity.md description: >- How statEntityID works. Values include home, away, all (combined), playerID for props, or teamID for tournaments. Used in odds and results. --- # Stat Entity 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 | 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 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 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` | > \[!TIP] > For more details on bet types and sides, see the [Bet Types and Sides](/v2/data-types/bet-types) documentation. --- --- url: /docs/v2/help.md description: >- Contact SportsGameOdds support via email, live chat, or Discord. Learn about how we prioritize issues and what to include when reporting an issue. --- # Getting Help ::: warning Note 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. You may find you're not the first person to ask this question. ### [Email](mailto:api@sportsgameodds.com) Send us an email to ask questions, report issues, request new features, or anything else. Chat Chat with us below. We'll respond if we're online. Otherwise, we'll get back to you as soon as we're back. ### [Discord](https://discord.gg/HhP9E7ZZaE) Join our Discord community for real-time help, feature discussions, and community support. ::: tip When reporting an issue, please include: * Endpoint and parameters used (full URL) * Relevant response data * Expected response behavior * Code sample (if possible) ::: ::: warning All-Star Plan Customers Please use the dedicated support email you received when during onboarding for priority support. If you don't have it, email us at api@sportsgameodds.com and we'll re-send it. ::: --- --- url: /docs/v2/data-types/markets.md description: >- Interactive market browser showing oddID coverage by league and bookmaker. Filter by sport, bet type, or sportsbook to see available markets. --- # Supported Markets ::: warning Deprecation Notice The `1ix5` periodID (1st 5 Innings) is being deprecated in favor of `1h` (1st Half) for Baseball. Planned deprecation date: September 30. Please migrate any usage of `1ix5` to `1h`. ::: ::: warning Under Construction Some data related to Bet365 and Pinnacle may be incomplete ::: Below is a list of odds markets that we make available. Additional markets (those not listed here) can be made available through a custom plan by [contacting us](https://sportsgameodds.com/contact-us). For reference an oddID has the following format: `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}` You can learn more about how each of these identifiers works here: [statID](/v2/data-types/stats), [statEntityID](/v2/data-types/stat-entity), [periodID](/v2/data-types/periods), [betTypeID & sideID](/v2/data-types/bet-types), [leagueID](/v2/data-types/leagues) & [oddID](/v2/data-types/odds) If you select a League or a Bookmaker, then you'll see green ✅ marks in the results. Items with this icon are fully supported whereas those without one indicate that support for that combination of market, league, and bookmaker is not fully supported. --- --- url: /docs/v2/guides/fetching-teams.md description: >- Query /teams endpoint by teamID, leagueID, or sportID. Get team names, colors, and identifiers with code examples in multiple languages. --- # Fetching Teams ## 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 Let's take the following example, where we want to fetch the details of a specific NBA team: ::: code-group ```js [Javascript] await axios.get("/v2/teams", { params: { teamID: "LOS_ANGELES_LAKERS_NBA", }, }); ``` ```python [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 [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 [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 [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 If you wanted to fetch a list of all the teams in the NBA, your request would look something like this: ::: code-group ```js [Javascript] await axios.get("/v2/teams", { params: { leagueID: "NBA", }, }); ``` ```python [Python] import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?leagueID=NBA', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby [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 [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 [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 If you wanted to fetch a list of all basketball teams across all of our supported leagues, your request would look something like this: ::: code-group ```js [Javascript] await axios.get("/v2/teams", { params: { sportID: "BASKETBALL", }, }); ``` ```python [Python] import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby [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 [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 [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. ] } ```