Widget Events

The widget communicates with the parent page via window.postMessage(). This allows your site to react to user interactions inside the iframe - for example opening a betslip when a user taps a bet, or tracking engagement analytics.

Communication is outbound only: the widget emits events to your page, but there is no JS API for sending messages or method calls into the iframe - all configuration happens via iframe parameters and presets. In native app embeds, the Mobile SDKs deliver the same events as typed callbacks through an injected native bridge instead of postMessage.

Listening for Events

Add a message listener on the page that embeds the widget:

window.addEventListener('message', (event) => {
  // In production, validate the origin
  // if (event.origin !== 'https://widget.example.com') return;

  const { type, payload, timestamp } = event.data;

  switch (type) {
    case 'WIDGET_READY':
      console.log('Widget loaded at', timestamp);
      break;
    case 'WIDGET_EMPTY':
      // Hide the whole section (heading included), not just the iframe
      document.getElementById('odditt-widget-section').style.display = 'none';
      break;
    case 'WIDGET_ERROR':
      console.warn('Widget error:', payload.message);
      break;
    case 'BET_CLICKED':
      openBetslip(payload.data);
      break;
    case 'GRAPH_EXPANDED':
      trackEngagement('expand', payload.flowId);
      break;
    case 'GRAPH_COLLAPSED':
      trackEngagement('collapse', payload.flowId);
      break;
    case 'PAGE_LOADED':
      console.log(`Page ${payload.page}: ${payload.newItemCount} new items`);
      break;
    case 'FILTER_CHANGED':
      console.log('Active filters:', payload.filters);
      break;
  }
});
⚠️

Caution

Always validate event.origin in production to ensure messages come from your widget domain and not from unrelated iframes on the same page.

Message Format

Every message has the same envelope:

{
  "type": "BET_CLICKED",
  "payload": { ... },
  "timestamp": 1711800000000
}
FieldTypeDescription
typestringEvent name (see table below)
payloadobjectEvent-specific data
timestampnumberUnix milliseconds when the event was emitted

Event Reference

WIDGET_READY

Fired once when the widget has mounted and is ready to display content.

Payload fieldTypeDescription
(empty object)No payload
{ "type": "WIDGET_READY", "payload": {}, "timestamp": 1711800000000 }

WIDGET_EMPTY

Fired when the configured parameters return zero flows. Use this to collapse or hide the whole widget section (heading included) so an empty widget doesn't take up space on your page - see Integration Recipes for the recommended pattern and how to avoid triggering it through over-scoping.

Payload fieldTypeDescription
(empty object)No payload
{ "type": "WIDGET_EMPTY", "payload": {}, "timestamp": 1711800000000 }

WIDGET_ERROR

Fired when the widget fails to load content. The widget only reports this when it has nothing to render, so it's safe to hide the section (or swap in fallback content) on any error - see Integration Recipes.

Payload fieldTypeDescription
messagestringHuman-readable error description
statusnumber | nullHTTP status code of the failed request, if applicable
phasestringWhen the error occurred: initial, retry, or loadMore
{ "type": "WIDGET_ERROR", "payload": { "message": "Failed to load flows", "status": 500, "phase": "initial" }, "timestamp": 1711800000000 }

BET_CLICKED

Fired when the user clicks the betslip button on any flow card.

Payload fieldTypeDescription
flowIdstring | numberUnique identifier of the flow or parlay
flowTypestringWidget type (see Flow Types)
buttonPayloadobject(optional) Operator-specific handoff payload attached to the tapped button, when configured for your deployment
dataobjectFull flow data object - contents vary by flow type

The data field contains the complete data for the flow, including player name, odds in all formats, payout multiplier, countdown, and type-specific fields. This avoids the need to request additional data from your backend after a click.

Example - single flow:

{
  "type": "BET_CLICKED",
  "payload": {
    "flowId": "abc-123",
    "flowType": "funflow",
    "data": {
      "id": "abc-123",
      "playerName": "Patrick Mahomes",
      "betDescription": "Over 274.5 passing yards",
      "oddsAmerican": 150,
      "oddsDecimal": 2.5,
      "oddsFractional": "3/2",
      "payoutMultiplier": 2.5,
      "gameTime": "Sun 4:25 PM",
      "eventShort": "KC @ BUF"
    }
  },
  "timestamp": 1711800000000
}

Example - parlay:

{
  "type": "BET_CLICKED",
  "payload": {
    "flowId": "parlay-456",
    "flowType": "parlay",
    "data": {
      "title": "Sunday Parlay",
      "subtitle": "3-Leg Player Props",
      "oddsAmerican": 600,
      "oddsDecimal": 7.0,
      "oddsFractional": "6/1",
      "payoutMultiplier": 7.0,
      "legs": [
        { "id": "leg-1", "playerName": "Patrick Mahomes", "betDescription": "Over 274.5 passing yards" },
        { "id": "leg-2", "playerName": "Travis Kelce", "betDescription": "Over 5.5 receptions" },
        { "id": "leg-3", "playerName": "Isiah Pacheco", "betDescription": "Over 64.5 rushing yards" }
      ]
    }
  },
  "timestamp": 1711800000000
}
📘

Tip

