# Real-Time Streaming API (WebSocket)
URL: https://sportsgameodds.com/docs/guides/realtime-streaming-api

Real-Time Streaming API (WebSocket) [#real-time-streaming-api-websocket]

<Callout type="warn" title="Access Required">
  This API endpoint is only available to **AllStar** and **custom plan** subscribers. It is not included with basic subscription tiers. [Contact support](mailto:api@sportsgameodds.com) to get access.
</Callout>

<Callout type="warn" title="Beta Feature">
  This streaming API is currently in **beta**. API call patterns, response formats, and functionality may change.
</Callout>

Our Streaming API provides real-time updates for Event objects through WebSocket connections. Instead of polling our REST endpoints, you can maintain a persistent connection to receive instant notifications when events change. This is ideal for applications that need immediate updates with minimal delay.

We use [Pusher Protocol](https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/) for WebSocket communication. While you can connect using any WebSocket library, we strongly recommend using any [Pusher Client Library](https://pusher.com/docs/channels/library_auth_reference/pusher-client-libraries) (ex: [Javascript](https://github.com/pusher/pusher-js), [Python](https://github.com/pusher/pusher-http-python))

How It Works [#how-it-works]

The streaming process involves two steps:

1. **Get Connection Details**: Make a request to `/v2/stream/events` to receive:
   * WebSocket authentication credentials
   * WebSocket URL/channel info
   * Initial snapshot of current data

2. **Connect and Stream**: Use the provided details to connect via Pusher (or another WebSocket library) and receive real-time `eventID` notifications for changed events

<Callout type="info" title="Rate Limits">
  Your API key will have limits on concurrent streams.
</Callout>

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`           |

<Callout type="info" title="Feeds">
  The number of supported feeds will increase over time. Please reach out if you have a use case which can't be covered by these feeds.
</Callout>

Quick Start Example [#quick-start-example]

Here's the minimal code to connect to live events:

<Tabs items={['JavaScript/Node.js', 'Python', 'Ruby']}>
  <Tab value="JavaScript/Node.js">
    ```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();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import requests
    import pusherclient
    import json

    API_KEY = 'YOUR_API_KEY'
    API_BASE_URL = 'https://api.sportsgameodds.com/v2'

    def handle_update(data):
        try:
            changed_events = json.loads(data)
            event_ids = ','.join([e['eventID'] for e in changed_events])

            # Fetch updated event data
            response = requests.get(
                f"{API_BASE_URL}/events",
                headers={'x-api-key': API_KEY},
                params={'eventIDs': event_ids}
            )

            for event in response.json()['data']:
                print(f"Updated: {event['eventID']}")

        except Exception as e:
            print(f"Error processing update: {e}")

    def connect_to_live_events():
        # Get connection details
        response = requests.get(
            f"{API_BASE_URL}/stream/events",
            headers={'x-api-key': API_KEY},
            params={'feed': 'events:live'}
        )

        stream_info = response.json()
        print(f"Connected with {len(stream_info['data'])} initial events")

        # Connect to Pusher (Note: private channels require custom auth in Python)
        pusher_key = stream_info['pusherKey']
        channel_name = stream_info['channel']

        pusher = pusherclient.Pusher(pusher_key)
        channel = pusher.subscribe(channel_name)
        channel.bind('data', handle_update)

        pusher.connection.bind('pusher:connection_established',
                              lambda data: print("Connected to stream"))

    if __name__ == "__main__":
        connect_to_live_events()
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    require 'net/http'
    require 'json'
    require 'pusher-client'
    require 'uri'

    STREAM_FEED = "events:live" # ex: events:upcoming, events:byid, events:live
    API_BASE_URL = "https://api.sportsgameodds.com/v2"
    API_KEY = "API_KEY_GOES_HERE"

    def convert_ruby_options(standard_options, api_key)
      ruby_client_options = {}

      ruby_client_options[:ws_host] = standard_options['wsHost'] if standard_options['wsHost']
      ruby_client_options[:ws_port] = standard_options['wsPort'] if standard_options['wsPort']
      ruby_client_options[:wss_port] = standard_options['wssPort'] if standard_options['wssPort']
      ruby_client_options[:ws_path] = standard_options['wsPath'] if standard_options['wsPath']
      ruby_client_options[:ws_host] = "ws-#{standard_options['cluster']}.pusher.com" if standard_options['cluster'] && !ruby_client_options[:ws_host]
      ruby_client_options[:secure] = true

      if (standard_options.dig('channelAuthorization', 'endpoint'))
        ruby_client_options[:auth_method] = ->(socket_id, channel) {
          headers = (standard_options.dig('channelAuthorization', 'headers') || {}).merge('Content-Type' => 'application/json')
          params = (standard_options.dig('channelAuthorization', 'params') || {}).merge('socket_id' => socket_id, 'channel_name' => channel.name)
          res = Net::HTTP.post(URI(standard_options.dig('channelAuthorization', 'endpoint')), params.to_json, headers)
          res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body)['auth'] : raise("Auth failed: HTTP #{res.code} - #{res.body}")
        }
      end

      ruby_client_options
    end

    def api_get(path, params = {})
      uri = URI("#{API_BASE_URL}#{path}")
      uri.query = URI.encode_www_form(params) unless params.empty?
      res = Net::HTTP.get_response(uri, {'x-api-key' => API_KEY})
      raise "HTTP #{res.code}: #{res.message}" unless res.is_a?(Net::HTTPSuccess)

      JSON.parse(res.body)
    end

    def run
      events = {}

      # Get initial data and connection parameters
      stream_info = api_get("/stream/events", feed: STREAM_FEED)
      stream_info['data'].each { |e| events[e['eventID']] = e }

      # Convert the standard options to Ruby SDK options
      connect_options = convert_ruby_options(stream_info['pusherOptions'], API_KEY)

      # Initialize the client
      client = PusherClient::Socket.new(stream_info['pusherKey'], connect_options)

      # Set up other event handlers as needed
      client.bind('pusher:connection_established') { puts "Connected" }
      client.bind('pusher:subscription_succeeded') { |_| puts "Subscribed to: #{stream_info['channel']}" }
      client.bind('pusher:subscription_error') { |data| puts "Error: #{data}" }

      # Subscribe to and handle data events in the channel
      channel = client.subscribe(stream_info['channel'], stream_info['user'])
      channel.bind('data') do |json|
        changed_events = JSON.parse(json)
        event_ids = changed_events.map { |e| e['eventID'] }.join(', ')
        puts "Changed Events: #{event_ids}"

        # Fetch and update full event data
        api_get("/events", eventIDs: event_ids)['data'].each { |e| events[e['eventID']] = e }
      rescue => e
        puts "Error processing data event: #{e.message}"
      end

      # Print initial event IDs
      puts "EVENTS: #{events.keys.join(',')}"

      # Graceful shutdown
      trap('INT') { puts "\nDisconnecting..."; client.disconnect; exit }

      # Connect to the WebSocket server
      client.connect # For async usage, you can instead call client.connect(true) after initializing the client

    end

    private

    # Runs the script
    run
    ```
  </Tab>
</Tabs>

API Response Format [#api-response-format]

The `/v2/stream/events` endpoint returns:

```json
{
  "success": true,
  "data": [
    <an Event object>,
    // ... more events
  ],
  "pusherKey": <key to pass when setting up the Pusher client>,
  "pusherOptions": <options to pass when setting up the Pusher client>,
  "channel": <channel to subscribe to>
}
```

Update Message Format [#update-message-format]

Real-time updates contain only the `eventID` of changed events:

```json
[
  { "eventID": <an 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.