# SportsGameOdds API Documentation > Real-time and historical sports betting odds data API. Get live odds, player props, spreads, totals, and moneylines from major sportsbooks. This file contains the complete API documentation. For the orientation index, see https://sportsgameodds.com/llms.txt --- # SportsGameOdds API - Sports Betting Odds Data for Developers URL: https://sportsgameodds.com/docs # SportsGameOdds API - Sports Betting Odds Data for Developers [#sportsgameodds-api---sports-betting-odds-data-for-developers] The easiest and most powerful way to add sports and betting odds data to your application, model, or whatever else! ## Quick Links [#quick-links] * [Setup Guide](/docs/basics/setup) — Get your API key and start making requests * [Quickstart](/docs/basics/quickstart) — Make your first API call in minutes * [API Reference](/docs/reference) — Interactive API reference with all endpoints * [SDK](/docs/sdk) — Official SDK for JavaScript/TypeScript, Python, and more * [FAQ](/docs/faq) — Common questions and troubleshooting --- # API Reference - Endpoints and Parameters URL: https://sportsgameodds.com/docs/reference --- # Data Explorer - Browse API Data Schema URL: https://sportsgameodds.com/docs/explorer --- # SDK Guide - TypeScript, Python, Ruby, Go, Java URL: https://sportsgameodds.com/docs/sdk # SDK Guide - TypeScript, Python, Ruby, Go, Java [#sdk-guide---typescript-python-ruby-go-java] While you can always make requests to the API directly, using our SDKs is an easy way to get started. Here you'll learn how to install, configure, and use our SDK across multiple programming languages with real-world examples. All code examples shown in this guide are available in the relevant [SportsGameOdds GitHub repositories](https://github.com/SportsGameOdds). ## Installation [#installation] Install the SDK using your language's package manager: ```bash # Using npm npm install sports-odds-api # Using yarn yarn add sports-odds-api # Using pnpm pnpm add sports-odds-api ``` ```bash # Using pip pip install sports-odds-api # Using poetry poetry add sports-odds-api ``` ```bash # Using bundler bundle add sports-odds-api # Or add to Gemfile gem "sports-odds-api", "~> 1.0" ``` ```bash go get github.com/SportsGameOdds/sports-odds-api-go ``` ```groovy // Maven (pom.xml) com.sportsgameodds.api sports-odds-api 1.0.0 // Gradle (build.gradle) implementation 'com.sportsgameodds.api:sports-odds-api:1.0.0' ``` ## Quick Start [#quick-start] Get up and running in under 60 seconds: ```ts 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 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 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 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 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")); }); } } ``` 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 [#pagination] The SportsGameOdds API returns data in pages to ensure optimal performance. Our SDKs provide both automatic and manual pagination options. ### Auto-Pagination [#auto-pagination] The easiest way to iterate through all results is using auto-pagination, which automatically fetches subsequent pages as you iterate: ```ts // 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 # 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 # 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 // 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 // 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 [#manual-pagination] For more control over pagination, you can manually navigate through pages: ```ts 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 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 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 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 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 [#filtering-and-query-parameters] Filter events by league, date, odds availability, and more using query parameters: ```ts // 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 # 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 # 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 // 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 // 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); ``` 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](/docs/reference) for a complete list of available parameters. ## Error Handling [#error-handling] Handle API errors gracefully with proper exception handling: ```ts 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 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 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 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 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()); } ``` * **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 [#timeout-and-retry-configuration] Configure timeouts and retry behavior for robust applications: ```ts // 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 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 # 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 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 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); ``` * 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 [#working-with-odds-data] Access and parse odds data efficiently: ```ts // 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 # 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 # 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| # [!code focus] bet_type_id = odd.bet_type_id # [!code focus] odds_by_type[bet_type_id] ||= [] # [!code focus] odds_by_type[bet_type_id] << odd # [!code focus] end # [!code focus] puts " Available bet types: #{odds_by_type.keys.join(', ')}" puts " Total markets: #{event.odds.length}" end ``` ```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 // 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) [#real-time-streaming-allstar-plan] Subscribe to real-time updates using our streaming API (requires AllStar plan): ```ts // 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 # 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 # 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 // Get stream connection info streamInfo, err := client.Stream.Events(ctx, sportsoddsapi.StreamEventsParams{ // [!code focus] Feed: sportsoddsapi.String("events:live"), // [!code focus] }) // [!code focus] 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 // 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 ``` 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 [#type-safety-and-ide-support] All SDKs provide full type definitions for excellent IDE autocomplete and type checking: ```ts 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 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 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 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 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 [#advanced-usage] ### Accessing Raw Response Data [#accessing-raw-response-data] Sometimes you need access to the raw HTTP response: ```ts // 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 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 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 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 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 [#concurrent-requests] Make multiple requests concurrently for better performance: ```ts // 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 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 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 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 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 [#example-projects] All examples shown in this guide are available as complete, runnable projects in our GitHub repositories: ### TypeScript [#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 [#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 [#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 [#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 [#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 [#next-steps] Now that you understand how to use the SDK, explore these resources: * **[API Reference](/docs/reference)** - Complete API endpoint documentation * **[Getting Started Guide](/docs/basics/cheat-sheet)** - Reference back to core concepts * **[Rate Limits](/docs/info/rate-limiting)** - Understand rate limiting and best practices --- # FAQ - Common Questions and Troubleshooting URL: https://sportsgameodds.com/docs/faq # FAQ - Common Questions and Troubleshooting [#faq---common-questions-and-troubleshooting] Common questions about using the SportsGameOdds API. Don't see your question? [Contact us](/contact-us) or join our [Discord](https://discord.gg/HhP9E7ZZaE). *** [Email](mailto:api@sportsgameodds.com) · [Chat](/) · [Discord](https://discord.gg/HhP9E7ZZaE) --- # Support - Get Help with the API URL: https://sportsgameodds.com/docs/help # Support - Get Help with the API [#support---get-help-with-the-api] Support times can vary, but we typically respond within 1 business day. We typically prioritize issues based on severity and impact. We also prioritize requests from customers on paid plans. ### FAQ [#faq] [Check this first.](/docs/faq) You may find you're not the first person to ask this question. ### Email [#email] [Send us an email](mailto:api@sportsgameodds.com) to ask questions, report issues, request new features, or anything else. ### Chat [#chat] [Chat with us](/) via the chat widget on our website. We'll respond if we're online. Otherwise, we'll get back to you as soon as we're back. ### Discord [#discord] [Join our Discord community](https://discord.gg/HhP9E7ZZaE) for real-time help, feature discussions, and community support. * Endpoint and parameters used (full URL) * Relevant response data * Expected response behavior * Code sample (if possible) Please use the dedicated support email you received during onboarding for priority support. If you don't have it, email us at [api@sportsgameodds.com](mailto:api@sportsgameodds.com) and we'll re-send it. --- # Introduction - SportsGameOdds API URL: https://sportsgameodds.com/docs/basics # Introduction - SportsGameOdds API [#introduction---sportsgameodds-api] Welcome to SportsGameOdds - We provide real-time odds, scores, stats and tons of other data for every major sports league in the world. Our system updates millions of data points every minute and serves them to you though this API. Whether you're working on an enterprise-level application or a personal side project, we want to make it dead simple to integrate sports and odds data into your application. ## Philosophy [#philosophy] * **Good odds data shouldn't cost tens of thousands of dollars per month**. We work hard to optimize our costs and pass the savings on to you. * **You should be able to get all the data you need with 1 request**. Most odds providers require certain parameters like the league or bookmaker on all requests. We don't. Our goal is to provide you with more data AND fewer requests. * **Repeatable schemas make APIs easy to understand and use**. Our system is designed from the ground up to be easy to work with. It's highly composable meaning you just need to understand a few basic concepts in order to understand the entire API. ## Pricing Model [#pricing-model] **We charge per event returned, not per market or bookmaker returned.** This means 1k credits on SportsGameOdds are typically worth the same as 100k-1M+ on other platforms. **Example:** A user needs to get data on 10 NBA games. Each has 250 odds markets and each market has odds for an average of 50 bookmakers. * **SportsGameOdds**: This consumes 10 credits ("objects") * **Other APIs**: This consumes 125,000 credits. (10 x 250 x 50) ## Full Coverage [#full-coverage] * **67+ leagues across 28+ sports\*** NFL, NBA, MLB, NHL, soccer, tennis, MMA, and more. * **85+ bookmakers\*** DraftKings, FanDuel, BetMGM, Caesars, PointsBet, and more * **Pre-game and live odds** with sub-minute update frequency * **Main markets, player props, game props** - thousands of markets supported * **Scores, stats, and results** - Everything in one place * **Historical odds data\*** for backtesting and analysis * **Partial game odds** (halves, quarters, periods, rounds, sets) * **Deeplinks** Navigate straight to relevant data on a bookmaker's website (\* depends on plan) ## Battle-Tested [#battle-tested] * **Millions of requests** handled daily from production applications * **Sub-100ms response times** with an average of 50ms * **99.9% uptime** with redundant infrastructure * **Real-time WebSocket streaming** for AllStar plan subscribers ## How It Works [#how-it-works] ### 1. Sign Up & Get Your API Key [#1-sign-up--get-your-api-key] [Sign up here](/pricing) and get your API key instantly. All plans include a 7 day free trial which we're always happy to extend if needed. We also have a 100% free tier available for testing and small projects. ### 2. Make Your First Request [#2-make-your-first-request] ```bash curl -X GET "https://api.sportsgameodds.com/v2/events?leagueID=NBA,NFL,MLB&oddsAvailable=true&apiKey=YOUR_API_KEY_GOES_HERE" ``` Simply replace `YOUR_API_KEY_GOES_HERE` with your actual API key ### 3. Build Something Amazing [#3-build-something-amazing] Use our [SDK](/docs/sdk), [examples](/docs/examples/live-odds-tracker), and [guides](/docs/guides/data-batches) to build faster. --- # Setup Guide - API Keys and Authentication URL: https://sportsgameodds.com/docs/basics/setup # Setup Guide - API Keys and Authentication [#setup-guide---api-keys-and-authentication] There are only 2 things you need to get started - an API key and a way to make requests. ## API Key [#api-key] * **[Get an API key here](/pricing)**. An API key is required to make requests and retrieve data from the API. * **We offer an eternally free plan**. Simply select the "Amateur" plan. During checkout, you will need to add a credit card, but it will never be charged unless you later decide to upgrade. * **Your key will be sent to your email address**. If you haven't received your API key within a couple of minutes of checking out, please check your spam folder. If it isn't there either, please reach out to support at [support@sportsgameodds.com](mailto:support@sportsgameodds.com) and we'll re-generate it for you. * **Keep your API key secret**. Never expose it publicly. Your API key is your password to the API. * **Include your API key in all requests**. It can be added in either the `x-api-key` header or the `apiKey` query param. ## Making Requests Manually [#making-requests-manually] ### Reference Docs Tool [#reference-docs-tool] You can make requests directly from our [API Reference](/docs/reference) page: 1. Navigate to [sportsgameodds.com/docs/reference](/docs/reference). 2. Enter your API key in the **Token** field under **API KEY** (Either ApiKeyHeader or ApiKeyParam should be selected). 3. Select an endpoint from the left sidebar (Events, Teams, Sports, etc.). 4. Click **Test Request** to open the interactive request panel. 5. Use the **Query** (or **Query Parameters**) section to set request parameters 6. Click **Send** to execute the request. 7. The response will be displayed in the **Response** section under **Body**. The reference docs page can also give you additional ready-to-use code snippets in multiple languages ### Postman [#postman] Postman is a popular tool for testing APIs. We provide an official collection you can import. 1. **Install Postman**: [postman.com/downloads](https://www.postman.com/downloads/) and install it. 2. **Download our collection**: SportsGameOdds Postman Collection 3. **Import the collection**: In Postman, click `Import`, then drag select the file you just downloaded. 4. **Set your API key**: Ensure `SportsGameOdds API` is selected → Go to the `Variables` tab → replace `YOUR_API_KEY_HERE` with your actual API key → click `Save`. 5. **Make a request**: Expand the `Events` folder → click `Get Events` → Adjust parameters (ex: set `oddsAvailable` to `true`) → click `Send` ### Directly in the Browser [#directly-in-the-browser] You can make requests directly in your browser's address bar using the apiKey parameter to authenticate. Simply visit a URL like this: ``` https://api.sportsgameodds.com/v2/sports/?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true&limit=3 ``` You can add or remove query parameters to adjust the data you receive. For example to only get moneylines, add `&oddIDs=points-home-game-ml-home,points-away-game-ml-away`to the end of the URL. API responses can be large and may slow/crash your browser tab. This method is great for quick/simple tests but not recommended for regular usage. ## Making Requests In Code [#making-requests-in-code] ### With HTTP Libraries [#with-http-libraries] Replace `YOUR_API_KEY` with your actual API key in the examples below: ```js // This example uses fetch() but you can use any HTTP library fetch("https://api.sportsgameodds.com/v2/sports/", {// [!code focus] method: "GET", // [!code focus] headers: { "X-Api-Key": "YOUR_API_KEY" }, // [!code focus] }) // [!code focus] .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ```python # This example uses the requests library import requests headers = { 'X-Api-Key': 'YOUR_API_KEY' } url = 'https://api.sportsgameodds.com/v2/sports/' # [!code focus] response = requests.get(url, headers=headers) # [!code focus] print(response.json()) ``` ```ruby # This example uses the net/http library require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/sports/') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_API_KEY' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end parsed_response = JSON.parse(response.body) puts parsed_response ``` ```php // This example uses cURL $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/sports/'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-Key: YOUR_API_KEY']); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java // This example uses HttpURLConnection import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/sports/"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_API_KEY"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); System.out.println(content.toString()); } catch (Exception e) { e.printStackTrace(); } } } ``` ### With Our SDK [#with-our-sdk] First, install the SDK: ```bash npm install sports-odds-api # [!code focus] yarn add sports-odds-api # if you're using yarn pnpm add sports-odds-api # if you're using pnpm ``` ```bash pip install sports-odds-api ``` ```bash gem install sports-odds-api ``` ```bash go get github.com/SportsGameOdds/sports-odds-api-go ``` ```groovy // Maven (pom.xml) com.sportsgameodds.api sports-odds-api 1.0.0 // Gradle (build.gradle) implementation 'com.sportsgameodds.api:sports-odds-api:1.0.0' ``` Then, make a request. Simply replace `YOUR_API_KEY` with your actual API key in the examples below: ```js import SportsGameOdds from "sports-odds-api"; const client = new SportsGameOdds({ apiKeyHeader: "YOUR_API_KEY", }); const sports = await client.sports.get(); // [!code focus] console.log(sports.data); // [!code focus] ``` ```python from sports_odds_api import SportsGameOdds client = SportsGameOdds( api_key_param='YOUR_API_KEY' ) # Fetch sports data sports = client.sports.get() # [!code focus] print(sports.data) # [!code focus] ``` ```ruby require "sports_odds_api" client = SportsOddsAPI::Client.new( api_key_param: 'YOUR_API_KEY' ) # Fetch sports data sports = client.sports.get # [!code focus] puts sports.data # [!code focus] ``` ```go package main import ( "context" "fmt" sportsoddsapi "github.com/SportsGameOdds/sports-odds-api-go" "github.com/SportsGameOdds/sports-odds-api-go/option" ) func main() { client := sportsoddsapi.NewClient( option.WithAPIKeyParam("YOUR_API_KEY"), ) // Fetch sports data ctx := context.Background() // [!code focus] sports, _ := client.Sports.Get(ctx) // [!code focus] fmt.Println(sports.Data) // [!code focus] } ``` ```java import com.sportsgameodds.api.client.SportsGameOddsClient; import com.sportsgameodds.api.client.okhttp.SportsGameOddsOkHttpClient; public class Main { public static void main(String[] args) { SportsGameOddsClient client = SportsGameOddsOkHttpClient.builder() .apiKeyHeader("YOUR_API_KEY") .build(); // Fetch sports data var sports = client.sports().get(); // [!code focus] System.out.println(sports.items()); // [!code focus] } } ``` Check out our [SDK Guide](/docs/sdk) for comprehensive documentation including pagination, error handling, filtering, and advanced features. --- # Quickstart - First API Call in 5 Minutes URL: https://sportsgameodds.com/docs/basics/quickstart # Quickstart - First API Call in 5 Minutes [#quickstart---first-api-call-in-5-minutes] A complete, copy-paste tutorial to fetch live odds ## Step 1: Get Your API Key [#step-1-get-your-api-key] 1. Sign up at [sportsgameodds.com/pricing](/pricing) 2. Check your email for your API key 3. Copy your API key - you'll need it in the next step ## Step 2: Make A Request [#step-2-make-a-request] 1. Copy one of the examples below into a file 2. Replace `YOUR_API_KEY` with your actual API key 3. Run it! ```text https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true ``` ```bash curl -X GET "https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY&leagueID=NBA,NFL,MLB&oddsAvailable=true" ``` ```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 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 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 ``` ```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()); } } ``` If you don't want to write code, paste the "Browser URL" example into your browser ## Step 3: See Live Odds [#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 [#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! [#done] **Congratulations!** You just fetched live odds from the SportsGameOdds API. ## Next Steps [#next-steps] Now that you've made your first successful call, explore what else you can do: ### Build Something Real [#build-something-real] Start with these complete, working examples: * **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Track line movement in real-time * **[Arbitrage Calculator](/docs/examples/arbitrage-calculator)** - Find guaranteed profit opportunities * **[Odds Comparison Dashboard](/docs/examples/odds-comparison-dashboard)** - Compare odds across bookmakers * **[Player Props Analyzer](/docs/examples/player-props-analyzer)** - Analyze player prop bets * **[Parlay Builder](/docs/examples/parlay-builder)** - Build parlay bet calculators ### Learn the Basics [#learn-the-basics] * **[Understanding Odds Data](/docs/data-types/odds)** - Deep dive into the oddID structure * **[Sports Betting Glossary](/docs/info/glossary)** - Learn all the terminology * **[Setup](/docs/basics/setup)** - More about how to get started with the API * **[Rate Limiting](/docs/info/rate-limiting)** - Understand request limits ### Use Our SDK [#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 →](/docs/sdk) ### Explore All Endpoints [#explore-all-endpoints] * **[API Reference](/docs/reference)** - Interactive documentation for all endpoints * **[Data Explorer](/docs/explorer)** - Browse our data schema interactively ## Common Next Questions [#common-next-questions] 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 →](/docs/data-types/bookmakers) 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! 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. **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 →](/docs/info/best-practices) 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 →](/docs/info/rate-limiting) **Ready to build something amazing?** Pick a recipe and start coding! 🚀 --- # API Cheat Sheet - Quick Reference URL: https://sportsgameodds.com/docs/basics/cheat-sheet # API Cheat Sheet - Quick Reference [#api-cheat-sheet---quick-reference] * Get an API key [here](/pricing). The key will be sent to your email. * Place your API key in the `apiKey` query param or the `x-api-key` header. * A full list of request endpoints and their parameters can be found [here](/docs/reference). * Responses are in JSON format. The `data` field contains the data you queried for. * Use our [Data Explorer](/docs/explorer) see the schema of the data that is returned. * The most commonly used endpoint is the `/events` endpoint. This returns a list of events with odds data. Common params include: * `oddsAvailable=true` - only return live/upcoming events with odds * `leagueID=NBA,NFL,MLB` - only return events for specified leagues * `oddID=points-home-game-ml-home,points-home-game-sp-home` - only return specified odds markets * `includeAltLines=true` - include alternate spread/over-under lines ## Endpoints [#endpoints] A full reference of all endpoints and their parameters can be found [here](/docs/reference). The API is currently on version 2 so each endpoint is prefixed with `https://api.sportsgameodds.com/v2`. For example the `/leagues` endpoint should be accessed at `https://api.sportsgameodds.com/v2/leagues`. The main endpoint you'll use to fetch odds and scores/stats is the [`/events`](/docs/reference#tag/events/GET/events/) endpoint. ## Authentication [#authentication] All requests require an [API key](/pricing) Place it in the header as `x-api-key`. ```javascript fetch("https://api.sportsgameodds.com/v2/events", { headers: { "x-api-key": "your-api-key-here" }, }); ``` Or, place it in the query params as `apiKey`. ```javascript fetch("https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY_GOES_HERE"); ``` ## Response Format [#response-format] All responses follow a consistent JSON structure: ```json { "success": true, "data": [...], "error": "...", "nextCursor": "...", } ``` The `success` field tells you if the request was successful and returned data or not. The `data` field contains a list of objects you queried for. The format of each object is dependent on the endpoint you called. The `error` field contains an error message. It's only present if `success` is false. The `nextCursor` field contains a cursor used to get the next page of data in some cases. More info [here](/docs/guides/data-batches). ## Example Request [#example-request] `https://api.sportsgameodds.com/v2/events?leagueID=NBA,NFL,MLB&oddsAvailable=true&limit=1&apiKey=YOUR_API_KEY` To test this, simply replace `YOUR_API_KEY` with your actual API key and visit this URL in your browser. You shoud see the next upcoming or live game for either NBA, NFL, or MLB along with all the odds data for that game. * `leagueID=NBA,NFL,MLB` - Tells the API to only return data for NBA, NFL, and MLB games * `oddsAvailable=true` - Tells the API to only return data for games where there are available/active odds markets * `limit=1` - Tells the API to only return 1 game. By default the API sorts results by start time in ascending order. * `apiKey=YOUR_API_KEY` - Authenticates your request. ## Data Schema [#data-schema] The best way to get a sense of how our data is structured is to play around with our [Data Explorer](/docs/explorer). In general, the data is organized into the following hierarchy: 1. Sports - fairly self-explanatory. 2. Leagues - each sport has one or more leagues. 3. Teams - each league has one or more teams. 4. Events - each team has one or more events. The core unit that you'll likely be working with is the Event. Each Event represents a single game/match/fight/etc. The Event object also contains all odds markets for that event. An odds market represents both the type of bet and the side of the bet. Each has its own unique ID called an `oddID`. You can find data for a specific odds market on an Event object at the path: `Event.odds.`. If you're looking for bookmaker specific data, you can find it here `Event.odds..byBookmaker.`. Each oddID is a combination of other identifiers which uniquely identify the odds market. These identifiers are: * `statID` - the statistic being measured (ex: "points", "assists", "rebounds", etc.) * `statEntityID` - Who's performance on the stat is being measured (ex: "home", "away", "LEBRON\_JAMES", "all" (both teams), etc.) * `periodID` - the period being tracked (ex: full game ("game"), first half ("1h"), etc.) * `betTypeID` - the type of bet (ex: spread ("sp"), moneyline ("ml"), over/under ("ou"), etc.) * `sideID` - the side of the bet (ex: "home", "away", "over", "under", "yes", "no", etc.) By combining these identifiers, we can uniquely identify any odds market! Pretty cool, right? --- # AI-Assisted Development - MCP Server and Context URL: https://sportsgameodds.com/docs/info/ai-vibe-coding # AI-Assisted Development - MCP Server and Context [#ai-assisted-development---mcp-server-and-context] AI tools can significantly speed up your development process. However, these tools need proper context in order to generate accurate, production-ready code. The information below is designed to help you do just that. ## Copy-Paste Context [#copy-paste-context] **We highly recommend you either paste the [AI Context](#ai-context) below into your prompt or add it to an applicable context/rules file.** ## AI-Friendly Docs [#ai-friendly-docs] ### Indexed Documentation [#indexed-documentation] [View llms.txt](/llms.txt) — Includes URLs and descriptions of each of our documentation pages. AI tools can use this to identify specific documentation pages to fetch/read when needed. Length: \~2k tokens ### Full Documentation [#full-documentation] [View llms-full.txt](/llms-full.txt) — Includes our entire documentation in a single file. This is helpful for AI tools that have a large context window. Length: \~85k tokens ### OpenAPI Specification [#openapi-specification] [View OpenAPI Spec](/openapi.json) — A full machine-readable API definition with exact schemas for all endpoints/requests and responses. Every page also advertises it via ``, so agents can find it without being told the URL. Length: \~600k tokens ### Markdown Versions of Any Page [#markdown-versions-of-any-page] Every page on this site has a Markdown representation — no HTML, no nav, no scripts. Request it either way: ```bash # Content negotiation (acceptmarkdown.com) curl -H "Accept: text/markdown" https://sportsgameodds.com/docs/basics/quickstart # Or append .md / .mdx to the URL curl https://sportsgameodds.com/docs/basics/quickstart.mdx curl https://sportsgameodds.com/leagues/nfl-odds-api.md ``` ## MCP Server [#mcp-server] Our [MCP](https://modelcontextprotocol.io/) server enables AI tools to directly make calls to the API. You can find the server here: [`sports-odds-api-mcp`](https://www.npmjs.com/package/sports-odds-api-mcp). It provides the following resources/tools: * **List endpoints** - Discover available API operations * **Fetch live data** - Get events, odds, teams, players, leagues, etc. * **Search documentation** - Find relevant info from the documentation * **Monitor usage** - Check your API quota and rate limits * **Stream events** - Get notified when Events change (AllStar plan only) Below are installation instructions. Be sure to replace `your-api-key-here` with your actual API key. ### Claude Code [#claude-code] ```bash claude mcp add sports-game-odds --env SPORTS_ODDS_API_KEY_HEADER="your-api-key-here" -- npx -y sports-odds-api-mcp ``` ```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 [#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) ```bash { "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 [#vs-code] ```bash code --add-mcp '{"name":"sports-game-odds","command":"npx","args":["-y","sports-odds-api-mcp@latest"],"env":{"SPORTS_ODDS_API_KEY_HEADER":"your-api-key-here"}}' ``` ```json { "name": "sports-game-odds", "command": "npx", "args": ["-y", "sports-odds-api-mcp@latest"], "env": { "SPORTS_ODDS_API_KEY_HEADER": "your-api-key-here" } } ``` ## AI Context [#ai-context] Paste the following into your prompt or add it to an applicable rules file: ```md # 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/openapi.json | Exact request/response schemas, parameter definitions, example responses | | Any page as Markdown | Append `.md` to a page URL, or send `Accept: text/markdown` | Clean prose without HTML — e.g. https://sportsgameodds.com/leagues/nfl-odds-api.md | > **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 | ``` --- # Pagination Guide - Cursor-Based Batching URL: https://sportsgameodds.com/docs/guides/data-batches # Pagination Guide - Cursor-Based Batching [#pagination-guide---cursor-based-batching] This following information only applies to the [`/events`](/docs/endpoints/getEvents), [`/teams`](/docs/endpoints/getTeams), and [`/players`](/docs/endpoints/getPlayers) endpoints. Other endpoints will always return all of the results which match your query. However, there may be hundreds or even thousands of results to these endpoints, so they must be fetched in batches. ## How It Works [#how-it-works] 1. Make a request. Each request has a limit on the number of items returned. 2. If there are more items to show, you'll get a `nextCursor` in the response. 3. Repeat the query but put the value from last request's `nextCursor` in the `cursor` parameter. 4. Repeat this until you no longer receive a `nextCursor` in the response in order to get all of the results. ## The `cursor` Parameter [#the-cursor-parameter] This tells the API where to pick up from. If you received a response from the API and there are more items to show, you'll get a `nextCursor` at the top level of the response. Use this value in the `cursor` parameter of your next request to pick up where you left off. Notes on using `cursor`: * The parameter name is exactly `cursor` (lowercase). The response field is called `nextCursor`, but sending it back as `nextCursor=...` returns a `400` error. * Always use the value from the last response's `nextCursor` property, unchanged. Don't try to reverse-engineer or modify the cursor value yourself. * Don't change any query parameters between cursor requests. This will cause the cursor to not function properly. * 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 back an empty `data` array, that just means you've reached the end of the results. * Events are sorted by start time (`status.startsAt`) and the cursor marks your position in that ordering. Since event data updates constantly, an event can change while you're partway through paginating. * If an event's start time is moved to a spot you've already paged past, you won't see it in that pass. If it's moved later, you may see it twice. * If you filter on a field that changes over time (ex: `oddsAvailable`, `live`, `started`), an event may enter or leave your results between requests. ## The `limit` Parameter [#the-limit-parameter] The `limit` parameter determines the max number of items to return in each request. It has a default value which can be overridden. * If you don't specify a `limit` parameter, then a limit of `10` will be used. * If you specify a `limit` above `300`, then you'll receive an error. * Otherwise, the limit applied is the smaller value between the `limit` parameter you supplied and the max-limit for the endpoint. * If you're making a request to the `/players` or `/teams` endpoints, the max-limit is `250` * If you're making a request to the `/events` endpoint, the max-limit varies from 25-100 depending on the query. Factors affecting this include: * If the query filters for specific fields (ex: `oddIDs`) * If the query is for upcoming or past events * If the query includes alt lines ## Example [#example] Let's take the following example, where we want to grab all unfinalized NBA events: ```js const allEvents = []; let nextCursor = null; let hasMore = true; while (hasMore) { try { const response = await axios.get("https://api.sportsgameodds.com/v2/events", { params: { leagueID: "NBA", finalized: false, limit: 100, cursor: nextCursor, }, headers: { "x-api-key": YOUR_API_KEY, }, }); allEvents.push(...response.data.data); nextCursor = response.data.nextCursor; hasMore = Boolean(nextCursor); } catch (error) { hasMore = false; } } console.log(`Found ${allEvents.length} events`); allEvents.forEach((event) => console.log(event.eventID)); return allEvents; ``` ```python import requests all_events = [] next_cursor = None has_more = True while has_more: try: response = requests.get( "https://api.sportsgameodds.com/v2/events", params={ "leagueID": "NBA", "finalized": "false", "limit": 100, "cursor": next_cursor }, headers={"x-api-key": YOUR_API_KEY} ) response.raise_for_status() data = response.json() all_events.extend(data.get("data", [])) next_cursor = data.get("nextCursor") has_more = next_cursor is not None except requests.RequestException: has_more = False print(f"Found {len(all_events)} events") for event in all_events: print(event["eventID"]) return all_events ``` ```ruby require "net/http" require "json" require "uri" def fetch_all_events(api_key) all_events = [] next_cursor = nil has_more = true while has_more begin uri = URI("https://api.sportsgameodds.com/v2/events") params = { leagueID: "NBA", finalized: false, limit: 100, cursor: next_cursor }.compact uri.query = URI.encode_www_form(params) req = Net::HTTP::Get.new(uri) req["x-api-key"] = api_key res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(req) end data = JSON.parse(res.body) all_events.concat(data["data"] || []) next_cursor = data["nextCursor"] has_more = !next_cursor.nil? && !next_cursor.empty? rescue StandardError has_more = false end end puts "Found #{all_events.length} events" all_events.each { |event| puts event["eventID"] } all_events end ``` ```php $allEvents = []; $nextCursor = null; $hasMore = true; while ($hasMore) { try { $params = [ 'leagueID' => 'NBA', 'finalized' => 'false', 'limit' => 100, ]; if ($nextCursor !== null) { $params['cursor'] = $nextCursor; } $ch = curl_init("https://api.sportsgameodds.com/v2/events?" . http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "x-api-key: " . YOUR_API_KEY ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { throw new Exception("HTTP Error: " . $httpCode); } $data = json_decode($response, true); $events = $data['data'] ?? []; $allEvents = array_merge($allEvents, $events); $nextCursor = $data['nextCursor'] ?? null; $hasMore = $nextCursor !== null; } catch (Exception $e) { $hasMore = false; } } echo "Found " . count($allEvents) . " events\n"; foreach ($allEvents as $event) { echo $event['eventID'] . "\n"; } return $allEvents; ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.List; import com.google.gson.JsonObject; import com.google.gson.JsonParser; List allEvents = new ArrayList<>(); String nextCursor = null; boolean hasMore = true; HttpClient client = HttpClient.newHttpClient(); while (hasMore) { try { String url = String.format( "https://api.sportsgameodds.com/v2/events?leagueID=NBA&finalized=false&limit=100%s", nextCursor != null ? "&cursor=" + nextCursor : "" ); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", YOUR_API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); JsonObject data = JsonParser.parseString(response.body()).getAsJsonObject(); data.getAsJsonArray("data").forEach(event -> allEvents.add(event.getAsJsonObject()) ); nextCursor = data.has("nextCursor") && !data.get("nextCursor").isJsonNull() ? data.get("nextCursor").getAsString() : null; hasMore = nextCursor != null; } catch (Exception e) { hasMore = false; } } System.out.println("Found " + allEvents.size() + " events"); allEvents.forEach(event -> System.out.println(event.get("eventID").getAsString()) ); return allEvents; ``` ``` ``` --- # Handling Odds - Parse Results and Grade Bets URL: https://sportsgameodds.com/docs/guides/handling-odds # Handling Odds - Parse Results and Grade Bets [#handling-odds---parse-results-and-grade-bets] ## Overview [#overview] The Sports Game Odds API comes complete with odds and result data for every event. This guide will show you how you can easily fetch and parse the odds for a specific event or group of events! ## Example [#example] In our previous example you saw how you would fetch upcoming NBA events from the API using our cursor pattern, let's take that a step further! Now, assuming the NBA week has passed, we will fetch all finalized NBA events from that week, then parse the odds results for each event, so we can grade them. ```js let nextCursor = null; let eventData = []; do { try { const response = await axios.get('/v2/events', { params: { leagueID: 'NBA', startsAfter: '2024-04-01', startsBefore: '2024-04-08', finalized: true, cursor: nextCursor } }); const data = response.data; eventData = eventData.concat(data.data); nextCursor = data?.nextCursor; } catch (error) { console.error('Error fetching events:', error); break; } } while (nextCursor); // Now that we have the events, let's parse the odd results! // Based on the bet type, compare score to the odds and grade the odds, for this example assume the odds are over/under eventData.forEach((event) => { const odds = event.odds; Object.values(odds).forEach((oddObject) => { const oddID = oddObject.oddID; const score = parseFloat(oddObject.score); const closeOverUnder = parseFloat(oddObject.closeOverUnder); if (score > closeOverUnder) console.log(`Odd ID: ${oddID} - Over Wins`); else if (score === closeOverUnder) console.log(`Odd ID: ${oddID} - Push`); else console.log(`Odd ID: ${oddID} - Under Wins`); }); }); ``` --- # Teams Guide - Fetch by ID, League, or Sport URL: https://sportsgameodds.com/docs/guides/fetching-teams # Teams Guide - Fetch by ID, League, or Sport [#teams-guide---fetch-by-id-league-or-sport] ## Overview [#overview] The Sports Game Odds API provides the ability to fetch a list of teams or a specific team's details. You can use a `sportID` or `leagueID` to get a list of associated teams and their details. You can also pass a `teamID` to just get a single team's details. Note that when specifying a `teamID`, it will still return as an array, just with a single object in it. A `teamID` almost never changes, but it can happen. If you request one that has changed, you get the team back under its new `teamID`, and the old one is in its `aliases` array. ## Fetching by Team [#fetching-by-team] Let's take the following example, where we want to fetch the details of a specific NBA team: ```js await axios.get("/v2/teams", { params: { teamID: "LOS_ANGELES_LAKERS_NBA", }, }); ``` ```python import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_TOKEN' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body) ``` ```php $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: YOUR_TOKEN' ]); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import org.json.JSONObject; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/teams?teamID=LOS_ANGELES_LAKERS_NBA"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_TOKEN"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); JSONObject data = new JSONObject(content.toString()); System.out.println(data.toString(2)); } catch (Exception e) { e.printStackTrace(); } } } ``` This will return a response that looks something like this: ```json { "success": true, "data": [ { "sportID": "BASKETBALL", "names": { "short": "LAL", "medium": "Lakers", "long": "Los Angeles Lakers" }, "leagueID": "NBA", "teamID": "LOS_ANGELES_LAKERS_NBA" } ] } ``` ## Fetching by league [#fetching-by-league] If you wanted to fetch a list of all the teams in the NBA, your request would look something like this: ```js await axios.get("/v2/teams", { params: { leagueID: "NBA", }, }); ``` ```python import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?leagueID=NBA', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/teams?leagueID=NBA') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_TOKEN' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body) ``` ```php $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/teams?leagueID=NBA'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: YOUR_TOKEN' ]); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import org.json.JSONObject; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/teams?leagueID=NBA"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_TOKEN"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); JSONObject data = new JSONObject(content.toString()); System.out.println(data.toString(2)); } catch (Exception e) { e.printStackTrace(); } } } ``` This will return a response that looks something like this: ```json { "nextCursor": "MILWAUKEE_BUCKS_NBA", "success": true, "data": [ { "sportID": "BASKETBALL", "names": { "short": "LAL", "medium": "Lakers", "long": "Los Angeles Lakers" }, "leagueID": "NBA", "teamID": "LAKERS_NBA", "colors": { "secondary": "#FFFFFF", "primaryContrast": "#000000", "secondaryContrast": "#552583", "primary": "#552583" } }, { "sportID": "BASKETBALL", "names": { "short": "BOS", "medium": "Celtics", "long": "Boston Celtics" }, "leagueID": "NBA", "teamID": "CELTICS_NBA", "colors": { "secondary": "#FFFFFF", "primaryContrast": "#000000", "secondaryContrast": "#007A33", "primary": "#007A33" } }, // ... // Up to 30 objects may be returned in this object. If there are more available // then you'll see a nextCursor property you can use to fetch the next // page of related objects. ] } ``` ## Fetching by sport [#fetching-by-sport] If you wanted to fetch a list of all basketball teams across all of our supported leagues, your request would look something like this: ```js await axios.get("/v2/teams", { params: { sportID: "BASKETBALL", }, }); ``` ```python import requests response = requests.get( 'https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL', headers={'X-Api-Key': 'YOUR_TOKEN'} ) print(response.json()) ``` ```ruby require 'net/http' require 'json' uri = URI('https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL') request = Net::HTTP::Get.new(uri) request['X-Api-Key'] = 'YOUR_TOKEN' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body) ``` ```php $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: YOUR_TOKEN' ]); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import org.json.JSONObject; public class Main { public static void main(String[] args) { try { URL url = new URL("https://api.sportsgameodds.com/v2/teams?sportID=BASKETBALL"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("X-Api-Key", "YOUR_TOKEN"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); conn.disconnect(); JSONObject data = new JSONObject(content.toString()); System.out.println(data.toString(2)); } catch (Exception e) { e.printStackTrace(); } } } ``` This will return a response that looks something like this: ```json { "nextCursor": "BELMONT_NCAAB", "success": true, "data": [ { "sportID": "BASKETBALL", "names": { "short": "LAL", "medium": "Lakers", "long": "Los Angeles Lakers" }, "leagueID": "NBA", "teamID": "LAKERS_NBA" }, { "sportID": "BASKETBALL", "names": { "short": "BOS", "medium": "Celtics", "long": "Boston Celtics" }, "leagueID": "NBA", "teamID": "CELTICS_NBA" }, { "sportID": "BASKETBALL", "names": { "short": "GSW", "medium": "Warriors", "long": "Golden State Warriors" }, "leagueID": "NBA", "teamID": "WARRIORS_NBA" }, // ... // Up to 30 objects may be returned in this object. If there are more available // then you'll see a nextCursor property you can use to fetch the next // page of related objects. ] } ``` --- # Optimize Response Speed and Latency URL: https://sportsgameodds.com/docs/guides/response-speed # Optimize Response Speed and Latency [#optimize-response-speed-and-latency] We're in the process of adding additional request params which are designed to allow you to more efficiently fetch only the data you need. We also plan on releasing a GraphQL API in the future. Check back here for updates. We're committed to finding balance between making as much data available to you as possible while also ensuring that you can fetch that data quickly and efficiently. This guide is designed to help you understand how to optimize your requests to reduce response times/latency. ## Use the `oddIDs` parameter [#use-the-oddids-parameter] * The most common cause of high response times is fetching a large number of odds at once. This can be especially problematic when fetching odds for a large number of Events. * To reduce this, you can use the `oddIDs` parameter to fetch only the odds you need. * The `oddIDs` parameter can be included in the `/events` * It accepts a comma-separated list of oddID values (See the [Markets](/docs/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 --- # Real-Time Streaming API (WebSocket) URL: https://sportsgameodds.com/docs/guides/realtime-streaming-api # Real-Time Streaming API (WebSocket) [#real-time-streaming-api-websocket] This API endpoint is only available to **AllStar** and **custom plan** subscribers. It is not included with basic subscription tiers. [Contact support](mailto:api@sportsgameodds.com) to get access. This streaming API is currently in **beta**. API call patterns, response formats, and functionality may change. Our Streaming API provides real-time updates for Event objects through WebSocket connections. Instead of polling our REST endpoints, you can maintain a persistent connection to receive instant notifications when events change. This is ideal for applications that need immediate updates with minimal delay. We use [Pusher Protocol](https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/) for WebSocket communication. While you can connect using any WebSocket library, we strongly recommend using any [Pusher Client Library](https://pusher.com/docs/channels/channels_libraries/libraries/) (ex: [Javascript](https://github.com/pusher/pusher-js), [Python](https://github.com/pusher/pusher-http-python)) ## How It Works [#how-it-works] The streaming process involves two steps: 1. **Get Connection Details**: Make a request to [`/v2/stream/events`](/docs/endpoints/streamEvents) to receive: * WebSocket authentication credentials * WebSocket URL/channel info * Initial snapshot of current data 2. **Connect and Stream**: Use the provided details to connect via Pusher (or another WebSocket library) and receive real-time `eventID` notifications for changed events Your API key will have limits on concurrent streams. ## Available Feeds [#available-feeds] Subscribe to different feeds using the `feed` query parameter: | Feed | Description | Required Parameters | | ----------------- | ----------------------------------------------------------- | ------------------- | | `events:live` | All events currently in progress (started but not finished) | None | | `events:upcoming` | Upcoming events with available odds for a specific league | `leagueID` | | `events:byid` | Updates for a single specific event | `eventID` | If the `eventID` you pass to `events:byid` has changed (this is rare), we set up the stream for the event under its new `eventID`. The old one is in that event's `aliases` array. The number of supported feeds will increase over time. Please reach out if you have a use case which can't be covered by these feeds. ## Quick Start Example [#quick-start-example] Here's the minimal code to connect to live events: ```js const axios = require("axios"); const Pusher = require("pusher-js"); const STREAM_FEED = "events:live"; // ex: events:upcoming, events:byid, events:live const API_BASE_URL = "https://api.sportsgameodds.com/v2"; const API_KEY = "YOUR API KEY"; const run = async () => { // Initialize a data structure where we'll save the event data const EVENTS = new Map(); // Call this endpoint to get initial data and connection parameters const streamInfo = await axios.get(`${API_BASE_URL}/stream/events`, { headers: { "x-api-key": API_KEY }, params: { feed: STREAM_FEED } }).then((r) => r?.data); // Seed initial data streamInfo.data.forEach((event) => EVENTS.set(event?.eventID, event)); // Connect to WebSocket server const pusher = new Pusher(streamInfo.pusherKey, streamInfo.pusherOptions); pusher.subscribe(streamInfo.channel).bind("data", async (changedEvents) => { // Get the eventIDs that changed const eventIDs = changedEvents.map(({ eventID }) => eventID).join(","); // Get the full event data for the changed events const eventDataResponse = await axios.get(`${API_BASE_URL}/events`, { headers: { "x-api-key": API_KEY }, params: { eventIDs } }).then((r) => r?.data); // Update our data with the full event data eventDataResponse.data.forEach((newEventData) => EVENTS.set(newEventData?.eventID, newEventData)); }); // Use pusher.disconnect to disconnect from the WebSocket server process.on("SIGINT", pusher.disconnect); }; run(); ``` ```python import requests import pusherclient import json API_KEY = 'YOUR_API_KEY' API_BASE_URL = 'https://api.sportsgameodds.com/v2' def handle_update(data): try: changed_events = json.loads(data) event_ids = ','.join([e['eventID'] for e in changed_events]) # Fetch updated event data response = requests.get( f"{API_BASE_URL}/events", headers={'x-api-key': API_KEY}, params={'eventIDs': event_ids} ) for event in response.json()['data']: print(f"Updated: {event['eventID']}") except Exception as e: print(f"Error processing update: {e}") def connect_to_live_events(): # Get connection details response = requests.get( f"{API_BASE_URL}/stream/events", headers={'x-api-key': API_KEY}, params={'feed': 'events:live'} ) stream_info = response.json() print(f"Connected with {len(stream_info['data'])} initial events") # Connect to Pusher (Note: private channels require custom auth in Python) pusher_key = stream_info['pusherKey'] channel_name = stream_info['channel'] pusher = pusherclient.Pusher(pusher_key) channel = pusher.subscribe(channel_name) channel.bind('data', handle_update) pusher.connection.bind('pusher:connection_established', lambda data: print("Connected to stream")) if __name__ == "__main__": connect_to_live_events() ``` ```ruby require 'net/http' require 'json' require 'pusher-client' require 'uri' STREAM_FEED = "events:live" # ex: events:upcoming, events:byid, events:live API_BASE_URL = "https://api.sportsgameodds.com/v2" API_KEY = "API_KEY_GOES_HERE" def convert_ruby_options(standard_options, api_key) ruby_client_options = {} ruby_client_options[:ws_host] = standard_options['wsHost'] if standard_options['wsHost'] ruby_client_options[:ws_port] = standard_options['wsPort'] if standard_options['wsPort'] ruby_client_options[:wss_port] = standard_options['wssPort'] if standard_options['wssPort'] ruby_client_options[:ws_path] = standard_options['wsPath'] if standard_options['wsPath'] ruby_client_options[:ws_host] = "ws-#{standard_options['cluster']}.pusher.com" if standard_options['cluster'] && !ruby_client_options[:ws_host] ruby_client_options[:secure] = true if (standard_options.dig('channelAuthorization', 'endpoint')) ruby_client_options[:auth_method] = ->(socket_id, channel) { headers = (standard_options.dig('channelAuthorization', 'headers') || {}).merge('Content-Type' => 'application/json') params = (standard_options.dig('channelAuthorization', 'params') || {}).merge('socket_id' => socket_id, 'channel_name' => channel.name) res = Net::HTTP.post(URI(standard_options.dig('channelAuthorization', 'endpoint')), params.to_json, headers) res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body)['auth'] : raise("Auth failed: HTTP #{res.code} - #{res.body}") } end ruby_client_options end def api_get(path, params = {}) uri = URI("#{API_BASE_URL}#{path}") uri.query = URI.encode_www_form(params) unless params.empty? res = Net::HTTP.get_response(uri, {'x-api-key' => API_KEY}) raise "HTTP #{res.code}: #{res.message}" unless res.is_a?(Net::HTTPSuccess) JSON.parse(res.body) end def run events = {} # Get initial data and connection parameters stream_info = api_get("/stream/events", feed: STREAM_FEED) stream_info['data'].each { |e| events[e['eventID']] = e } # Convert the standard options to Ruby SDK options connect_options = convert_ruby_options(stream_info['pusherOptions'], API_KEY) # Initialize the client client = PusherClient::Socket.new(stream_info['pusherKey'], connect_options) # Set up other event handlers as needed client.bind('pusher:connection_established') { puts "Connected" } client.bind('pusher:subscription_succeeded') { |_| puts "Subscribed to: #{stream_info['channel']}" } client.bind('pusher:subscription_error') { |data| puts "Error: #{data}" } # Subscribe to and handle data events in the channel channel = client.subscribe(stream_info['channel'], stream_info['user']) channel.bind('data') do |json| changed_events = JSON.parse(json) event_ids = changed_events.map { |e| e['eventID'] }.join(', ') puts "Changed Events: #{event_ids}" # Fetch and update full event data api_get("/events", eventIDs: event_ids)['data'].each { |e| events[e['eventID']] = e } rescue => e puts "Error processing data event: #{e.message}" end # Print initial event IDs puts "EVENTS: #{events.keys.join(',')}" # Graceful shutdown trap('INT') { puts "\nDisconnecting..."; client.disconnect; exit } # Connect to the WebSocket server client.connect # For async usage, you can instead call client.connect(true) after initializing the client end private # Runs the script run ``` ## API Response Format [#api-response-format] The `/v2/stream/events` endpoint returns: ```json { "success": true, "data": [ , // ... more events ], "pusherKey": , "pusherOptions": , "channel": } ``` ## Update Message Format [#update-message-format] Real-time updates contain only the `eventID` of changed events: ```json [ { "eventID": }, ] ``` You then fetch full event details using the `/v2/events` endpoint with the `eventIDs` parameter. ## Complete Example: NFL Game Tracker [#complete-example-nfl-game-tracker] This example demonstrates a production-ready implementation for tracking NFL games: ```js const axios = require("axios"); const Pusher = require("pusher-js"); const API_KEY = "PUT YOUR API KEY HERE"; const API_BASE_URL = "https://api.sportsgameodds.com/v2"; const LEAGUE_ID = "NFL"; // State variables let events = new Map(); let pusher = null; let channel = null; let connectionState = "disconnected"; let heartbeatInterval = null; const getEventTitle = (event) => { const awayTeam = event.teams?.away?.names?.medium || event.teams?.away?.names?.long || event.teams?.away?.names?.short; const homeTeam = event.teams?.home?.names?.medium || event.teams?.home?.names?.long || event.teams?.home?.names?.short; const gameTime = event.status.startsAt.toLocaleString(); return `${awayTeam} vs ${homeTeam} @ ${gameTime} (${event.eventID})`; }; const startMonitoring = () => { // Heartbeat heartbeatInterval = setInterval(() => { console.log(`💓 Heartbeat...connection: ${connectionState}, events tracked: ${events.size}`); }, 10000); // Connection state changes pusher.connection.bind("state_change", (states) => { connectionState = states.current; console.log(`🔌 Connection state: ${states.previous} → ${states.current}`); }); // Connection errors pusher.connection.bind("error", (error) => { console.error("🚨 Connection error:", error); }); // Connection established pusher.connection.bind("connected", () => { console.log("🎉 Connection established successfully"); }); // Connection disconnected pusher.connection.bind("disconnected", () => { console.log("👋 Connection disconnected"); }); // Failed connection pusher.connection.bind("failed", () => { console.log("💥 Connection failed permanently"); }); }; const connect = async () => { console.log(`🔄 Connecting upcoming events for: ${LEAGUE_ID}...`); connectionState = "connecting"; try { // Get stream metadata console.log("📡 Fetching stream configuration..."); const response = await axios.get(`${API_BASE_URL}/stream/events`, { headers: { "x-api-key": API_KEY }, params: { feed: "events:upcoming", leagueID: LEAGUE_ID, }, }); console.log("✅ Stream configuration received"); const { data: initialEvents, pusherKey, pusherOptions, channel: channelName } = response.data; console.log(`📊 Stream config...channel: ${channelName}, pusherKey: ${pusherKey}, initialEvents: ${initialEvents.length}`); initialEvents.forEach((event) => { events.set(event.eventID, event); console.log(`⏳ Initial Event: ${getEventTitle(event)}`); }); console.log("🔌 Initializing WebSocket connection..."); pusher = new Pusher(pusherKey, pusherOptions); startMonitoring(); // Subscribe to channel with error handling console.log(`📺 Subscribing to channel: ${channelName}`); channel = pusher.subscribe(channelName); channel.bind("pusher:subscription_succeeded", () => { console.log(`✅ Successfully subscribed to channel: ${channelName}`); connectionState = "subscribed"; }); channel.bind("data", async (changedEvents) => { console.log(`🔔 Received change notification for ${changedEvents.length} event(s)`); const eventIDs = changedEvents.map((e) => e.eventID).join(","); if (!eventIDs) return; console.log(`🔍 Fetching full data for events: ${eventIDs}`); const response = await axios .get(`${API_BASE_URL}/events`, { headers: { "x-api-key": API_KEY }, params: { eventIDs }, }) .catch(({ data }) => data); if (!response?.data?.length) return; console.log(`📦 Received ${response.data.length} updated events`); response.data.forEach((current) => { const prev = events.get(current.eventID); if (!prev) { console.log(`🆕 New Event: ${getEventTitle(current)}`); return; } else if (!prev.status.started && current.status.started) { console.log(`🏈 Event started: ${getEventTitle(current)}`); return; } else { console.log(`🔄 Event updated: ${getEventTitle(current)}`); } events.set(current.eventID, current); }); }); console.log("🎯 Setup complete, waiting for events..."); } catch (error) { console.log(error); console.error("❌ Failed to connect:", error.message); connectionState = "failed"; } }; const disconnect = () => { console.log("🔌 Disconnecting from stream..."); if (heartbeatInterval) clearInterval(heartbeatInterval); if (channel?.name) pusher.unsubscribe(channel.name); if (pusher) pusher.disconnect(); }; console.log(`🚀 Starting upcoming event tracker for: ${LEAGUE_ID}...`); connect(); process.on("SIGINT", () => { console.log("\n🛑 Shutdown signal received..."); disconnect(); }); ``` ## Feed-Specific Examples [#feed-specific-examples] ### Live Events [#live-events] Monitor all currently live games across all sports: ```js axios.get("https://api.sportsgameodds.com/v2/stream/events", { headers: { 'x-api-key': API_KEY }, params: { feed: 'events:live' } }); ``` ### Upcoming Events by League [#upcoming-events-by-league] Monitor upcoming games for a specific league: ```js axios.get("https://api.sportsgameodds.com/v2/stream/events", { headers: { 'x-api-key': API_KEY }, params: { feed: 'events:upcoming', leagueID: 'NFL' } }); ``` ### Single Event Tracking [#single-event-tracking] Track updates for one specific event: ```js axios.get("https://api.sportsgameodds.com/v2/stream/events", { headers: { 'x-api-key': API_KEY }, params: { feed: 'events:byid', eventID: 'DENVER_NUGGETS_VS_MIAMI_HEAT_2024-05-20T00:30:00Z_NBA' } }); ``` ## Connection Management [#connection-management] You'll want to be mindful of the connection state and handle errors and disconnections properly in order to maintain a reliable connection. ```js const pusher = new Pusher(pusherKey, pusherOptions); // Monitor connection state pusher.connection.bind('state_change', (states) => { console.log('Connection state:', states.current); // States: connecting, connected, unavailable, failed, disconnected }); // Handle connection errors pusher.connection.bind('error', (error) => { console.error('Connection error:', error); // Implement reconnection logic if needed }); // Clean disconnect pusher.disconnect(); ``` ## Troubleshooting [#troubleshooting] **Connection Issues** * Verify your API key has streaming permissions * Check that you haven't exceeded your concurrent stream limit * Ensure proper Pusher client library installation **No Updates Received** * Confirm you're subscribed to the correct channel * Verify the feed parameters match available events * Check network connectivity and firewall settings **High Memory Usage** * Implement data cleanup for old events * Store only necessary event properties * Use efficient data structures (Map vs Object) For additional support, contact our support team with your API key and specific error messages. --- # Best Practices and Common Mistakes URL: https://sportsgameodds.com/docs/info/best-practices # Best Practices and Common Mistakes [#best-practices-and-common-mistakes] This guide covers our recommended patterns as well as some common mistakes to avoid when working with the SportsGameOdds API. ## Patterns to Follow [#patterns-to-follow] ### Set up a server-side process to sync data from the API into your database [#set-up-a-server-side-process-to-sync-data-from-the-api-into-your-database] * This is the most secure and scalable way to ingest API data * A simple cron job can handle this well. * Since you're syncing the data, you're in full control of how up-to-date your data is. * If your app starts doing well and you get 10x more traffic, your API calls remain consistent and predictable. ### Calculate your expected usage [#calculate-your-expected-usage] * Use these factors to determine how many requests/objects you'll need: * How many leagues you're tracking data for * How frequently you want to refresh your data * How many games you want track/update at a time for each league * All games? Then determine how many that will roughly be at each time of year based on active sports seasons. * All games in the next 24 hours? 48 hours? week? month? If so, what do those numbers look like for each tracked league? * Only games with active odds markets? If so, then what do the average numbers look like for each tracked league? ### Specify `oddIDs` in `/events` requests when you only need specific odds markets [#specify-oddids-in-events-requests-when-you-only-need-specific-odds-markets] * Including this parameter can significantly reduce the respoonse payload size and improve response times. ### Implement retry logic on 500-level errors [#implement-retry-logic-on-500-level-errors] * If you ever receive a 500-level error, you should wait a short period, then retry a single time. * In most cases, you'll get a successful response on the second attempt. * If you don't then stop retrying and log the error. ### Use query params to filter data [#use-query-params-to-filter-data] * Filtering at the API level means less irrelevant/unused data is returned to you. * This improves response times and reduces your costs. ### Use `eventID` by itself for direct lookups [#use-eventid-by-itself-for-direct-lookups] * When you already know which event(s) you want, query by `eventID` or `eventIDs` — this is the fastest query path. * Note that when `eventID`/`eventIDs` are provided, all other filters (`sportID`, `leagueID`, `live`, `started`, `startsAfter`, etc.) are ignored. Only the eventIDs are used for filtering. * Response-shaping parameters like `oddID`, `bookmakerID`, and `playerID` still apply normally, so you'll only get odds/bookmakers/players inside each event corresponding to your filters but you may see events without any odds/bookmakers/players due to that filtering. ### Handle ID changes with `aliases` [#handle-id-changes-with-aliases] * An `eventID`, `teamID` or `playerID` almost never changes, but it can happen (for example, when we merge two duplicate entries). * When it does, we add the old ID to the `aliases` array of the Event, Team or Player that replaced it. * If you request the old ID and supply *only* the `eventID`, `teamID` or `playerID` param, you get the new Event, Team or Player back. * If the ID in a response doesn't match the one you asked for, check the object's `aliases` for your ID and update your saved data to the new one. * We only check `aliases` when none of the IDs you requested exist. If an ID is missing from a batch response, request it on its own to see whether it changed. ### Use the `limit` and `cursor` parameters together [#use-the-limit-and-cursor-parameters-together] * Before you start using the `cursor` parameter, increase the `limit` parameter so you get more results per request. ### Keep your API key secure [#keep-your-api-key-secure] * Never expose it in your frontend code. * Never commit it to version control. ### Monitor your usage [#monitor-your-usage] * Check the `/account/usage` endpoint to see how you're doing against your rate limits ### Handle missing fields defensively [#handle-missing-fields-defensively] * Some fields may not always be available across all of our data. You should handle such cases gracefully. * Typically, critical fields will always have a value (ex: Events will always have an `eventID`, `sportID`, `leagueID`), but less critical fields won't. * For example, use `Event.teams.home.names.long || Event.teams.home.names.medium || Event.teams.home.names.short` rather than assuming `Event.teams.home.names.long` is always available. ### Vary Your Polling/Caching Intervals [#vary-your-pollingcaching-intervals] * Odds change very infrequently when a game is far in the future * Many markets aren't even offered until a game is less than 24-48 hours away * You can save on API calls polling less frequently for games in the far future and more frequently for games in the near future. ### Learn the oddID Structure [#learn-the-oddid-structure] * Knowing this structure and what the individual values mean will make working with the API much faster and easier * `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}-{bookmakerID}` ## Anti-Patterns to Avoid [#anti-patterns-to-avoid] ### Making API Requests from a Frontend/Browser [#making-api-requests-from-a-frontendbrowser] * Frontend code (ex: React, Vue, etc.) should never make API requests directly. * This exposes your API key to the public. * In general, to avoid this you have two options: * Set up a server-side proxy that makes the API requests and returns the data to the frontend. * Set up a server-side process to sync data from the API into your database. Query your database from your frontend. ### Polling on an Interval on the Free/Amateur plan [#polling-on-an-interval-on-the-freeamateur-plan] * If you set up a system that makes a number of API requests continuously on an interval, you'll quickly hit your limits on the Amateur tier * We recomend only running your API requests manually and upgrading to a higher-limit plan when you're ready to set something like this up. ### Polling Too Frequently [#polling-too-frequently] * Based on your plan, your data may not update more than every 30 seconds to 1 minute * Polling more frequently than this wastes your rate limit quota ### Always Polling All Upcoming Games [#always-polling-all-upcoming-games] * Instead, cache data for longer when a game is happening far in the future * Such games likely won't have frequent odds movements * This can help you save on API calls and reduce your costs ### Not Considering Response Codes [#not-considering-response-codes] * For example, if you get a 429 (Rate Limit Exceeded) error, you should wait before retrying * Not waiting is just going to cause you to burn through your rate limit quota faster * You'll also want to consider whether you've hit a minute-level, month-level, or other rate limit. Use the `/account/usage` endpoint to check your usage. ### Not Considering Error Messages [#not-considering-error-messages] * Whenever you get an error response, there will be an `error` field at the top-level of the response body * This field will tell you what went wrong and why * In most cases, you should log this info and use it to help debug any problems you're encountering ### Always Including `includeAltLines=true` in `/events` requests [#always-including-includealtlinestrue-in-events-requests] * Don't include alt lines unless you actually need them * They can significantly increase the response payload size and slow down your response times ### Using startsAfter/startsBefore when unneeded [#using-startsafterstartsbefore-when-unneeded] * The startsAfter and startsBefore parameters can impede query performance in some cases * Performance suffers the most when it's combined with other less-performant parameters such as: * `playerID`, `bookmakerID`, `teamID`, `includeAltLines` * Try to re-evaluate whether you can accomplish the same thing without these parameters (sometimes you can, sometimes you can't) ### Using too many query parameters [#using-too-many-query-parameters] * Queries are best optimized for requests that use 1-3 parameters (excluding `limit`, `cursor`, `apiKey`) * Using more than this can make your queries slower in some cases ### Filtering Results on the Client [#filtering-results-on-the-client] * Our API can return very large payloads. * Therefore, your goal should be to filter as much as possible at the API level and not in your code * That way, you don't have to download and parse unnecessary data. * This is best accomplished with query parameters ### Using Wrong/Invalid Query Parameters [#using-wronginvalid-query-parameters] * See our [Reference Docs](/docs/reference) for more information on the valid query parameters for each endpoint ### Not Monitoring Usage [#not-monitoring-usage] * You want to keep track of your usage in order to optimize your usage patterns and avoid hitting your rate limits. * Use the `/account/usage` endpoint to check your usage. --- # Data Types Overview - API Schema Reference URL: https://sportsgameodds.com/docs/data-types # Data Types Overview - API Schema Reference [#data-types-overview---api-schema-reference] The API organizes data into a hierarchy. Understanding these relationships helps you navigate the API efficiently and build powerful integrations. Odds markets are identified using an **oddID** which is made up of a combination of: **`{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}`** | Data Type | Identifier | Description | Example Values | | ------------------------------------------- | -------------- | ----------------------------- | ------------------------------------------- | | [Sports](/docs/data-types/sports) | `sportID` | Top-level sports | `BASKETBALL`, `FOOTBALL`, `SOCCER` | | [Leagues](/docs/data-types/leagues) | `leagueID` | Leagues within each sport | `NBA`, `NFL`, `EPL` | | [Bookmakers](/docs/data-types/bookmakers) | `bookmakerID` | Sportsbooks that offer odds | `draftkings`, `fanduel`, `betmgm` | | [Periods](/docs/data-types/periods) | `periodID` | Time period for a stat or bet | `game`, `1h`, `1q` | | [Stats](/docs/data-types/stats) | `statID` | Statistic being tracked | `points`, `touchdowns`, `assists` | | [Stat Entity](/docs/data-types/stat-entity) | `statEntityID` | Who the stat applies to | `home`, `away`, `all`, `LEBRON_JAMES_1_NBA` | | [Bet Types](/docs/data-types/bet-types) | `betTypeID` | Type of bet | `ml`, `sp`, `ou` | | [Sides](/docs/data-types/bet-types) | `sideID` | Outcome a bet is taking | `home`, `away`, `over`, `under` | | [Odds](/docs/data-types/odds) | `oddID` | A betting odds market | `points-all-game-ou-over` | --- # Supported Markets - Odds Coverage by League URL: https://sportsgameodds.com/docs/data-types/markets # Supported Markets - Odds Coverage by League [#supported-markets---odds-coverage-by-league] The `1ix5` periodID (1st 5 Innings) is being deprecated in favor of `1h` (1st Half) for Baseball. Please migrate any usage of `1ix5` to `1h`. Below is a list of odds markets that we make available. Each row represents a unique oddID that can be used to query our API. Additional markets (those not listed here) can be made available through a custom plan by [contacting us](/contact-us). For reference an oddID has the following format: `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}` The table below can get slightly out of date. For the most up-to-date data on supported markets, query the [`GET /markets`](/docs/endpoints/getMarkets) endpoint. It returns every market keyed by oddID as well as per-league and per-bookmaker support breakdowns. --- # bookmakerID Reference - Bookmaker Data Type URL: https://sportsgameodds.com/docs/data-types/bookmakers # Bookmaker Data Type - bookmakerID [#bookmaker-data-type---bookmakerid] Each `bookmakerID` corresponds to a sportbook, daily fantasy site, or betting platform. Some data (used for testing or consensus calculations) isn't attributable and is assigned a bookmakerID of `unknown`. Coverage details, plan availability and request examples for each book are on the [bookmakers index](/bookmakers). Not seeing a bookmaker you're looking for? More bookmakers (including ones not shown here) can be made available upon request through a custom (AllStar) plan. [Contact us](/contact-us) to discuss your needs. --- # Sports Data Type - sportID URL: https://sportsgameodds.com/docs/data-types/sports # Sports Data Type - sportID [#sports-data-type---sportid] 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](/docs/endpoints/getSports). 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. --- # League Data Type - leagueID URL: https://sportsgameodds.com/docs/data-types/leagues # League Data Type - leagueID [#league-data-type---leagueid] A `leagueID` is used to uniquely identify each sports league. Each league belongs to only one `sportID` (no cross-sport leagues), but each `sportID` typically has multiple leagues. Below is a list of leagueIDs. One thing worth mentioning is that Teams (`teamID`) and Players (`playerID`) are defined on a per-league basis. So for example, if Arsenal competes both in the English Premier League and the UEFA Champions League then they will have two different `teamID` values, `ARSENAL_EPL` and `ARSENAL_UEFA_CHAMPIONS_LEAGUE`. Not all leagues may be available to you depending on your subscription plan. Make an API request to the [`/leagues` endpoint](/docs/endpoints/getLeagues) 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](/contact-us) to discuss your needs. --- # Stat Data Type - statID URL: https://sportsgameodds.com/docs/data-types/stats # Stat Data Type - statID [#stat-data-type---statid] A **statID** corresponds to a specific statistic or value. Each sport has its own set of supported statIDs. You can use it to find event/game statistics: * When looking at the `results` object of an Event, the statID allows you to find a specific stat value at `results...`. It's also used to define odds markets: * When looking at the `odds` object of an Event, each item contains a statID corresponding to the stat which will determine the outcome of the bet. This is found at `odds..statID`. ## The `points` statID [#the-points-statid] The statID `points` is special. It's used in every sport to define the stat which determines the winner of an event. We've found that maintaining a single statID for the main (most important) stat across all sports helps keep things simple and consistent. However, this also means the `points` statID is used even in sports where the word "points" isn't necessarily used to describe that stat. Here are some examples: * In Baseball, `points` is the statID for "runs scored" * In Hockey, `points` is the statID for "goals scored" and `goals+assists` is the statID for traditional "hockey points" * In Golf, `points` is the number of strokes (to par) * In Tennis, `points` is the statID for "sets won" (since most sets won wins the match) and `truePoints` is used for the actual number of tennis points won * In MMA or Boxing, `points` is used to determine the winner of a fight. The winner will have a value of 1 and the loser will have a value of 0. For sports that can go beyond regulation, the full-game `points` stat value (from the results and team.score object) includes overtime / extra time and any penalty shootouts when applicable. See [Periods docs](/docs/data-types/periods#full-event-scores) for more info. --- # Stat Entity Data Type - statEntityID URL: https://sportsgameodds.com/docs/data-types/stat-entity # Stat Entity Data Type - statEntityID [#stat-entity-data-type---statentityid] A `statEntityID` identifies **who's** performance on a given statistic we're tracking. In other words, if the [statID](/docs/data-types/stats) tracks WHAT we're tracking, the `statEntityID` tracks WHO we're tracking. In the context of odds data, the `statEntityID` determines who's performance on a given `statID` dictates the outcome of the bet. ## Possible Values [#possible-values] | Value | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `all` | Tracks the combined performance of all teams/participants. Example: an over/under bet on the total points scored in a game uses `all` because the bet tracks points scored by both teams combined. | | `home` | Tracks the performance of the home team. | | `away` | Tracks the performance of the away team. | | *playerID* | When tracking player-specific stats or player prop bets, the `statEntityID` will be a specific playerID. | | *teamID* | Used only in Events where `type` = `tournament`. In a normal event (`type` = `match`), the `statEntityID` will be `home` or `away`. However, in tournament-type events (like a Golf tournament), there isn't a home and away team, so the `statEntityID` will be a specific teamID. | ## Usage in Odds [#usage-in-odds] In odds data, the `statEntityID` determines who's performance on a given `statID` dictates the outcome of the bet. **Example:** If you're looking at odds for an over-under rebounds bet (`sideID` = `over`, `betTypeID` = `ou`, `statID` = `rebounds`): * If `statEntityID` = `LEBRON_JAMES_LA_LAKERS` → The bet is on LeBron James' individual rebounds * If `statEntityID` = `home` → The bet is on the home team's total rebounds * If `statEntityID` = `all` → The bet is on total rebounds from both teams combined ## Fixed statEntityID Values [#fixed-statentityid-values] In some cases, the `statEntityID` has a fixed value determined by the `betTypeID` and `sideID` combination. In these cases, `statEntityID` is somewhat redundant with `sideID` but is included for consistency. **Why?** For example, if you're taking the home team on a spread bet, the outcome of your bet is always dictated by the performance of the home team. | betTypeID | sideID | statEntityID | | --------- | ----------- | ------------ | | `ml` | `home` | `home` | | `ml` | `away` | `away` | | `sp` | `home` | `home` | | `sp` | `away` | `away` | | `ml3way` | `home` | `home` | | `ml3way` | `away` | `away` | | `ml3way` | `draw` | `all` | | `ml3way` | `home+draw` | `home` | | `ml3way` | `away+draw` | `away` | | `ml3way` | `not_draw` | `all` | | `prop` | `side1` | `side1` | | `prop` | `side2` | `side2` | For more details on bet types and sides, see the [Bet Types and Sides](/docs/data-types/bet-types) documentation. --- # Period Data Type - periodID URL: https://sportsgameodds.com/docs/data-types/periods # Period Data Type - periodID [#period-data-type---periodid] A period corresponds to a unit of time in a sport or league that covers the portion of an event. Each sport has its own periods. Different odds can correspond to different periods. Each period for each sport is listed below. The `1ix5` periodID (1st 5 Innings) is being deprecated in favor of `1h` (1st Half) for Baseball. Please migrate any usage of `1ix5` to `1h`. ## Full event scores [#full-event-scores] The `game` periodID covers the **entire event**, so `results.game.{home,away}.points` — and the `teams.{home,away}.score` fields that mirror it will include extra time and penalty shootouts when applicable. In such cases the stat values for `game` = `reg` + `ot` + `so`. You may read any individual segment from its own periodID inside the `results` object: | Segment | Read from | | ------------------------------------------- | ------------------------------------------- | | Regulation only (e.g. 90 minutes in soccer) | `results.reg.{home,away}.points` | | Each half / quarter / period | `results.1h`, `results.2h`, `results.1q`, … | | Extra time / overtime only | `results.ot.{home,away}.points` | | Penalty shootout (PKs scored by each team) | `results.so.{home,away}.points` | `game.{home/away}.points` stats are always present by the time an event finalized; breakdown periods appear most but not 100% of the time, so null-check whichever one you depend on. ## Overtime in half and quarter markets [#overtime-in-half-and-quarter-markets] Overtime counts only toward the full game. `2h` is the second half of regulation (the 3rd and 4th quarters in football and basketball, innings 6-9 in baseball, the second 45 minutes in soccer), and `4q` ends when overtime begins, so neither `results.2h` nor `results.4q` ever includes overtime or extra-innings scoring. Read `results.ot` for overtime on its own, or `results.game` for the full game including it. Some sportsbooks settle their own 2nd half and 4th quarter markets **including** overtime. We publish those odds under `2h` / `4q` anyway, so in a game that goes to overtime the bookmaker may settle such a line differently from the `results.2h` / `results.4q` values we report. --- # Bet Type and Side Data Types - betTypeID and sideID URL: https://sportsgameodds.com/docs/data-types/bet-types # Bet Type and Side Data Types - betTypeID and sideID [#bet-type-and-side-data-types---bettypeid-and-sideid] A `betTypeID` corresponds to the style/grading system of a bet. A `sideID` corresponds outcome of that bet which is being selected. ## Number of Sides [#number-of-sides] For a 2-way betTypeID, there are 2 possible sideID values for each betTypeID * Example: `ou` (Over/Under) has 2 possible sideID values: `over` and `under` For a 3-way betTypeID, there are 6 possible sideID values for each betTypeID * 1 sideID for each of the 3 core outcomes plus 1 sideID for the inverse outcome of each * In a 3-way bet, one person (typically the bettor) gets 1 of the 3 outcomes and the other (typically the sportsbook) gets the other 2 outcomes. * Ex: `ml3way` (3-Way Moneyline) has an outcome for `home` (home team wins) plus the inverse of `away+draw` (away team wins or draw). We also support special-case Events where **Event.type** = `prop`. These are one-off/custom prop bets which are not part of the standard betting schema. The definitions of these bets (and their sides) are defined on the Event object itself. The betTypeID is always `prop` and the sideID is always `side1` or `side2`. If you're just getting started with your integration, we recommend you ignore these for now as the vast majority of data uses the other betTypeID and sideID values. --- # Odds Data Type - oddID URL: https://sportsgameodds.com/docs/data-types/odds # Odds Data Type - oddID [#odds-data-type---oddid] An `oddID` is a unique identifier for a specific betting option. It identifies a specific side/outcome to bet on within a specific market. ## oddID Format [#oddid-format] The oddID combines multiple identifiers into a single unique value: ``` {statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID} ``` | Component | Description | Examples | | -------------------------------------------- | ------------------------------ | ------------------------------------ | | [statID](/docs/data-types/stats) | The statistic being wagered on | `points`, `touchdowns`, `assists` | | [statEntityID](/docs/data-types/stat-entity) | Who the stat applies to | `home`, `away`, `all`, or a playerID | | [periodID](/docs/data-types/periods) | The time period covered | `game`, `1h`, `1q` | | [betTypeID](/docs/data-types/bet-types) | The type of bet | `ml`, `sp`, `ou` | | [sideID](/docs/data-types/bet-types) | Which side of the bet | `home`, `away`, `over`, `under` | ## Example oddIDs [#example-oddids] | oddID | Description | | --------------------------------------- | --------------------------------------------------- | | `points-home-game-ml-home` | Moneyline bet on the home team to win the full game | | `points-away-game-sp-away` | Spread bet on the away team for the full game | | `points-all-game-ou-over` | Over bet on total points for the full game | | `points-home-1h-ml-home` | Moneyline bet on the home team to win the 1st half | | `assists-LEBRON_JAMES_NBA-game-ou-over` | Over bet on Lebron James assists for the full game | ## Accessing Odds [#accessing-odds] Each event returned by the [`/events` endpoint](/docs/endpoints/getEvents) contains odds data in the `odds` field. The `odds` field is an object where each key is an `oddID` and the value contains all data related to that betting option. In other words, you can access odds data by looking at `odds.` for a given oddID. ```json { "eventID": "...", "odds": { "points-home-game-ml-home": { "oddID": "points-home-game-ml-home", "statID": "points", "statEntityID": "home", "periodID": "game", "betTypeID": "ml", "sideID": "home", "fairOdds": "-110", "bookOdds": "-115", "byBookmaker": { "draftkings": { "odds": "-112", "available": true }, "fanduel": { "odds": "-118", "available": true } } }, "points-all-game-ou-over": { "oddID": "points-all-game-ou-over", "statID": "points", "statEntityID": "all", "periodID": "game", "betTypeID": "ou", "sideID": "over", "fairOdds": "-108", "fairOverUnder": "224.5", "bookOdds": "-110", "bookOverUnder": "224.5", "byBookmaker": { "draftkings": { "odds": "-110", "overUnder": "224.5", "available": true } } } } } ``` ## Key Odds Fields [#key-odds-fields] | Field | Description | | --------------------------------- | ------------------------------------------------- | | `fairOdds` | Consensus odds without juice (vig removed) | | `bookOdds` | Consensus odds across bookmakers (includes juice) | | `fairSpread` / `bookSpread` | The spread/handicap value (for spread bets) | | `fairOverUnder` / `bookOverUnder` | The over/under line (for O/U bets) | | `byBookmaker` | Odds broken down by individual bookmaker | | `started` / `ended` | Whether the bet period has started or ended | | `score` | The final result value (when available) | ### Open/Close Odds by Bookmaker [#openclose-odds-by-bookmaker] The `byBookmaker` object can include historical open and close values when requested with `includeOpenCloseOdds=true`: | Field | Description | | ---------------- | ------------------------------------------------------ | | `openOdds` | Odds when first available for betting at this book | | `closeOdds` | Odds at event start time at this book | | `openSpread` | Spread when first available for betting at this book | | `closeSpread` | Spread at event start time at this book | | `openOverUnder` | O/U line when first available for betting at this book | | `closeOverUnder` | O/U line at event start time at this book | These fields are **not included by default** to reduce response size. Add `includeOpenCloseOdds=true` to your request to include them. ## Filtering by oddID [#filtering-by-oddid] Use the `oddID` query parameter on the `/events` endpoint to request specific odds: ``` /events?oddID=points-home-game-ml-home,points-all-game-ou-over ``` Use `includeOpposingOdds=true` to automatically include the opposite side of each requested oddID. See [Supported Markets](/docs/data-types/markets) for a complete list of oddID values supported for each league and bookmaker. --- # GET /events - Fetch Games, Odds, and Results URL: https://sportsgameodds.com/docs/endpoints/getEvents Get a list of Events *** ## Usage Notes [#usage-notes] When you provide `eventID` or `eventIDs`, the API uses only those IDs to fetch results — all other query filters (`sportID`, `leagueID`, `live`, `started`, `finalized`, `startsAfter`, `startsBefore`, etc.) are ignored. Response-shaping parameters like `oddID`, `bookmakerID`, `playerID`, `includeAltLines`, and `includeOpposingOdds` still apply as expected. --- # GET /teams - Fetch Team Data URL: https://sportsgameodds.com/docs/endpoints/getTeams Get a list of Teams by ID or league --- # GET /players - Fetch Player Data URL: https://sportsgameodds.com/docs/endpoints/getPlayers Get a list of Players for a specific Team or Event --- # GET /sports - List All Sports URL: https://sportsgameodds.com/docs/endpoints/getSports Get a list of sports --- # GET /leagues - List All Leagues URL: https://sportsgameodds.com/docs/endpoints/getLeagues Get a list of Leagues --- # GET /stats - List All Stats URL: https://sportsgameodds.com/docs/endpoints/getStats Get a list of StatIDs --- # GET /markets - Fetch Market Metadata URL: https://sportsgameodds.com/docs/endpoints/getMarkets Get a list of Markets --- # GET /account/usage - Check API Usage and Limits URL: https://sportsgameodds.com/docs/endpoints/getUsageData Get rate-limits and usage data about your API key --- # GET /stream/events - Real-Time Odds Streaming URL: https://sportsgameodds.com/docs/endpoints/streamEvents Setup streamed (WebSocket) connection --- # Sports Betting Glossary - Terms and Definitions URL: https://sportsgameodds.com/docs/info/glossary # Sports Betting Glossary - Terms and Definitions [#sports-betting-glossary---terms-and-definitions] A guide to sports betting terminology and how it relates to the SportsGameOdds API. ## Aliases [#aliases] A list of IDs an Event, Team or Player used to have. IDs almost never change, but when one does we add the old `eventID`, `teamID` or `playerID` to the new object's `aliases`. Requesting the old ID on its own returns the new object. See [Best Practices](/docs/info/best-practices#handle-id-changes-with-aliases). ## Alternate Lines (Alt Lines) [#alternate-lines-alt-lines] Non-standard point spreads or totals offered at different prices. In the API, alternate lines appear as multiple entries for the same market with different `line` values. ## American Odds [#american-odds] Standard US format using positive/negative numbers. Negative odds (e.g., -110) indicate how much to risk to win $100. Positive odds (e.g., +150) indicate how much you win on a $100 bet. ## API Key [#api-key] A unique authentication token required to access the SportsGameOdds API. Passed via the `x-api-key` header in all API requests. ## Arbitrage (Arb) [#arbitrage-arb] Placing bets on all possible outcomes of an event at different bookmakers to guarantee profit by exploiting odds discrepancies. See our [Arbitrage Calculator recipe](/docs/examples/arbitrage-calculator). ## ATS [#ats] Against the Spread - betting on a team to cover the point spread. ## Bad Beat [#bad-beat] Losing a bet in an unlikely or devastating way after appearing to have won, such as a last-second score changing the cover. ## betTypeID [#bettypeid] Identifier for the type of bet: `ml` (moneyline), `sp` (spread), `ou` (over/under), `yn` (yes/no), `eo` (even/odd), or `prop` (proposition). Part of the `oddID` format. See [Bet Types](/docs/data-types/bet-types). ## Book Odds [#book-odds] Consensus odds across all bookmakers including juice. Fields include `bookOdds`, `bookSpread`, and `bookOverUnder`. Represents the average market price with vig included. ## Bookmaker ID [#bookmaker-id] Unique identifier for each sportsbook in the API (e.g., `draftkings`, `fanduel`, `betmgm`). See [Bookmakers reference](/docs/data-types/bookmakers). ## BTTS [#btts] Both Teams To Score - a soccer bet on whether both teams will score in the match. ## Buying Points [#buying-points] Paying additional juice to move the spread or total in your favor. For example, buying a half-point to move from -3 to -2.5. Not commonly available in API data. ## Closing Line [#closing-line] The final odds before an event starts. The closing line reflects all available information and is considered the most efficient, accurate price. In the API, closing lines are available via the `closing` field. ## CLV [#clv] Closing Line Value - the difference between the odds you received and the closing line. Consistently beating the closing line indicates sharp betting. ## Consensus Line [#consensus-line] The average or median odds across all bookmakers, useful for identifying the fair market price and spotting outlier opportunities. See our [Consensus Odds guide](/docs/info/consensus-odds). ## Cover [#cover] When a team beats the point spread. For example, if the Lakers are -5.5 and win by 6 or more points, they "covered" the spread. ## Cursor [#cursor] A pagination token used to fetch the next batch of results from the API. Provided in the `nextCursor` field of responses. See [Data Batches guide](/docs/guides/data-batches). ## Decimal Odds [#decimal-odds] Common in Europe and Australia. Represents total payout per $1 wagered, including your original stake. Example: 1.91 means a $1 bet returns $1.91 total ($0.91 profit). ## DNB [#dnb] Draw No Bet - a soccer bet where your stake is refunded if the match ends in a draw. ## EV [#ev] Expected Value - the average amount you can expect to win or lose per bet over the long term, based on probability and payout. ## Fair Odds [#fair-odds] Consensus odds with the juice (vig) removed, representing a more accurate probability. Fields include `fairOdds`, `fairSpread`, and `fairOverUnder`. See [Consensus Odds guide](/docs/info/consensus-odds). ## Favorite [#favorite] The team expected to win, indicated by negative odds in American format (e.g., -150). ## Fractional Odds [#fractional-odds] Traditional UK format showing profit relative to stake. Example: 10/11 means you win $10 profit for every $11 wagered. ## Futures [#futures] Long-term bets on season outcomes like championship winners, MVP awards, or team win totals. ## Handle [#handle] The total amount of money wagered on an event or market. ## Hedge [#hedge] Placing a bet on the opposite side of an existing bet to guarantee profit or reduce potential losses. Commonly used when the first bet is in a favorable position. ## Hook [#hook] A half-point in the spread or total (e.g., the .5 in -7.5). Prevents pushes by ensuring one side always wins. ## Implied Probability [#implied-probability] The probability of an outcome implied by the odds, calculated as `(1 / decimalOdds) * 100`. The sum of both sides' implied probabilities exceeds 100% due to the vig (bookmaker's commission). ## Juice / Vig (Vigorish) [#juice--vig-vigorish] The bookmaker's commission built into the odds. For example, when both sides are -110, the combined implied probability is \~104.76%, with the extra \~4.76% being the juice. ## League ID [#league-id] Unique identifier for each league or competition in the API (e.g., `nba`, `nfl`, `epl`). See [Leagues reference](/docs/data-types/leagues). ## Line Movement [#line-movement] Changes to odds or point spreads over time. Line movement is driven by sharp money, public betting patterns, injury news, weather conditions, and other market factors. ## Live Betting (In-Play) [#live-betting-in-play] Bets placed after an event has started, with odds updating in real-time as the game progresses. Use `live=true` in event queries to filter for live events. ## Middle [#middle] Betting both sides of an event at different lines, creating a scenario where both bets win if the result lands between the two lines. For example, betting Team A -3 and Team B +7. ## ML [#ml] Moneyline - a straight up bet on which team wins. ## Moneyline [#moneyline] A straight up bet on which team wins the game outright. Negative odds indicate the favorite (amount you risk to win $100), positive odds indicate the underdog (amount you win on a $100 bet). In the API, moneylines use `betTypeID: "ml"`. ## O/U [#ou] Over/Under - betting on whether the total score will be over or under a set number. ## Object [#object] The API's main pricing unit. An object is a single item returned in an API response. This can be an event (game), a league, a team, a player, etc. A single API query might return multiple objects, so if a query returned 10 events, then it would cost 10 objects. It's worth noting that we don't charge per odds market or bookmaker value returned, we only charge per object returned. This makes the SportsGameOdds API 60-80% cheaper than other APIs. ## oddID [#oddid] Unique identifier for each odd in the API, formatted as `{statID}-{statEntityID}-{periodID}-{betTypeID}-{sideID}-{bookmakerID}`. See our [Odds guide](/docs/data-types/odds). ## Opening Line [#opening-line] The first odds posted by a bookmaker for an event. In the API, opening lines are available via the `opening` field within `byBookmaker`. ## Parlay [#parlay] A single bet combining multiple selections where all must win for the bet to pay out. Odds multiply together for higher risk and higher reward. ## Period ID [#period-id] Identifier for the time period of a bet in the API: `game` for full game, `h1` for first half, `q1` for first quarter, `reg` for regulation only, etc. See [Periods reference](/docs/data-types/periods). ## PL [#pl] Puck Line - hockey's version of the point spread, typically set at ±1.5 goals. ## Positive EV (Expected Value) [#positive-ev-expected-value] A bet with a positive expected return, meaning it's mathematically profitable. ## Props (Proposition Bets) [#props-proposition-bets] Bets on specific events or player performances within a game, such as "LeBron James Over 28.5 Points" or "First Team to Score." In the API, props typically use `betTypeID: "ou"`, `"yn"`, or `"prop"`. ## Public Money [#public-money] Bets from recreational bettors. Public money tends to favor favorites and overs, often following popular teams and narratives. ## Push [#push] When the result lands exactly on the line and the bet is refunded with no win or loss. Half-point lines (e.g., -5.5) prevent pushes. ## RL [#rl] Run Line - baseball's version of the point spread, typically set at ±1.5 runs. ## ROI [#roi] Return on Investment - the percentage return on your betting bankroll over time. ## SGP [#sgp] Same Game Parlay - a parlay that combines multiple bets from the same game, such as a team to win plus a player prop. ## Sharp Money [#sharp-money] Bets placed by professional, well-informed bettors. Sharp action often causes significant line movement as bookmakers respect and adjust to these bets. ## sideID [#sideid] Identifier for which side of a bet: `home`, `away`, `over`, `under`, `yes`, `no`, `draw`, etc. Part of the `oddID` format. See [Bet Types](/docs/data-types/bet-types). ## Spread (Point Spread) [#spread-point-spread] A handicap applied to the favorite to level the playing field. If Lakers are -5.5, they must win by 6+ points to cover the spread. In the API, spreads use `betTypeID: "sp"`. ## Stat Entity ID [#stat-entity-id] Identifies who the stat applies to: `home` (home team), `away` (away team), `all` (both teams combined), or a specific `playerID`. Part of the `oddID` format. See [Stat Entity reference](/docs/data-types/stat-entity). ## Stat ID [#stat-id] Identifier for the statistic being bet on in the API (e.g., `points`, `rebounds`, `assists`, `touchdowns`). See [Stats reference](/docs/data-types/stats). ## Steam Move [#steam-move] Sudden, significant line movement across multiple bookmakers simultaneously. Steam moves typically indicate sharp money or important new information entering the market. ## Total (Over/Under) [#total-overunder] A bet on whether the combined score of both teams will be over or under a specified number. In the API, totals use `betTypeID: "ou"` with `sideID: "over"` or `"under"`. ## Underdog [#underdog] The team expected to lose, indicated by positive odds in American format (e.g., +150). --- # Error Codes and Troubleshooting URL: https://sportsgameodds.com/docs/info/errors # Error Codes and Troubleshooting [#error-codes-and-troubleshooting] All API errors return an applicable HTTP status code (400-level or 500-level) along with the following JSON body: ```json { "success": false, "error": "Human-readable error description" } ``` ## TLDR [#tldr] | Code | Meaning | Your Action | | ---- | ------------------- | ------------------------------------------------------ | | 200 | Success | Use the data | | 400 | Bad Request | Fix parameters - check validation errors | | 401 | Unauthorized | Check API key header/parameter | | 403 | Forbidden | Check subscription status or feature access | | 404 | Not Found | Verify endpoint path | | 429 | Rate Limited | Wait and then retry. Upgrade subscriptionif persistent | | 500 | Server Error | Retry once after delay, simplify query if persistent | | 503 | Service Unavailable | Wait for service to reconnect | | 504 | Gateway Timeout | Reduce query complexity, retry once after delay | ## Types of Errors [#types-of-errors] ### Non-Standard Response Format [#non-standard-response-format] In rare cases you may receive: * An empty response body * A non-JSON response body (ex: text or html) * A response body which isn't parseable into JSON * A response body without a success field **Meaning:** These are typically due to transient networking issues or server errors. **Recommendation:** * Treat these the same as a 500-level error. * Retry the request once after a short delay * If you see these repeatedly and the request still fails after a retry, please contact support. ### 400 Bad Request [#400-bad-request] **Meaning:** Your request has invalid or missing parameters. **Troubleshooting**: * Typically the `error` field in the response will be helpful to determine what the issue is * Check the [reference](/docs/reference) docs and the [data-types](/docs/data-types) docs to help ensure you're using the correct values. * Make sure you haven't included the same parameter multiple times. * If it's a boolean parameter, make sure it's either `true` or `false` (as a string) * If it's a date parameter, make sure it's a valid ISO-formatted date or Unix timestamp. Ex: `2026-01-01T00:00:00.000Z` or `1736899200000` * In some cases, you may be using an invalid combination of parameters. These are combinations of query parameters which can't logically go together such as `live=true` with `ended=true`. ### 401 Unauthorized [#401-unauthorized] **Meaning:** Authentication failed or API key is missing. **Troubleshooting**: * Make sure you've included your API key either in the header as `x-api-key` (case-insensitive) or as a query parameter as `apiKey`. * Ensure that the API key doesn't contain any leading or trailing whitespace. * Make sure you copied your API key correctly. Check your email and ensure you're using the same key which was sent to you. * Make sure your API key hasn't been re-generated or revoked. ### 403 Forbidden [#403-forbidden] **Meaning:** Your API key doesn't have permission to access this data **Troubleshooting**: * Make sure you haven't cancelled your subscription. [Check your subscription here](/account). * If you're on a paid plan, make sure you didn't have a failed payment/billing issue * Make sure you're using the latest API version. By default API keys are not granted access to old/legacy API versions. * Certain endpoints may be restricted by tier. For example, the /stream/events endpoint is only available on the AllStar plan. Contact support to upgrade. ### 404 Not Found [#404-not-found] **Meaning:** The requested resource doesn't exist. **Troubleshooting**: * Make sure you're using the correct URL to query the API. Check for typos. * The version of your API request was invalid. Make sure your URL path starts with `/v2/` or `/v1/` or another valid version. ### 429 Too Many Requests [#429-too-many-requests] **Meaning:** You've exceeded your rate limits **Troubleshooting**: * Try waiting up to a minute and then try again * Check your rate limit usage using the `/account/usage` endpoint * If you ran out of "objects per month" then you may need to upgrade your plan ### 500 Internal Server Error [#500-internal-server-error] **Meaning:** Something went wrong on our end. **Troubleshooting**: * Wait a few seconds and then retry the request once. * If it still fails, then contact support * Consider trying a different endpoint or different set of query parameters. Sometimes active problems are isolated to only specific cases of these * In general, these errors are not your fault and should be exceedingly rare * We ask that you do not continuously retry the request as this tends to overload our servers and may result in additional errors ### 503 Service Unavailable [#503-service-unavailable] **Meaning:** API is temporarily unavailable or our server is offline **Troubleshooting**: * Wait a few seconds and then retry the request once. * If it still fails, then contact support * In some cases, these types of errors are isolated to a single server instance. If that's what's happening, your retry may work. Otherwise, it's likely a more widespread issue with our servers. ### 504 Gateway Timeout [#504-gateway-timeout] **Meaning:** Request took too long to complete. **Troubleshooting**: * Your query may be too complex. The following can help reduce query complexity: * Remove these parameters: `includeAltLines`, `startsBefore`, `startsAfter`, `playerID`/`playerIDs`, `bookmakerID`/`bookmakerIDs`, `teamID`/`teamIDs` * Reduce the overall number of query parameters you're using * Our servers may be overloaded. Wait a few seconds and then retry the request once. * If it still fails, then contact support ## Error Handling Example [#error-handling-example] ```js 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 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 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 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 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; } ``` --- # Consensus Odds - Fair and Book Calculations URL: https://sportsgameodds.com/docs/info/consensus-odds # Consensus Odds - Fair and Book Calculations [#consensus-odds---fair-and-book-calculations] ## "Lines" [#lines] * The word "line" is used in this guide to refer to the spread or over-under value associated with a given set of odds * For bet types without a line (ex: moneyline or yes/no bets), all odds are treated as if they have a line of 0 * Both sides of the bet will have mirrored line values * ex: On a spread bet, if the fairSpread for the home side is 3, then the fairSpread for the away side will be -3 * ex: On an over-under bet, if the fairOverUnder for the over side is 5, then the fairOverUnder for the under side will also be 5 ## Fair Odds [#fair-odds] ### Key Fields [#key-fields] * `fairSpread` or `fairOverUnder`: The most fair spread or over/under line (if applicable) * `fairOdds`: The most fair odds for the given bet (associated with the line if applicable) * `fairOddsAvailable`: Indicates if sufficient data was available to calculate fairOdds (and fairSpread/fairOverUnder) ### Summary [#summary] We group odds by line, perform linear regression to find the most balanced line, calculate median odds across bookmakers for that line, and remove the juice to determine fair odds. ### Calculation Process [#calculation-process] 1. **Group odds data** for each Event + oddID by `bookmakerID` + `line` 2. **Include related bet types** into the groupings if possible * ex: moneyline bets are spread bets with a line of 0, so moneyline odds are included as such in spread calculations) 3. **Get the latest odds data** from each group in cases where we have multiple values 4. **Combine these values** into a single dataset * We first attempt to do this only with odds that are available/open for betting at the given bookmaker * If that doesn't yield sufficient data, then all values will be considered (and `fairOddsAvailable` will be set to false) 5. **Perform linear regression** to estimate the line with the most even odds 6. **Choose the fair line** based on: * Separate regressions for each side of the bet determine the line's upper and lower bounds for the fair line * Regression on the entire dataset gives us a target line value * Only positive lines are considered for over-unders and non-zero lines are considered for spreads * Line closest to target value with most data points is chosen 7. **Calculate median odds** for each side across all bookmakers for the chosen line * If no bookmakers offer odds at the fair line, then we use the odds returned by our regression calculation instead 8. **Remove the juice** to get the fair odds value by: * Calculating implied probability for each side * Recalculating odds based on the combined implied probabilities of both sides ## Book Odds [#book-odds] ### Key Fields [#key-fields-1] * `bookSpread` or `bookOverUnder`: Consensus/average spread or over/under line across bookmakers * `bookOdds`: Consensus/average odds for the associated line * `bookOddsAvailable`: Indicates if odds are available/open at at least one bookmaker ### Summary [#summary-1] We identify the main line for each bookmaker, select the most common main line across all bookmakers, and calculate the median odds across bookmakers for that line. ### Full Calculation Process [#full-calculation-process] 1. **Group odds data** for each Event + oddID by `bookmakerID` + `line` 2. **Identify the main line** for each group. * If a bookmaker doesn't distinguish between main and alt lines (and we can't easily infer that based on data attributes which can vary by bookmaker), then... * We identify the main line used most frequently by other bookmakers * And identify this bookmaker's main line as the line closest to that one 3. **Get the latest odds data** from each group in cases where we have multiple values 4. **Combine these values** into a single dataset * We first attempt to do this only with odds that are available/open for betting at the given bookmaker * If that doesn't yield sufficient data, then all values will be considered 5. **Select the consensus book line** by applying the following criteria: * The line which has the most data points (ie: the largest number of bookmakers have identified this as their main line) * If two have the same number of data points, then the line closest to the consensus fair line is selected 6. **Calculate consensus book odds** as the median odds across all bookmakers for the selected line --- # Migration Guide - V1 to V2 API URL: https://sportsgameodds.com/docs/info/v1-to-v2 # Migration Guide - V1 to V2 API [#migration-guide---v1-to-v2-api] ## Overview of Changes [#overview-of-changes] * **Odds can update significantly more frequently** - We overhauled our system which aggregates and syncs odds data to the API, allowing us to significantly speed up the polling frequency of odds data and offer data at less than 50% the delay of the previous system. Effects on your data depend on tier. * **Faster API Response Times** - When fetching data from the API, you should receive data significantly faster. This change affects both v1 and v2 endpoints. Note that v2 responses often contain more data than v2 which may affect response times. * **Combined /odds and /events endpoints** - You no longer need to query one endpoint to get odds breakdowns by sportsbook and another endpoint to get event metadata (status, teams, players, results, etc.). All of this data has now been combined into a single `/v2/events` endpoint. * **Added deeplinks to odds data** - We've added a deeplink field to the bookmaker-specific odds data. Only major US sportsbooks were included at launch (FanDuel, Draftkings, BetMGM) with more to come soon after. * **Bookmaker-specific odds persist when unavailable/inactive** - Previously, odds for a specific bookmaker (under the byBookmaker field) would only show when those odds were actively being offered (open for wagers) by that bookmaker and would disappear when no longer offered. In v2, all odds (both for main lines and alt lines) will show along with an `available` field which tells you whether those odds are actively being offered (open for wagers). * **Improved Player System** - Players are now unique across all teams in a league. This means a player will have the same playerID regardless of which team they are on. This new player system also allows us to return a higher number of player props odds and results/stats data when querying Events. * **More request options added to Events (/Odds) endpoint** We added 10 additional parameters to the /events endpoint to help you zero-in on the data you need. Even more options are coming soon. ## Upgrade Guide [#upgrade-guide] Some of the changes made to the v2 API cause the /events endpoint it to return a lot of data. We highly recomend you make use of the new params we've added to the /events endpoint in order ensure you're only requesting the data you need. This helps keep the API running fast for both yourself as well as everyone else. Thanks! ### Change the URLs you use to access the API to use the new v2 endpoints by simply swapping `v1` for `v2`. [#change-the-urls-you-use-to-access-the-api-to-use-the-new-v2-endpoints-by-simply-swapping-v1-for-v2] ```diff - https://api.sportsgameodds.com/v1/... + https://api.sportsgameodds.com/v2/... ``` ### Change any request to either **v1/events** or v1/odds endpoints to use the new combined v2/events endpoint (there is no v2/odds endpoint) [#change-any-request-to-either-v1events-or-v1odds-endpoints-to-use-the-new-combined-v2events-endpoint-there-is-no-v2odds-endpoint] ```diff - https://api.sportsgameodds.com/v1/events... - https://api.sportsgameodds.com/v1/odds... + https://api.sportsgameodds.com/v2/events... ``` ### The v2/events endpoint now returns everything found in the v1/events + v1/odds endpoints [#the-v2events-endpoint-now-returns-everything-found-in-the-v1events--v1odds-endpoints] If you were parsing data from the **v1/events endpoint's odds object**, (not the v1/odds endpoint's odds object) then you will need to make some changes: * `Event.odds..odds` -> `Event.odds..fairOdds` * `Event.odds..spread` -> `Event.odds..fairSpread` * `Event.odds..overUnder` -> `Event.odds..fairOverUnder` * `Event.odds..closeOdds` -> `Event.odds..closeFairOdds` * `Event.odds..closeSpread` -> `Event.odds..closeFairSpread` * `Event.odds..closeOverUnder` -> `Event.odds..closeFairOverUnder` * `Event.odds..isFallbackOdds` -> `REMOVED` * We previously showed placeholder even money odds marked with `isFallbackOdds = true` when odds were not yet available. This is no longer the case in v2. ### Some event status fields changes [#some-event-status-fields-changes] * `Event.status.hasMarketOdds` -> `Event.status.oddsPresent` * `Event.status.hasAnyOdds` -> `Event.status.oddsPresent` * `Event.status.anyOddsAvailable` -> `Event.status.oddsAvailable` * `Event.status.marketOddsAvailable` -> `Event.status.oddsAvailable` * `Event.status.nextUpdateAt` -> `REMOVED` ### Event players changed in certain cases [#event-players-changed-in-certain-cases] The Event.players object returned by the /v2/events endpoint will include each player's new playerID. On v1 of the API, any players already attached to an event (meaning there were odds or results data on the event for that player) by Jan 31, 2025 will remain unaffected. However, players attached after this date will carry the new/V2 playerID on the Event. Reach out to support if this may pose a problem to you. ### Add the includeAltLines parameter to /v2/events requests if you need alt lines by bookmaker. [#add-the-includealtlines-parameter-to-v2events-requests-if-you-need-alt-lines-by-bookmaker] By default, altLines are not included in v2 responses. To also include altLines add the parameter `includeAltLines=true` to your request. Please note that this can significantly increase the amount of data returned which in turn will increase latency of response times. ### Filter out unavailable byBookmaker odds if needed [#filter-out-unavailable-bybookmaker-odds-if-needed] Odds returned in the byBookmaker now include both available and unavailable odds. Each item in byBookmaker has an `available` field to tell you whether those odds are currently available. Therefore, if you only care about bookmaker-specific odds which are available and open for wagers, you'll need to ignore/filter any byBookmaker odds where `available == false`. You'll find the available field at the following path: ```diff + Event.odds..byBookmaker..available + Event.odds..byBookmaker..altLines.[i].available ``` ### Parse player names data from different path on Player objects [#parse-player-names-data-from-different-path-on-player-objects] Player names are now wrapped in a name object and the variation `name` has been renamed to `display` * `Player.firstName` -> `Player.names.firstName` * `Player.lastName` -> `Player.names.lastName` * `Player.name` -> `Player.names.display` **NOTE:** This does not affect the player data found on Events (at `Event.players.{playerID}`) ### Update saved/stored playerIDs [#update-savedstored-playerids] If you haven't saved playerIDs anywhere in your system or wouldn't run into issues if a player's playerID changed, then you can skip this section. All playerIDs have changed.. If you have saved a list of playerID values somewhere in your system which you use to identify certain players, you will likely need to update those playerIDs to use the player's new value. The format of playerIDs changed like so: ```diff - PLAYER_NAME_TEAM_ID_LEAGUE_ID + PLAYER_NAME_NUMBER_LEAGUE_ID ``` And since players with the exact same name are rare, the vast majority of players were renamed like so: ```diff - PATRICK_MAHOMES_KANSAS_CITY_CHIEFS_NFL + PATRICK_MAHOMES_1_NFL ``` However, while this covers most cases, be warned that there are a number of discrepancies. Therefore, migrating your saved playerIDs using the above pattern is NOT RECOMMENDED. Instead we recommend the following approaches to convert between old and new playerID formats in the more accurate possible way: #### **v1 playerID -> v2 playerID** [#v1-playerid---v2-playerid] Here we recommend calling the v2/players endpoint and supplying an `alias` param. This will return a single Player object with the new playerID. Passing the old playerID as the `playerID` param on its own does the same thing. #### **v2 playerID -> v1 playerID** [#v2-playerid---v1-playerid] Here we recommend calling the v2/players endpoint providing the v2 playerID as the playerID param and then finding the v1 playerID you're looking for in the response's aliases field ## New Features & Non-Breaking Changes [#new-features--non-breaking-changes] ### The API key can now be included in the URL query params [#the-api-key-can-now-be-included-in-the-url-query-params] You no longer have to include your API key in the headers. You can also include it in the request query params instead. `https://api.sportsgameodds.com/v2/events?apiKey=YOUR_API_KEY_CAN_GO_HERE` ### New params for v2/events endpoint [#new-params-for-v2events-endpoint] * `type` - Only include Events of the specified type which can be `match`, `prop`, or `tournament` * `oddsPresent` - Whether you want only Events which do (true) or do not (false) have any associated odds markets regardless of whether those odds markets are currently available (open for wagering) * `includeOpposingOdds` - Whether to include opposing odds for each included oddID. This was renamed from `includeOpposingOddIDs` in v1 but the old name will still work. * `includeAltLines` - Whether to include alternate lines in the odds byBookmaker data * `bookmakerID` - A bookmakerID or comma-separated list of bookmakerIDs to include odds for * `teamID` - A teamID or comma-separated list of teamIDs to include Events (and associated odds) for * `playerID` - A single playerID or comma-separated list of playerIDs to include Events (and associated odds) for * `live` - Only include live Events (true), only non-live Events (false) or all Events (omit) * `started` - Only include Events which have have previously started (true), only Events which have not previously started (false) or all Events (omit) * `ended` - Only include Events which have have ended (true), only Events which have not ended (false) or all Events (omit) * `cancelled` - Only include cancelled Events (true), only non-cancelled Events (false) or all Events (omit) * `includeOpenCloseOdds` - Whether to include open and close odds values (openOdds, closeOdds, openSpread, closeSpread, openOverUnder, closeOverUnder) in the odds byBookmaker data. Defaults to false. ### New deeplink fields [#new-deeplink-fields] You'll be able to find deeplink information on a per-bookmaker basis at the following path on Event objects: `Event.odds..byBookmaker..deeplink` Deeplinks will also appear on altLines but only if they are different from the deeplink value on the main line `Event.odds..byBookmaker..altLines.[i].deeplink` ### Market name field added to odds [#market-name-field-added-to-odds] Odds items now contain a human-readable `marketName` field. ### Standardized weight, height and salary fields [#standardized-weight-height-and-salary-fields] These should now return a more consistent string-based value including units. ### Added nickname and suffix to player names [#added-nickname-and-suffix-to-player-names] These fields will show only when applicable ### Renamed Parameters with Backwards Compatibility [#renamed-parameters-with-backwards-compatibility] Certain API request options/parameters have been renamed. However all of the previous names will still work. For example, we've renamed `includeOpposingOddIDs` to `includeOpposingOdds` but the old param will also still work ### Per-request default and max limits have changed [#per-request-default-and-max-limits-have-changed] * For /events * default limit: 30 -> 10 * max limit: 300 -> 100 * For teams and players * default limit: 30 -> 50 * max limit: 300 -> 250 --- # Rate Limits by Plan - Requests and Objects URL: https://sportsgameodds.com/docs/info/rate-limiting # Rate Limits by Plan - Requests and Objects [#rate-limits-by-plan---requests-and-objects] Based on the package you chose, your API key will be limited to a certain number of requests made and/or objects returned during a given time interval. If you exceed this limit, you will receive a [`429` status code](/docs/info/errors) and will be unable to make further requests until the limit resets. While we do make changes to our standard rate limits, these changes usually won't affect existing subscribers, so unless you receive an email, your rate limit will remain the same as it was when you signed up. Therefore, the limits posted in this guide may be different from those your API key is subject to. ## Request Limits [#request-limits] Each request you make to the Sports Game Odds API server will count towards your request rate limit. * Amateur plan: 10 requests per minute * Rookie plan: 50 requests per minute * Pro plan: 300 requests per minute * All-Star plan: Unlimited requests per minute ## Object Limits [#object-limits] Each request you make to the Sports Game Odds API server may return multiple objects in the response. Each object returned will count towards your object rate limit over a given time interval. Each response counts as a minimum of 1 object. * Amateur plan: 2,500 objects per month * Rookie plan: 100,000 objects per month * Pro plan: Unlimited objects per month * All-Star plan: Unlimited objects per month ## Default Limits [#default-limits] In order to protect our servers and ensure maximum uptime, the following default limits apply to all API keys. In general you shouldn't ever hit these limits regardless of your plan, but if you do, feel free to reach out to us and we can find a solution for removing or increasing these default limits * 50k requests per hour * 300k objects per hour * 3M objects per day ## Strategies to Avoid Rate Limiting [#strategies-to-avoid-rate-limiting] To protect your application from rate limiting, you can use the following strategies: 1. Avoid a high frequency of calls to endpoints serving data that doesn't change frequently (e.g., Teams, Players, Stats). 2. Cache this data locally to avoid making unnecessary requests. 3. Make use of available query params at each endpoint to focus on only the data you need. 4. You can always fetch data about your current rate limit usage using the [`/account/usage` endpoint](/docs/endpoints/getUsageData). ## Response Filtering Notice [#response-filtering-notice] When your API key has limitations that cause data to be filtered from responses (such as limited bookmakers or restricted data types), you may receive a `notice` field in the API response. This notice informs you about what data has been filtered and can be accessed at higher API tiers. The notice field appears in responses when: * Events are filtered due to sport or league restrictions * Bookmaker odds are filtered due to bookmaker access limitations * Data types are filtered due to subscription tier restrictions Example response with notice: ```json { "success": true, "data": [...], "nextCursor": "n.1720564800000.DCtqsAt8d0GIFAvMmfzD", "notice": "Response is missing 3 events and 15 bookmaker odds. Upgrade your API key to access all data from this query." } ``` This notice is designed to help you understand when your current subscription might be limiting your access to data, allowing you to make informed decisions about upgrading your plan. ## Checking Your Rate Limit Usage [#checking-your-rate-limit-usage] You can use the `/account/usage` endpoint to get information on your rate limits. This endpoint provides details about your current usage and remaining limits. A sample API call to the endpoint is below: ```javascript fetch("https://api.sportsgameodds.com/v2/account/usage", { headers: { "X-Api-Key": "YOUR_TOKEN", }, }); ``` A sample response is below ```json { "success": true, "data": { "keyID": "abc123xyz456", "customerID": "cus_987xyz321abc", "isActive": true, "rateLimits": { "per-second": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": "n/a", "currentIntervalEntities": "n/a", "currentIntervalEndTime": "n/a" }, "per-minute": { "maxRequestsPerInterval": 1000, "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": 1, "currentIntervalEntities": "n/a", "currentIntervalEndTime": "2024-01-01T00:01:00.000Z" }, "per-hour": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": "n/a", "currentIntervalEntities": "n/a", "currentIntervalEndTime": "n/a" }, "per-day": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": "unlimited", "currentIntervalRequests": "n/a", "currentIntervalEntities": "n/a", "currentIntervalEndTime": "n/a" }, "per-month": { "maxRequestsPerInterval": "unlimited", "maxEntitiesPerInterval": 1000000, "currentIntervalRequests": "n/a", "currentIntervalEntities": 100, "currentIntervalEndTime": "2024-01-31T00:01:00.000Z" } } } } ``` --- # Live Odds Tracker Example - JavaScript URL: https://sportsgameodds.com/docs/examples/live-odds-tracker # Live Odds Tracker Example - JavaScript [#live-odds-tracker-example---javascript] Complete working example that tracks NBA odds every 30 seconds and detects line movement. ## What You'll Build [#what-youll-build] A Node.js script that: * Fetches live NBA odds every 30 seconds * Tracks line movement across all bookmakers * Alerts when odds change * Shows before/after comparisons **Perfect for:** Detecting sharp money, identifying steam moves, monitoring arbitrage opportunities ## Prerequisites [#prerequisites] * Node.js 18+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic JavaScript knowledge ## Complete Code [#complete-code] ### Step 1: Setup Project [#step-1-setup-project] ```bash mkdir odds-tracker && cd odds-tracker npm init -y npm install node-fetch@2 ``` > **Note:** We use `node-fetch@2` because v3+ is ESM-only. The `@2` ensures CommonJS compatibility. ### Step 2: Create tracker.js [#step-2-create-trackerjs] ```javascript // tracker.js const fetch = require("node-fetch"); const API_KEY = process.env.SPORTSGAMEODDS_KEY; const API_BASE = "https://api.sportsgameodds.com/v2"; const POLL_INTERVAL = 30000; // 30 seconds // Store previous odds for comparison let previousOdds = {}; // Fetch current NBA odds async function fetchNBAOdds() { try { // Use finalized=false to get upcoming games (live=true only works during live games) const response = await fetch(`${API_BASE}/events?leagueID=NBA&finalized=false&oddsAvailable=true`, { headers: { "x-api-key": API_KEY }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.data; } catch (error) { console.error("Error fetching odds:", error.message); return []; } } // Compare current odds with previous and detect changes function detectLineMovement(events) { const movements = []; events.forEach((event) => { // Get team names from the nested names structure const awayName = event.teams.away.names.long; const homeName = event.teams.home.names.long; const matchup = `${awayName} @ ${homeName}`; // Check each odd for movement Object.entries(event.odds || {}).forEach(([oddID, currentOdd]) => { // Check each bookmaker's odds for this market Object.entries(currentOdd.byBookmaker || {}).forEach(([bookmakerID, currentBookmakerOdds]) => { const key = `${event.eventID}-${oddID}-${bookmakerID}`; const previousBookmakerOdds = previousOdds[key]; if (previousBookmakerOdds) { const currentLine = currentBookmakerOdds.spread || currentBookmakerOdds.overUnder; const previousLine = previousBookmakerOdds.spread || previousBookmakerOdds.overUnder; const lineChanged = previousLine !== currentLine; const priceChanged = previousBookmakerOdds.odds !== currentBookmakerOdds.odds; if (lineChanged || priceChanged) { movements.push({ matchup, eventID: event.eventID, betType: currentOdd.betTypeID, bookmaker: bookmakerID, side: currentOdd.sideID, previous: { line: previousLine, price: previousBookmakerOdds.odds, }, current: { line: currentLine, price: currentBookmakerOdds.odds, }, lineChanged, priceChanged, }); } } // Store current odds for next comparison previousOdds[key] = { spread: currentBookmakerOdds.spread, overUnder: currentBookmakerOdds.overUnder, odds: currentBookmakerOdds.odds, lastUpdatedAt: currentBookmakerOdds.lastUpdatedAt, }; }); }); }); return movements; } // Display line movements in console function displayMovements(movements) { if (movements.length === 0) { console.log("No line movement detected"); return; } console.log(`\n${movements.length} LINE MOVEMENT(S) DETECTED\n`); movements.forEach((movement) => { console.log("----------------------------------------"); console.log(`${movement.matchup}`); console.log(`${movement.betType.toUpperCase()} (${movement.side}) - ${movement.bookmaker}`); if (movement.lineChanged) { console.log(`Line: ${movement.previous.line} -> ${movement.current.line}`); } if (movement.priceChanged) { console.log(`Price: ${movement.previous.price} -> ${movement.current.price}`); } console.log("----------------------------------------\n"); }); } // Main tracking loop async function trackOdds() { console.log(new Date().toLocaleTimeString(), "- Checking for line movement..."); const events = await fetchNBAOdds(); if (events.length === 0) { console.log("No NBA games found\n"); return; } console.log(`Tracking ${events.length} NBA game(s)`); const movements = detectLineMovement(events); displayMovements(movements); } // Start tracking console.log("Starting NBA Live Odds Tracker..."); console.log(`Polling every ${POLL_INTERVAL / 1000} seconds\n`); // Run immediately, then every 30 seconds trackOdds(); setInterval(trackOdds, POLL_INTERVAL); ``` ### Step 3: Run It [#step-3-run-it] ```bash export SPORTSGAMEODDS_KEY=your_api_key_here node tracker.js ``` ## Expected Output [#expected-output] ``` Starting NBA Live Odds Tracker... Polling every 30 seconds 7:45:32 PM - Checking for line movement... Tracking 3 NBA game(s) No line movement detected 7:46:02 PM - Checking for line movement... Tracking 3 NBA game(s) 2 LINE MOVEMENT(S) DETECTED ---------------------------------------- Boston Celtics @ Los Angeles Lakers SP (home) - draftkings Line: -5.5 -> -6.0 ---------------------------------------- ---------------------------------------- Golden State Warriors @ Phoenix Suns ML (away) - fanduel Price: +145 -> +150 ---------------------------------------- ``` ## How It Works [#how-it-works] ### 1. Fetch Odds [#1-fetch-odds] ```javascript const response = await fetch(`${API_BASE}/events?leagueID=NBA&finalized=false&oddsAvailable=true`); ``` **Query parameters:** * `leagueID=NBA` - Only NBA games * `finalized=false` - Games not yet completed * `oddsAvailable=true` - Must have odds data > **Tip:** Use `live=true` instead of `finalized=false` to only get games currently in progress. ### 2. Process Nested Bookmaker Structure [#2-process-nested-bookmaker-structure] ```javascript // Each odd has a byBookmaker object with odds from each bookmaker Object.entries(currentOdd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { const currentLine = bookmakerOdds.spread || bookmakerOdds.overUnder; const currentPrice = bookmakerOdds.odds; }); ``` The API returns odds nested by bookmaker: ```json { "byBookmaker": { "draftkings": { "odds": "-110", "spread": "-5.5", "available": true }, "fanduel": { "odds": "-108", "spread": "-5.5", "available": true } } } ``` ### 3. Compare with Previous Odds [#3-compare-with-previous-odds] ```javascript // Key includes bookmakerID since odds are per-bookmaker const key = `${eventID}-${oddID}-${bookmakerID}`; const previousBookmakerOdds = previousOdds[key]; if (previousBookmakerOdds) { const lineChanged = previousLine !== currentLine; const priceChanged = previousBookmakerOdds.odds !== currentBookmakerOdds.odds; } ``` We store every odd by a unique key (`eventID-oddID-bookmakerID`) and compare on each poll. ### 4. Detect Significant Movement [#4-detect-significant-movement] The script detects: * **Line changes** - Spread/total moves (e.g., -5.5 -> -6.0) * **Price changes** - Juice moves (e.g., -110 -> -115) ### 5. Store for Next Comparison [#5-store-for-next-comparison] ```javascript previousOdds[key] = { spread: currentBookmakerOdds.spread, overUnder: currentBookmakerOdds.overUnder, odds: currentBookmakerOdds.odds, lastUpdatedAt: currentBookmakerOdds.lastUpdatedAt, }; ``` ## Enhancements [#enhancements] ### Track Multiple Leagues [#track-multiple-leagues] ```javascript const LEAGUES = ["NBA", "NFL", "NHL"]; async function trackAllLeagues() { for (const league of LEAGUES) { console.log(`\n${league} Updates:`); const events = await fetchOdds(league); const movements = detectLineMovement(events); displayMovements(movements); } } ``` ### Add Discord/Slack Notifications [#add-discordslack-notifications] ```javascript async function sendDiscordAlert(movement) { await fetch(process.env.DISCORD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: `Line moved: ${movement.matchup}\n${movement.betType} ${movement.previous.line || "N/A"} -> ${movement.current.line || "N/A"}`, }), }); } // Call after detecting movement movements.forEach(sendDiscordAlert); ``` ### Filter by Significant Moves Only [#filter-by-significant-moves-only] ```javascript function isSignificantMove(movement) { // Only alert on moves of 0.5 points or more if (movement.lineChanged && movement.current.line && movement.previous.line) { const currentLine = parseFloat(movement.current.line); const previousLine = parseFloat(movement.previous.line); const lineDiff = Math.abs(currentLine - previousLine); return lineDiff >= 0.5; } // Or price moves of 5 points or more if (movement.priceChanged) { const currentPrice = parseInt(movement.current.price); const previousPrice = parseInt(movement.previous.price); const priceDiff = Math.abs(currentPrice - previousPrice); return priceDiff >= 5; } return false; } const significantMoves = movements.filter(isSignificantMove); ``` ### Store Historical Movement Data [#store-historical-movement-data] ```javascript const fs = require("fs"); function logMovement(movement) { const logEntry = { timestamp: new Date().toISOString(), ...movement, }; fs.appendFileSync("movement-history.jsonl", JSON.stringify(logEntry) + "\n"); } movements.forEach(logMovement); ``` ### Add Real-Time Streaming (AllStar Plan) [#add-real-time-streaming-allstar-plan] Instead of polling, use WebSocket streaming for instant updates: ```javascript // Requires AllStar plan const stream = await client.stream.events({ feed: "events:live" }); channel.bind("update", (event) => { const movements = detectLineMovement([event]); displayMovements(movements); }); ``` See [Real-time Streaming Guide](/docs/guides/realtime-streaming-api) ## Troubleshooting [#troubleshooting] ### "No NBA games found" [#no-nba-games-found] **Cause:** No NBA games currently available (off-season or no games scheduled). **Solution:** Try different leagues or check schedule: ```javascript // Try multiple leagues const LEAGUES = ["NBA", "NFL", "NHL", "MLB"]; for (const league of LEAGUES) { const events = await fetchOdds(league); if (events.length > 0) { console.log(`Found ${events.length} ${league} events`); } } ``` ### Rate Limit Errors (429) [#rate-limit-errors-429] **Cause:** Polling too frequently for your plan tier. **Solutions:** 1. Increase `POLL_INTERVAL` to 60 seconds (60000) 2. Reduce number of leagues tracked 3. Upgrade to higher tier See [Rate Limiting Guide](/docs/info/rate-limiting) ### Missing Odds Data [#missing-odds-data] **Cause:** Some games may not have odds available yet. **Solution:** Add null check: ```javascript Object.entries(event.odds || {}).forEach(([oddID, odd]) => { // Check if any bookmaker has odds before processing if (!odd.byBookmaker || Object.keys(odd.byBookmaker).length === 0) return; // ... rest of code }); ``` ## Next Steps [#next-steps] ### Combine with Other Examples [#combine-with-other-examples] * **[Arbitrage Calculator](/docs/examples/arbitrage-calculator)** - Detect arb opportunities from line movements * **[Odds Comparison Dashboard](/docs/examples/odds-comparison-dashboard)** - Visualize movements in real-time * **[Player Props Analyzer](/docs/examples/player-props-analyzer)** - Track prop line movements ### Learn More [#learn-more] * **[Understanding oddID](/docs/data-types/odds)** - Deep dive into odds structure * **[Best Practices](/docs/info/best-practices)** - Optimize your polling strategy * **[SDK Guide](/docs/sdk)** - Use our TypeScript/Python SDK for easier development --- # Arbitrage Calculator Example - Python URL: https://sportsgameodds.com/docs/examples/arbitrage-calculator # Arbitrage Calculator Example - Python [#arbitrage-calculator-example---python] Find guaranteed profit opportunities by identifying odds discrepancies across bookmakers. ## What You'll Build [#what-youll-build] A Python script that: * Scans all bookmakers for odds discrepancies * Calculates arbitrage opportunities * Shows optimal bet sizing * Calculates guaranteed profit percentage **Perfect for:** Finding risk-free profit opportunities, comparing bookmaker odds, beating the vig ## What is Arbitrage? [#what-is-arbitrage] **Arbitrage** (or "arbing") is betting on all possible outcomes across different bookmakers to guarantee profit regardless of the result. **Example:** * **Book A:** Lakers -5.5 @ +105 * **Book B:** Celtics +6.0 @ -105 By betting both sides across different books, you can sometimes lock in profit. ## Prerequisites [#prerequisites] * Python 3.8+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic Python knowledge ## Complete Code [#complete-code] ### Step 1: Setup Project [#step-1-setup-project] ```bash mkdir arb-calculator && cd arb-calculator python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install requests ``` ### Step 2: Create arb\_calculator.py [#step-2-create-arb_calculatorpy] ```python # arb_calculator.py 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 [#step-3-run-it] ```bash export SPORTSGAMEODDS_KEY=your_api_key_here python arb_calculator.py ``` ## Expected Output [#expected-output] ``` Scanning for arbitrage opportunities... Fetching NBA events... Found 8 NBA events Fetching NFL events... Found 14 NFL events Fetching NHL events... Found 6 NHL events Found 2 ARBITRAGE OPPORTUNITIES! ============================================================ #1 - Boston Celtics @ Los Angeles Lakers Market: SPREAD Guaranteed Profit: 2.34% ------------------------------------------------------------ HOME -5.5 @ +105 (fanduel) Bet: $48.78 -> Payout: $100.00 AWAY +6.0 @ +100 (draftkings) Bet: $51.22 -> Payout: $102.44 Total Stake: $100.00 Guaranteed Profit: $2.34 ============================================================ ============================================================ #2 - Tampa Bay Lightning @ Florida Panthers Market: TOTAL Guaranteed Profit: 1.15% ------------------------------------------------------------ OVER 6.0 @ -105 (betmgm) Bet: $51.22 -> Payout: $100.00 UNDER 6.5 @ +110 (caesars) Bet: $48.78 -> Payout: $102.44 Total Stake: $100.00 Guaranteed Profit: $1.15 ============================================================ ``` ## How It Works [#how-it-works] ### 1. Fetch Events with Odds [#1-fetch-events-with-odds] ```python response = requests.get( f'{API_BASE}/events', params={ 'leagueID': league, 'finalized': 'false', # Upcoming games only 'oddsAvailable': 'true' # Must have odds } ) ``` ### 2. Convert American to Decimal Odds [#2-convert-american-to-decimal-odds] ```python def american_to_decimal(american_odds): if american_odds > 0: return (american_odds / 100) + 1 # e.g., +150 -> 2.50 else: return (100 / abs(american_odds)) + 1 # e.g., -110 -> 1.909 ``` **Why decimal?** Makes arbitrage math easier. ### 3. Process Odds by Bookmaker [#3-process-odds-by-bookmaker] The API returns odds with a nested `byBookmaker` structure: ```python for odd_id, odd in event.get('odds').items(): bet_type = odd['betTypeID'] # 'sp', 'ml', 'ou' side = odd['sideID'] # 'home', 'away', 'over', 'under' # Each odd contains prices from multiple bookmakers for bookmaker_id, bookmaker_data in odd.get('byBookmaker', {}).items(): price = int(bookmaker_data['odds']) decimal_odds = american_to_decimal(price) ``` ### 4. Find Best Odds for Each Side [#4-find-best-odds-for-each-side] ```python best_home = max(sides['home'], key=lambda x: x['decimal']) best_away = max(sides['away'], key=lambda x: x['decimal']) ``` We want the **highest odds** (best payout) for each outcome. ### 5. Calculate Arbitrage [#5-calculate-arbitrage] ```python implied_prob_sum = sum(1 / odd for odd in [home_decimal, away_decimal]) if implied_prob_sum < 1: # Arbitrage exists! profit_pct = ((1 / implied_prob_sum) - 1) * 100 ``` **Math explained:** * Normal market: implied probabilities sum to >100% (bookmaker edge) * Arbitrage: implied probabilities sum to \<100% (your edge) **Example:** * Home @ 2.10 (decimal) = 47.62% implied probability * Away @ 2.10 (decimal) = 47.62% implied probability * Sum = 95.24% \< 100% -> **4.76% arbitrage!** ### 6. Calculate Optimal Stakes [#6-calculate-optimal-stakes] ```python stakes = [(100 / odd) / implied_prob_sum for odd in odds_list] ``` This distributes your total stake to guarantee equal profit on all outcomes. ## Real-World Example [#real-world-example] **Scenario:** Lakers vs Celtics | Bookmaker | Market | Odds | Decimal | Implied Prob | | ---------- | ------------ | ---- | ------- | ------------ | | FanDuel | Lakers -5.5 | +105 | 2.05 | 48.78% | | DraftKings | Celtics +6.0 | +100 | 2.00 | 50.00% | **Sum:** 48.78% + 50.00% = **98.78%** **Profit:** (1 / 0.9878) - 1 = **1.23%** **Stakes (for $100 total):** * Lakers: $100 x (48.78 / 98.78) = **$49.38** * Celtics: $100 x (50.00 / 98.78) = **$50.62** **Outcome 1:** Lakers cover -5.5 * Win $49.38 x 2.05 = $101.23 * Profit: $101.23 - $100 = **$1.23** **Outcome 2:** Celtics cover +6.0 * Win $50.62 x 2.00 = $101.24 * Profit: $101.24 - $100 = **$1.24** **Guaranteed profit either way!** ## Enhancements [#enhancements] ### Add Minimum Profit Filter [#add-minimum-profit-filter] ```python MIN_PROFIT_PCT = 1.0 # Only show 1%+ opportunities opportunities = [ opp for opp in opportunities if opp['profit_pct'] >= MIN_PROFIT_PCT ] ``` ### Include Middle Opportunities [#include-middle-opportunities] A "middle" is when you can win both bets: ```python # Lakers -5.5 @ Book A # Celtics +6.0 @ Book B # If Lakers win by exactly 6, you win BOTH bets! def find_middles(sides): for home_odd in sides['home']: for away_odd in sides['away']: home_line = float(home_odd['line'] or 0) away_line = float(away_odd['line'] or 0) gap = abs(home_line) - abs(away_line) if gap >= 0.5: # Middle exists yield { 'gap': gap, 'home': home_odd, 'away': away_odd } ``` ### Real-Time Monitoring [#real-time-monitoring] ```python 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 [#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 [#important-considerations] ### Account Limitations [#account-limitations] **Warning:** Bookmakers may limit or ban accounts that consistently arbitrage. **Tips to avoid detection:** * Don't bet only arbitrage opportunities * Vary bet sizes * Use round numbers ($50 not $49.38) * Space out bets across time * Use different devices/IPs ### Execution Speed [#execution-speed] Arbitrage opportunities disappear quickly (seconds to minutes). **Solutions:** * Use our [Real-time Streaming](/docs/guides/realtime-streaming-api) (AllStar plan) * Automate bet placement (requires bookmaker APIs) * Set up price alerts ### Line Differences [#line-differences] In our example: * Book A: -5.5 * Book B: +6.0 The 0.5 point difference creates the arbitrage opportunity (a "middle"). ### Commission & Fees [#commission--fees] Remember to account for: * Withdrawal fees * Currency conversion * Deposit bonuses with rollover requirements ## Troubleshooting [#troubleshooting] ### "No arbitrage opportunities found" [#no-arbitrage-opportunities-found] **Causes:** 1. **Efficient markets** - Bookmakers adjust quickly 2. **Not enough bookmakers** - Need odds from 5+ books 3. **Wrong timing** - Arbitrage appears closer to game time **Solutions:** * Scan more leagues * Check closer to event start * Look for less popular markets ### Negative profit calculation [#negative-profit-calculation] **Cause:** Bug in odds conversion or stake calculation. **Solution:** Add validation: ```python def validate_arbitrage(opportunity): """Verify calculations are correct""" total_stake = sum(leg['stake_pct'] for leg in opportunity['legs']) assert abs(total_stake - 100) < 0.01, "Stakes don't sum to 100%" # Verify profit on all outcomes for leg in opportunity['legs']: payout = leg['stake_pct'] * american_to_decimal(leg['odds']) profit = payout - 100 assert profit > 0, f"Negative profit on {leg['side']}" ``` --- # Odds Comparison Dashboard Example - React/Next.js URL: https://sportsgameodds.com/docs/examples/odds-comparison-dashboard # Odds Comparison Dashboard Example - React/Next.js [#odds-comparison-dashboard-example---reactnextjs] Create a React/Next.js dashboard that compares odds across multiple sportsbooks and highlights the best lines. ## What You'll Build [#what-youll-build] A web dashboard that: * Displays live odds from all bookmakers side-by-side * Highlights best odds for each market * Auto-refreshes every 30 seconds * Shows line movement indicators * Responsive design for mobile/desktop **Perfect for:** Odds comparison sites, betting platforms, research tools ## Prerequisites [#prerequisites] * Node.js 18+ * Basic React/Next.js knowledge * SportsGameOdds API key ([Get one free](/pricing)) ## Complete Code [#complete-code] ### Step 1: Create Next.js Project [#step-1-create-nextjs-project] ```bash npx create-next-app@latest odds-dashboard cd odds-dashboard npm install ``` When prompted, choose: * TypeScript: Yes * ESLint: Yes * Tailwind CSS: Yes * App Router: Yes ### Step 2: Create API Route [#step-2-create-api-route] Create `app/api/odds/route.ts`: ```typescript // app/api/odds/route.ts 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 [#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 [#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 [#step-5-add-environment-variable] Create `.env.local`: ```bash SPORTSGAMEODDS_KEY=your_api_key_here ``` ### Step 6: Run It [#step-6-run-it] ```bash npm run dev ``` Open `http://localhost:3000` in your browser ## Expected Output [#expected-output] **Features you'll see:** * All bookmakers side-by-side * Best odds highlighted in green * League switcher (NBA/NFL/NHL/MLB) * Auto-refresh every 30 seconds * Start time for each game * Best odds summary at bottom ## How It Works [#how-it-works] ### 1. Proxy API Calls Through Backend [#1-proxy-api-calls-through-backend] ```typescript // app/api/odds/route.ts const response = await fetch(`${API_BASE}/events?leagueID=${league}&finalized=false`, { headers: { "x-api-key": API_KEY }, next: { revalidate: 30 }, // Cache for 30 seconds }); ``` **Why backend?** Keeps API key secure (never exposed in browser). ### 2. Process Nested Bookmaker Structure [#2-process-nested-bookmaker-structure] The API returns odds with a nested `byBookmaker` structure: ```json { "odds": { "points-home-game-sp-home": { "oddID": "points-home-game-sp-home", "betTypeID": "sp", "sideID": "home", "periodID": "game", "byBookmaker": { "draftkings": { "odds": "-110", "spread": "-5.5", "available": true }, "fanduel": { "odds": "-108", "spread": "-5.5", "available": true } } } } } ``` We transform this into a grouped format by bookmaker: ```typescript const groupOddsByMarket = (odds, betType) => { const grouped = {}; Object.values(odds).forEach((odd) => { if (odd.betTypeID !== betType || odd.periodID !== "game") return; Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { if (!grouped[bookmakerID]) { grouped[bookmakerID] = {}; } grouped[bookmakerID][odd.sideID] = bookmakerOdds; }); }); return grouped; }; ``` ### 3. Find Best Odds [#3-find-best-odds] ```typescript // Find best odds across all bookmakers for a specific market let bestBookmaker = null; let bestOddsValue = -Infinity; Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { const oddsNum = parseInt(bookmakerOdds.odds); // Higher (less negative or more positive) odds = better value if (oddsNum > bestOddsValue) { bestOddsValue = oddsNum; bestBookmaker = bookmakerID; } }); ``` **Higher odds value = better odds** for bettors (e.g., +150 is better than +120, -110 is better than -120). ### 4. Highlight Best Odds [#4-highlight-best-odds] ```typescript className={ bestAwaySpread === bookmaker ? 'bg-green-100 border-2 border-green-500 font-bold' : 'bg-gray-100' } ``` Green highlight shows best available odds. ### 5. Auto-Refresh [#5-auto-refresh] ```typescript useEffect(() => { fetchOdds(); const interval = setInterval(fetchOdds, 30000); // Every 30s return () => clearInterval(interval); }, [league]); ``` ## Enhancements [#enhancements] ### Add Moneyline and Totals [#add-moneyline-and-totals] ```typescript // In OddsDashboard component const [selectedMarket, setSelectedMarket] = useState('sp'); // Market selector // Use selectedMarket in groupOddsByMarket const odds = groupOddsByMarket(event.odds, selectedMarket); ``` ### Show Line Movement [#show-line-movement] ```typescript const [previousOdds, setPreviousOdds] = useState>({}); useEffect(() => { const current: Record = {}; events.forEach((event) => { Object.entries(event.odds).forEach(([oddID, odd]) => { Object.entries(odd.byBookmaker || {}).forEach(([bookmakerID, bookmakerOdds]) => { const key = `${oddID}-${bookmakerID}`; current[key] = parseInt(bookmakerOdds.odds); }); }); }); setPreviousOdds(current); }, [events]); // In render const getMovement = (oddID: string, bookmakerID: string, currentOdds: string) => { const key = `${oddID}-${bookmakerID}`; const previous = previousOdds[key]; const current = parseInt(currentOdds); if (!previous) return null; if (current > previous) return "up"; // Improved for bettor if (current < previous) return "down"; // Worsened for bettor return null; }; ``` ### Add Filters [#add-filters] ```typescript const [filters, setFilters] = useState({ minOdds: -200, maxOdds: 200, bookmakers: [] as string[], }); const filteredOdds = Object.entries(spreadOdds).filter(([bookmaker, odds]) => { // Filter by bookmaker if (filters.bookmakers.length > 0 && !filters.bookmakers.includes(bookmaker)) { return false; } // Filter by odds range const homeOdds = odds.home?.odds ? parseInt(odds.home.odds) : 0; const awayOdds = odds.away?.odds ? parseInt(odds.away.odds) : 0; return homeOdds >= filters.minOdds && homeOdds <= filters.maxOdds && awayOdds >= filters.minOdds && awayOdds <= filters.maxOdds; }); ``` ### Export to CSV [#export-to-csv] ```typescript const exportToCSV = () => { const rows: any[] = []; events.forEach((event) => { Object.entries(event.odds).forEach(([oddID, odd]) => { Object.entries(odd.byBookmaker || {}).forEach(([bookmaker, bOdds]) => { rows.push({ game: `${event.teams.away.names.long} @ ${event.teams.home.names.long}`, bookmaker, market: odd.betTypeID, side: odd.sideID, line: bOdds.spread || bOdds.overUnder, price: bOdds.odds, }); }); }); }); const csv = [Object.keys(rows[0]).join(","), ...rows.map((row) => Object.values(row).join(","))].join("\n"); const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `odds-${Date.now()}.csv`; a.click(); }; ``` ## Deployment [#deployment] ### Deploy to Vercel [#deploy-to-vercel] ```bash npm install -g vercel vercel ``` Add environment variable in Vercel dashboard: * `SPORTSGAMEODDS_KEY=your_api_key` ### Deploy to Netlify [#deploy-to-netlify] ```bash npm run build netlify deploy --prod --dir=.next ``` Add environment variable in Netlify settings. ## Troubleshooting [#troubleshooting] ### "Failed to fetch odds" Error [#failed-to-fetch-odds-error] **Cause:** Missing or invalid API key. **Solution:** Check `.env.local`: ```bash # Verify file exists cat .env.local # Should output: SPORTSGAMEODDS_KEY=your_key_here # Restart dev server after adding npm run dev ``` ### Odds Not Updating [#odds-not-updating] **Cause:** Browser caching. **Solution:** Add cache-busting: ```typescript const response = await fetch( `/api/odds?league=${league}&t=${Date.now()}`, // Add timestamp { cache: "no-store" } // Disable caching ); ``` ### Best Odds Not Highlighted [#best-odds-not-highlighted] **Cause:** Type mismatch (string vs number). **Solution:** Ensure odds values are parsed as numbers: ```typescript const oddsNum = parseInt(bookmakerOdds.odds); if (isNaN(oddsNum)) return; // Skip invalid odds ``` ## Next Steps [#next-steps] ### Combine with Other Examples [#combine-with-other-examples] * **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Add real-time movement alerts * **[Arbitrage Calculator](/docs/examples/arbitrage-calculator)** - Highlight arb opportunities in UI ### Learn More [#learn-more] * **[Understanding oddID](/docs/data-types/odds)** - Decode odds structure * **[SDK Guide](/docs/sdk)** - Use TypeScript SDK for type safety * **[Best Practices](/docs/info/best-practices)** - Optimize performance --- # Player Props Analyzer Example - Python URL: https://sportsgameodds.com/docs/examples/player-props-analyzer # Player Props Analyzer Example - Python [#player-props-analyzer-example---python] Analyze player prop bets by fetching props for a specific game and comparing lines across bookmakers. ## What You'll Build [#what-youll-build] A Python script that: * Fetches all player props for an NBA game * Compares lines across bookmakers * Shows consensus lines * Identifies outlier books * Highlights potential value bets **Perfect for:** Finding value in player props, comparing bookmaker offerings, prop bet research ## Prerequisites [#prerequisites] * Python 3.8+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic Python knowledge ## Complete Code [#complete-code] ### Step 1: Setup Project [#step-1-setup-project] ```bash mkdir props-analyzer && cd props-analyzer python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install requests ``` ### Step 2: Create analyzer.py [#step-2-create-analyzerpy] ```python # analyzer.py 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 [#step-3-run-it] **List upcoming games:** ```bash export SPORTSGAMEODDS_KEY=your_api_key_here python analyzer.py ``` **Analyze specific game:** ```bash export SPORTSGAMEODDS_KEY=your_api_key_here export EVENT_ID=abc123 python analyzer.py ``` ## Expected Output [#expected-output] ``` SportsGameOdds Player Props Analyzer Boston Celtics @ Los Angeles Lakers Start: 2024-11-14T19:00:00Z ================================================================================ Found props for 15 players LeBron James -------------------------------------------------------------------------------- Points Ou Consensus: 25.5 (from 8 books) Range: 24.5 - 26.5 Bookmaker Line Over Under Notes ---------------------------------------------------------------------- betmgm 24.5 -105 -115 1.0pts lower draftkings 25.5 -110 -110 fanduel 25.5 -108 -112 caesars 25.5 -110 -110 pointsbet 26.0 -105 -115 barstool 26.5 -110 -110 1.0pts higher Value Opportunities: - betmgm: Line 1.0pts lower (consider OVER) - barstool: Line 1.0pts higher (consider UNDER) Rebounds Ou Consensus: 7.5 (from 7 books) Range: 7.5 - 8.5 Bookmaker Line Over Under Notes ---------------------------------------------------------------------- draftkings 7.5 -110 -110 fanduel 7.5 -115 -105 betmgm 7.5 -110 -110 caesars 8.5 -120 +100 1.0pts higher Value Opportunities: - caesars: Line 1.0pts higher (consider UNDER) Assists Ou Consensus: 6.5 (from 6 books) Range: 6.5 - 6.5 Bookmaker Line Over Under Notes ---------------------------------------------------------------------- draftkings 6.5 -120 +100 fanduel 6.5 -110 -110 betmgm 6.5 -105 -115 Anthony Davis -------------------------------------------------------------------------------- Points Ou Consensus: 23.5 (from 7 books) Range: 22.5 - 24.5 ... ``` ## How It Works [#how-it-works] ### 1. Identify Player Props [#1-identify-player-props] Player props are identified by their `statEntityID`. Team-level odds have values like `'all'`, `'home'`, or `'away'`, while player props have player identifiers: ```python stat_entity = odd.get('statEntityID', 'all') # Skip team-level odds if stat_entity in ['all', 'home', 'away']: continue # This is a player prop - statEntityID is the player (e.g., 'LEBRON_JAMES_1_NBA') player = stat_entity ``` ### 2. Process Bookmaker Odds [#2-process-bookmaker-odds] Each odd contains prices from multiple bookmakers in the `byBookmaker` object: ```python for bookmaker_id, bookmaker_data in odd.get('byBookmaker', {}).items(): if not bookmaker_data.get('available', True): continue line = bookmaker_data.get('overUnder') # e.g., "25.5" price = bookmaker_data.get('odds') # e.g., "-110" ``` ### 3. Calculate Consensus [#3-calculate-consensus] ```python lines = [float(p['line']) for p in props if p['line'] is not None] consensus = { 'mean': round(mean(lines), 1), # Average line 'median': median(lines), # Middle value 'min': min(lines), # Lowest line 'max': max(lines) # Highest line } ``` **Example:** * DraftKings: 25.5 * FanDuel: 25.5 * BetMGM: 24.5 * Caesars: 26.5 **Consensus:** 25.5 (mean), Range: 24.5 - 26.5 ### 4. Find Outliers [#4-find-outliers] ```python diff = abs(float(prop['line']) - mean_line) if diff >= 1.0: # 1+ points from consensus outliers.append({ 'bookmaker': prop['bookmaker'], 'diff': diff, 'direction': 'higher' if prop['line'] > mean_line else 'lower' }) ``` **Why outliers matter:** * Lower line = easier to hit OVER * Higher line = easier to hit UNDER * Potential value if one book is off-market ### 5. Identify Value [#5-identify-value] ```python if outlier['direction'] == 'lower': print(f"Consider OVER (easier to beat {outlier['line']})") else: print(f"Consider UNDER (easier to stay under {outlier['line']})") ``` ## Real-World Example [#real-world-example] **LeBron James Points:** | Bookmaker | Line | Over | Under | | ---------- | -------- | -------- | -------- | | DraftKings | 25.5 | -110 | -110 | | FanDuel | 25.5 | -108 | -112 | | **BetMGM** | **24.5** | **-105** | **-115** | | Caesars | 25.5 | -110 | -110 | | Barstool | 26.5 | -110 | -110 | **Consensus:** 25.5 points **Analysis:** * **BetMGM outlier:** 24.5 (1 point lower) * **Value:** OVER 24.5 @ -105 * **Why:** Easier to hit than market consensus of 25.5 * If LeBron scores 25 points, you win at BetMGM but lose everywhere else ## Enhancements [#enhancements] ### Track Props Over Time [#track-props-over-time] ```python 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 [#filter-by-prop-type] ```python def main(): prop_filter = input('Filter by prop type (points/rebounds/assists/all): ') # In analyze function for prop_type, props in sorted(prop_types.items()): if prop_filter != 'all' and prop_filter not in prop_type.lower(): continue # Skip non-matching props ``` ### Export to Spreadsheet [#export-to-spreadsheet] ```python 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 [#add-ev-calculations] ```python def american_to_decimal(american): """Convert American odds to decimal""" odds = int(american) if odds > 0: return (odds / 100) + 1 return (100 / abs(odds)) + 1 def calculate_ev(line, consensus_line, price, hit_rate=0.5): """ Calculate expected value Assumes hit_rate based on distance from consensus """ # Adjust hit rate based on line difference diff = float(line) - consensus_line if diff < 0: # Lower line (easier to hit over) adjusted_hit_rate = hit_rate + (abs(diff) * 0.05) else: adjusted_hit_rate = hit_rate - (diff * 0.05) adjusted_hit_rate = max(0.3, min(0.7, adjusted_hit_rate)) # Calculate EV decimal_odds = american_to_decimal(price) ev = (adjusted_hit_rate * (decimal_odds - 1)) - (1 - adjusted_hit_rate) return ev * 100 # As percentage ``` ## Troubleshooting [#troubleshooting] ### "No player props found" [#no-player-props-found] **Causes:** 1. Props not posted yet (too early before game) 2. Event has no props available 3. Wrong event ID **Solution:** Check when props are typically posted: * NBA: 12-24 hours before tip-off * NFL: 48+ hours before kickoff ### Props missing for some players [#props-missing-for-some-players] **Cause:** Bookmakers don't offer props for all players (usually starters + key bench players only). **Expected:** Props for 10-15 players per NBA game. ### Player names showing as IDs [#player-names-showing-as-ids] **Cause:** The API returns player IDs like `LEBRON_JAMES_1_NBA`. **Solution:** Use the `format_player_name` function to convert: ```python def format_player_name(player_id): parts = player_id.split('_') if len(parts) >= 2: name_parts = parts[:-2] if len(parts) > 2 else parts return ' '.join(word.capitalize() for word in name_parts) return player_id ``` ## Next Steps [#next-steps] ### Combine with Other Examples [#combine-with-other-examples] * **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Monitor prop line movement * **[Arbitrage Calculator](/docs/examples/arbitrage-calculator)** - Find prop arbitrage ### Learn More [#learn-more] * **[Understanding oddID](/docs/data-types/odds)** - Decode prop identifiers * **[Best Practices](/docs/info/best-practices)** - Optimize queries --- # Parlay Calculator Example - JavaScript URL: https://sportsgameodds.com/docs/examples/parlay-builder # Parlay Calculator Example - JavaScript [#parlay-calculator-example---javascript] Build a parlay calculator that combines multiple bets and calculates total odds and payout. ## What You'll Build [#what-youll-build] A JavaScript tool that: * Fetches live odds from multiple games * Allows selecting multiple bets (legs) * Calculates combined parlay odds * Shows potential payout * Supports American and decimal odds **Perfect for:** Parlay betting tools, sportsbook calculators, bet slip builders ## What is a Parlay? [#what-is-a-parlay] A **parlay** (or accumulator) combines multiple bets into one wager. **All bets must win** for the parlay to pay out, but the payout is much higher than individual bets. **Example:** * Lakers -5.5 @ -110 * Warriors ML @ -150 * Over 220.5 @ -110 **Individual bets:** Risk $10 each = $30 total -> Win \~$27 **As parlay:** Risk $30 -> Win \~$170 ## Prerequisites [#prerequisites] * Node.js 18+ * SportsGameOdds API key ([Get one free](/pricing)) * Basic JavaScript knowledge ## Complete Code [#complete-code] ### Step 1: Setup Project [#step-1-setup-project] ```bash mkdir parlay-calculator && cd parlay-calculator npm init -y npm install node-fetch@2 ``` > **Note:** We use `node-fetch@2` because v3+ is ESM-only. The `@2` ensures CommonJS compatibility with `require()`. ### Step 2: Create calculator.js [#step-2-create-calculatorjs] ```javascript // calculator.js const fetch = require("node-fetch"); const readline = require("readline"); const API_KEY = process.env.SPORTSGAMEODDS_KEY; const API_BASE = "https://api.sportsgameodds.com/v2"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // Utility functions const americanToDecimal = (american) => { if (american > 0) { return american / 100 + 1; } return 100 / Math.abs(american) + 1; }; const decimalToAmerican = (decimal) => { if (decimal >= 2.0) { return Math.round((decimal - 1) * 100); } return Math.round(-100 / (decimal - 1)); }; const formatOdds = (american) => { return american > 0 ? `+${american}` : american.toString(); }; // Fetch games async function fetchGames(league = "NBA") { try { const response = await fetch(`${API_BASE}/events?leagueID=${league}&finalized=false&oddsAvailable=true&limit=20`, { headers: { "x-api-key": API_KEY } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error("Error fetching games:", error.message); return { data: [] }; } } // Display available bets function displayAvailableBets(events) { const bets = []; let betId = 1; events.forEach((event) => { // Get team names from nested structure const awayName = event.teams.away.names.long; const homeName = event.teams.home.names.long; const matchup = `${awayName} @ ${homeName}`; // Get spread odds (focusing on DraftKings for simplicity) const spreadOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "sp" && odd.periodID === "game" && odd.byBookmaker?.draftkings); spreadOdds.forEach((odd) => { const dkOdds = odd.byBookmaker.draftkings; if (!dkOdds.available) return; const team = odd.sideID === "home" ? homeName : awayName; const spread = dkOdds.spread || "0"; // Spread already includes + or - from API const line = spread; bets.push({ id: betId++, matchup, description: `${team} ${line}`, odds: parseInt(dkOdds.odds), decimalOdds: americanToDecimal(parseInt(dkOdds.odds)), eventID: event.eventID, oddID: odd.oddID, betTypeID: odd.betTypeID, sideID: odd.sideID, spread: dkOdds.spread, }); }); // Get moneyline odds const mlOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "ml" && odd.periodID === "game" && odd.byBookmaker?.draftkings); mlOdds.forEach((odd) => { const dkOdds = odd.byBookmaker.draftkings; if (!dkOdds.available) return; const team = odd.sideID === "home" ? homeName : awayName; bets.push({ id: betId++, matchup, description: `${team} ML`, odds: parseInt(dkOdds.odds), decimalOdds: americanToDecimal(parseInt(dkOdds.odds)), eventID: event.eventID, oddID: odd.oddID, }); }); // Get totals (over/under) const totalOdds = Object.values(event.odds || {}).filter((odd) => odd.betTypeID === "ou" && odd.periodID === "game" && odd.byBookmaker?.draftkings); totalOdds.forEach((odd) => { const dkOdds = odd.byBookmaker.draftkings; if (!dkOdds.available) return; const side = odd.sideID === "over" ? "Over" : "Under"; bets.push({ id: betId++, matchup, description: `${side} ${dkOdds.overUnder}`, odds: parseInt(dkOdds.odds), decimalOdds: americanToDecimal(parseInt(dkOdds.odds)), eventID: event.eventID, oddID: odd.oddID, betTypeID: odd.betTypeID, sideID: odd.sideID, overUnder: dkOdds.overUnder, }); }); }); return bets; } // Calculate parlay odds function calculateParlay(legs, stake = 100) { if (legs.length === 0) { return null; } // Multiply all decimal odds const combinedDecimalOdds = legs.reduce((acc, leg) => acc * leg.decimalOdds, 1); // Convert back to American const combinedAmericanOdds = decimalToAmerican(combinedDecimalOdds); // Calculate payout const payout = stake * combinedDecimalOdds; const profit = payout - stake; return { legs: legs.length, combinedDecimalOdds: combinedDecimalOdds.toFixed(2), combinedAmericanOdds: formatOdds(combinedAmericanOdds), stake, payout: payout.toFixed(2), profit: profit.toFixed(2), }; } // Display parlay summary function displayParlay(parlay, legs) { console.log("\n" + "=".repeat(80)); console.log("PARLAY SUMMARY"); console.log("=".repeat(80)); console.log("\nSelected Legs:\n"); legs.forEach((leg, i) => { console.log(`${i + 1}. ${leg.description} (${leg.matchup})`); console.log(` Odds: ${formatOdds(leg.odds)} (${leg.decimalOdds.toFixed(2)} decimal)`); console.log(); }); console.log("-".repeat(80)); console.log(`\nCombined Odds: ${parlay.combinedAmericanOdds} (${parlay.combinedDecimalOdds} decimal)`); console.log(`Stake: $${parlay.stake.toFixed(2)}`); console.log(`Potential Payout: $${parlay.payout}`); console.log(`Potential Profit: $${parlay.profit}`); // Calculate implied probability const impliedProb = (1 / parseFloat(parlay.combinedDecimalOdds)) * 100; console.log(`Implied Probability: ${impliedProb.toFixed(1)}%`); console.log("\n" + "=".repeat(80)); console.log("Remember: ALL legs must win for parlay to pay out!"); console.log("=".repeat(80) + "\n"); } // Interactive CLI async function main() { console.log("Parlay Calculator - SportsGameOdds API\n"); // Fetch games console.log("Fetching NBA games...\n"); const { data: events } = await fetchGames("NBA"); if (events.length === 0) { console.log("No upcoming NBA games found"); console.log("Tip: Try during NBA season (October-June) or use a different league"); rl.close(); process.exit(1); } const availableBets = displayAvailableBets(events); if (availableBets.length === 0) { console.log("No DraftKings odds available for these games"); console.log("Tip: Try a different bookmaker or check back later"); rl.close(); process.exit(1); } console.log("Available Bets:\n"); availableBets.forEach((bet) => { console.log(`${bet.id}. ${bet.description} ${formatOdds(bet.odds)} - ${bet.matchup}`); }); const selectedLegs = []; const selectLeg = () => { rl.question('\nEnter bet ID to add to parlay (or "done" to finish): ', (answer) => { if (answer.toLowerCase() === "done") { if (selectedLegs.length < 2) { console.log("\nParlay requires at least 2 legs!\n"); selectLeg(); return; } rl.question("\nEnter stake amount ($): ", (stakeInput) => { const stake = parseFloat(stakeInput) || 100; const parlay = calculateParlay(selectedLegs, stake); displayParlay(parlay, selectedLegs); rl.close(); }); return; } const betId = parseInt(answer); const bet = availableBets.find((b) => b.id === betId); if (!bet) { console.log("Invalid bet ID"); selectLeg(); return; } // Check if already selected if (selectedLegs.find((l) => l.id === bet.id)) { console.log("Bet already in parlay"); selectLeg(); return; } // Check if from same game (not allowed in most parlays) const sameGame = selectedLegs.find((l) => l.eventID === bet.eventID); if (sameGame) { console.log("Cannot parlay multiple bets from same game (correlated)"); selectLeg(); return; } selectedLegs.push(bet); console.log(`Added: ${bet.description} ${formatOdds(bet.odds)}`); console.log(` Current legs: ${selectedLegs.length}`); if (selectedLegs.length >= 2) { const preview = calculateParlay(selectedLegs, 100); console.log(` Preview: $100 -> $${preview.payout} (${preview.combinedAmericanOdds})`); } selectLeg(); }); }; selectLeg(); } main(); ``` ### Step 3: Run It [#step-3-run-it] ```bash export SPORTSGAMEODDS_KEY=your_api_key_here node calculator.js ``` ## Expected Output [#expected-output] ``` Parlay Calculator - SportsGameOdds API Fetching NBA games... Available Bets: 1. Los Angeles Lakers -5.5 -110 - Boston Celtics @ Los Angeles Lakers 2. Boston Celtics +5.5 -110 - Boston Celtics @ Los Angeles Lakers 3. Los Angeles Lakers ML -220 - Boston Celtics @ Los Angeles Lakers 4. Boston Celtics ML +180 - Boston Celtics @ Los Angeles Lakers 5. Over 220.5 -110 - Boston Celtics @ Los Angeles Lakers 6. Under 220.5 -110 - Boston Celtics @ Los Angeles Lakers 7. Golden State Warriors -3.5 -110 - Phoenix Suns @ Golden State Warriors ... Enter bet ID to add to parlay (or "done" to finish): 1 Added: Los Angeles Lakers -5.5 -110 Current legs: 1 Enter bet ID to add to parlay (or "done" to finish): 7 Added: Golden State Warriors -3.5 -110 Current legs: 2 Preview: $100 -> $364.46 (+264) Enter bet ID to add to parlay (or "done" to finish): 13 Added: Miami Heat ML +150 Current legs: 3 Preview: $100 -> $911.16 (+811) Enter bet ID to add to parlay (or "done" to finish): done Enter stake amount ($): 50 ================================================================================ PARLAY SUMMARY ================================================================================ Selected Legs: 1. Los Angeles Lakers -5.5 (Boston Celtics @ Los Angeles Lakers) Odds: -110 (1.91 decimal) 2. Golden State Warriors -3.5 (Phoenix Suns @ Golden State Warriors) Odds: -110 (1.91 decimal) 3. Miami Heat ML (Orlando Magic @ Miami Heat) Odds: +150 (2.50 decimal) -------------------------------------------------------------------------------- Combined Odds: +811 (9.11 decimal) Stake: $50.00 Potential Payout: $455.58 Potential Profit: $405.58 Implied Probability: 11.0% ================================================================================ Remember: ALL legs must win for parlay to pay out! ================================================================================ ``` ## How It Works [#how-it-works] ### 1. Calculate Combined Odds [#1-calculate-combined-odds] ```javascript // Multiply all decimal odds const combinedDecimalOdds = legs.reduce((acc, leg) => acc * leg.decimalOdds, 1); ``` **Example:** * Leg 1: -110 -> 1.91 decimal * Leg 2: -110 -> 1.91 decimal * Leg 3: +150 -> 2.50 decimal **Combined:** 1.91 x 1.91 x 2.50 = **9.11 decimal** (+811 American) ### 2. Calculate Payout [#2-calculate-payout] ```javascript const payout = stake * combinedDecimalOdds; const profit = payout - stake; ``` **Example:** * Stake: $50 * Combined odds: 9.11 decimal * Payout: $50 x 9.11 = **$455.58** * Profit: $455.58 - $50 = **$405.58** ### 3. Prevent Correlated Bets [#3-prevent-correlated-bets] ```javascript const sameGame = selectedLegs.find((l) => l.eventID === bet.eventID); if (sameGame) { console.log("Cannot parlay bets from same game"); return; } ``` **Why?** Most sportsbooks don't allow parlaying bets from the same game because they're correlated (e.g., if Lakers win big, they'll likely cover the spread too). ## Parlay Odds Table [#parlay-odds-table] Quick reference for common parlay combinations: | Legs | All @ -110 | Combined Odds | $100 Pays | | ---- | ---------------------------- | ------------- | --------- | | 2 | -110, -110 | +264 | $264 | | 3 | -110, -110, -110 | +596 | $596 | | 4 | -110, -110, -110, -110 | +1228 | $1,228 | | 5 | -110, -110, -110, -110, -110 | +2436 | $2,436 | **Mixed odds example (2-leg):** * -110 + +150 = +355 ($100 -> $455) * -200 + +200 = +150 ($100 -> $250) * +100 + +100 = +300 ($100 -> $400) ## Enhancements [#enhancements] ### Add Same Game Parlays [#add-same-game-parlays] Some books allow same-game parlays (SGP). You need to check for correlation: ```javascript function checkCorrelation(leg1, leg2) { // Example: Can't parlay "Lakers -5.5" with "Over 220" // If Lakers cover, game likely went over if (leg1.eventID !== leg2.eventID) { return false; // Different games, no correlation } // Check for correlated bets const correlatedPairs = [ ["sp", "ou"], // Spread and total often correlated ["ml", "ou"], // Moneyline and total often correlated ]; const pair = [leg1.betTypeID, leg2.betTypeID].sort(); return correlatedPairs.some((cp) => cp[0] === pair[0] && cp[1] === pair[1]); } ``` ### Calculate True Odds [#calculate-true-odds] Adjust for bookmaker vig: ```javascript function calculateTrueOdds(americanOdds) { // Remove vig to get true probability const decimal = americanToDecimal(americanOdds); const impliedProb = 1 / decimal; // Assuming ~5% vig const trueProb = impliedProb * 0.95; return 1 / trueProb; } // Use true odds for expected value calculations const trueDecimal = calculateTrueOdds(leg.odds); ``` ### Add Round Robin [#add-round-robin] Round robin creates multiple parlays from your selections: ```javascript function generateRoundRobin(legs, parlaySize) { const combinations = []; function combine(start, combo) { if (combo.length === parlaySize) { combinations.push([...combo]); return; } for (let i = start; i < legs.length; i++) { combo.push(legs[i]); combine(i + 1, combo); combo.pop(); } } combine(0, []); return combinations; } // Example: 4 legs, 2-team parlays (6 combinations) const legs = [leg1, leg2, leg3, leg4]; const roundRobin = generateRoundRobin(legs, 2); console.log(`Creating ${roundRobin.length} 2-team parlays`); roundRobin.forEach((combo, i) => { const parlay = calculateParlay(combo, 10); // $10 each console.log(`Parlay ${i + 1}: $${parlay.payout}`); }); ``` ### Show Win Probability [#show-win-probability] ```javascript function calculateWinProbability(legs) { // Convert odds to probabilities (removing vig) const probs = legs.map((leg) => { const decimal = leg.decimalOdds; return (1 / decimal) * 0.95; // Adjust for ~5% vig }); // Multiply probabilities (independent events) const combinedProb = probs.reduce((acc, prob) => acc * prob, 1); return (combinedProb * 100).toFixed(1); } console.log(`Win Probability: ${calculateWinProbability(legs)}%`); ``` ### Add Teaser Calculation [#add-teaser-calculation] Teasers adjust spreads in your favor for lower payout: ```javascript function applyTeaser(legs, points = 6) { return legs.map((leg) => { if (leg.betTypeID === "sp") { // Adjust spread by teaser points const currentSpread = parseFloat(leg.spread); const newSpread = leg.sideID === "home" ? currentSpread - points : currentSpread + points; return { ...leg, spread: newSpread.toString(), odds: -110, // Standard teaser odds decimalOdds: americanToDecimal(-110), }; } if (leg.betTypeID === "ou") { // Move total in bettor's favor const currentTotal = parseFloat(leg.overUnder); const newTotal = leg.sideID === "over" ? currentTotal - points : currentTotal + points; return { ...leg, overUnder: newTotal.toString(), odds: -110, decimalOdds: americanToDecimal(-110), }; } return leg; }); } // 6-point teaser const teaserLegs = applyTeaser(selectedLegs, 6); const teaserParlay = calculateParlay(teaserLegs, 100); ``` ## Troubleshooting [#troubleshooting] ### "Cannot parlay bets from same game" [#cannot-parlay-bets-from-same-game] **Cause:** Selected multiple bets from one game. **Solution:** Most sportsbooks don't allow this. Use "Same Game Parlay" feature if available (different odds calculation). ### Calculated odds don't match sportsbook [#calculated-odds-dont-match-sportsbook] **Causes:** 1. **Correlation adjustments** - Books adjust odds for correlated bets 2. **Rounding** - Slight rounding differences 3. **Promotions** - Odds boosts not reflected **Solution:** Use our calculator for estimates. Always verify with actual sportsbook before placing. ### Parlay odds seem too good [#parlay-odds-seem-too-good] **Remember:** Parlays are -EV (negative expected value) over time. The high payouts are offset by low win probability. **Example:** * 3-leg parlay @ -110 each * Pays: +596 ($100 -> $696) * True odds: Each leg 52.4% -> Combined 14.4% * Expected value: $696 x 0.144 = $100.22 (barely break-even after vig) ## Best Practices [#best-practices] ### 1. Limit Leg Count [#1-limit-leg-count] **Recommendation:** 2-4 legs max **Why:** * Win probability drops exponentially * 5+ leg parlays rarely hit * Better to do multiple 2-leg parlays | Legs | Win % (all 50/50) | Reality | | ---- | ----------------- | ---------------- | | 2 | 25% | \~22% (with vig) | | 3 | 12.5% | \~10% | | 4 | 6.25% | \~5% | | 5 | 3.12% | \~2% | ### 2. Avoid Heavy Favorites [#2-avoid-heavy-favorites] **Problem:** Adding -400 favorites doesn't help much **Example:** * 2-leg: -110 + -110 = +264 * 2-leg: -110 + -400 = +110 (much worse!) **Better:** Use favorites in straights, underdogs in parlays ### 3. Shop for Best Odds [#3-shop-for-best-odds] **Impact of small differences:** Base parlay (3 legs @ -110 each): **+596** If you improve each leg by 5 cents (-105): * 3 legs @ -105 each: **+649** (+53 better!) ### 4. Consider Round Robins [#4-consider-round-robins] Instead of one 4-leg parlay: * **Option A:** 1x 4-leg @ +1228 (risk $100, all must win) * **Option B:** 6x 2-leg @ +264 each (risk $100 total, need 2+ to profit) **Option B is safer** - you can lose 1-2 legs and still profit. ## Next Steps [#next-steps] ### Combine with Other Examples [#combine-with-other-examples] * **[Live Odds Tracker](/docs/examples/live-odds-tracker)** - Monitor parlay legs in real-time * **[Odds Comparison Dashboard](/docs/examples/odds-comparison-dashboard)** - Find best odds for each leg ### Learn More [#learn-more] * **[Understanding oddID](/docs/data-types/odds)** - Decode bet identifiers * **[Best Practices](/docs/info/best-practices)** - Optimize your betting strategy * **[Glossary](/docs/info/glossary)** - Learn all terminology