The data object is the same object the widget renders from. Its shape depends on flowType - see the Flow Types table for the variants.


GRAPH_EXPANDED

Fired when the user expands a graph, stats section, or parlay leg.

Payload fieldTypeDescription
flowIdstring | numberID of the flow whose graph was expanded
flowTypestringWidget type
{ "type": "GRAPH_EXPANDED", "payload": { "flowId": "abc-123", "flowType": "factflow-base" }, "timestamp": 1711800000000 }

GRAPH_COLLAPSED

Fired when the user collapses a previously expanded graph or parlay leg.

Payload fieldTypeDescription
flowIdstring | numberID of the flow whose graph was collapsed
flowTypestringWidget type
{ "type": "GRAPH_COLLAPSED", "payload": { "flowId": "abc-123", "flowType": "factflow-base" }, "timestamp": 1711800000000 }

PAGE_LOADED

Fired when the widget loads a new page of content via infinite scroll or pagination.

Payload fieldTypeDescription
pagenumberPage number that was loaded
itemCountnumberTotal items now in the feed
newItemCountnumberItems added in this page
{ "type": "PAGE_LOADED", "payload": { "page": 2, "itemCount": 40, "newItemCount": 20 }, "timestamp": 1711800000000 }
📘

Info

The initial server-rendered page does not emit a PAGE_LOADED event. Only subsequent client-side page fetches emit this event. Pages that return only already-displayed items are skipped silently, so page values are monotonically increasing but not necessarily consecutive - treat this event as "new content arrived", not as a backend page counter.


FILTER_CHANGED

Fired when the user changes a filter in the carousel filter bar.

Payload fieldTypeDescription
filtersobjectCurrent filter selections keyed by category (context, wager_type, market_type, probability, bet_type)
factFlowDisplayTypestringCurrent fact flow display variant (base, expanded)
useCartoonImagesbooleanWhether cartoon-jersey images are active
productModestringCurrent product mode (sportsbook, dfs, prediction_market)
widgetModestringCurrent widget mode (operator, affiliate, clean)
{
  "type": "FILTER_CHANGED",
  "payload": {
    "filters": {
      "context": ["fact"],
      "wager_type": ["singles"],
      "market_type": [],
      "probability": [],
      "bet_type": ["overs"]
    },
    "factFlowDisplayType": "base",
    "useCartoonImages": false,
    "productMode": "sportsbook",
    "widgetMode": "operator"
  },
  "timestamp": 1711800000000
}
📘

Info

Filters set via iframe parameters do not emit a FILTER_CHANGED event on initial load. The event only fires on subsequent user-initiated changes.

Target Origin

By default the widget posts messages with target origin "*" (any parent). For production, restrict this to your site's origin using the ?parentOrigin= query param in the iframe URL, or ask the Odditt team to configure a default origin for your deployment.

<!-- Restrict events to your domain -->
<iframe
  src="https://widget.example.com/?preset=brand_dark_v2&parentOrigin=https://mysite.com"
  width="100%"
  height="480"
  frameborder="0"
  style="border: none;"
></iframe>
⚠️

Caution

Using "*" as the target origin means any page can receive your widget's events if they embed it. Always set the parentOrigin query param in production.

Complete Example

A full integration that embeds the widget and handles all events:

<!DOCTYPE html>
<html>
<head>
  <title>Odditt Widget Integration</title>
</head>
<body>
  <section id="odditt-widget-section">
    <h2>Trending Bets</h2>
    <iframe
      id="odditt-widget"
      src="https://widget.example.com/?preset=brand_dark_v2&parentOrigin=https://mysite.com&leagueKey=nfl"
      width="100%"
      height="480"
      frameborder="0"
      style="border: none;"
    ></iframe>
  </section>

  <script>
    const WIDGET_ORIGIN = 'https://widget.example.com';

    window.addEventListener('message', (event) => {
      if (event.origin !== WIDGET_ORIGIN) return;

      const { type, payload } = event.data;

      switch (type) {
        case 'WIDGET_READY':
          console.log('Widget is ready');
          break;

        case 'WIDGET_EMPTY':
        case 'WIDGET_ERROR':
          // Nothing to show - collapse the whole section, heading included
          document.getElementById('odditt-widget-section').style.display = 'none';
          break;

        case 'BET_CLICKED':
          // Open your betslip with the flow data
          openBetslip({
            id: payload.flowId,
            type: payload.flowType,
            odds: payload.data.oddsDecimal,
            description: payload.data.betDescription,
          });
          break;

        case 'PAGE_LOADED':
          // Track scroll depth
          analytics.track('widget_page_loaded', {
            page: payload.page,
            totalItems: payload.itemCount,
          });
          break;

        case 'FILTER_CHANGED':
          // Sync filters with your own UI
          updateFilterDisplay(payload.filters);
          break;

        case 'GRAPH_EXPANDED':
        case 'GRAPH_COLLAPSED':
          // Track engagement
          analytics.track('widget_graph_toggle', {
            action: type === 'GRAPH_EXPANDED' ? 'expand' : 'collapse',
            flowId: payload.flowId,
          });
          break;
      }
    });
  </script>
</body>
</html>

Next Steps


Did this page help you?