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;
}
});
CautionAlways validate
event.originin 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
}| Field | Type | Description |
|---|---|---|
type | string | Event name (see table below) |
payload | object | Event-specific data |
timestamp | number | Unix milliseconds when the event was emitted |
Event Reference
WIDGET_READY
WIDGET_READYFired once when the widget has mounted and is ready to display content.
| Payload field | Type | Description |
|---|---|---|
| (empty object) | No payload |
{ "type": "WIDGET_READY", "payload": {}, "timestamp": 1711800000000 }WIDGET_EMPTY
WIDGET_EMPTYFired 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 field | Type | Description |
|---|---|---|
| (empty object) | No payload |
{ "type": "WIDGET_EMPTY", "payload": {}, "timestamp": 1711800000000 }WIDGET_ERROR
WIDGET_ERRORFired 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 field | Type | Description |
|---|---|---|
message | string | Human-readable error description |
status | number | null | HTTP status code of the failed request, if applicable |
phase | string | When the error occurred: initial, retry, or loadMore |
{ "type": "WIDGET_ERROR", "payload": { "message": "Failed to load flows", "status": 500, "phase": "initial" }, "timestamp": 1711800000000 }BET_CLICKED
BET_CLICKEDFired when the user clicks the betslip button on any flow card.
| Payload field | Type | Description |
|---|---|---|
flowId | string | number | Unique identifier of the flow or parlay |
flowType | string | Widget type (see Flow Types) |
buttonPayload | object | (optional) Operator-specific handoff payload attached to the tapped button, when configured for your deployment |
data | object | Full 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
}
TipThe
dataobject is the same object the widget renders from. Its shape depends onflowType- see the Flow Types table for the variants.
GRAPH_EXPANDED
GRAPH_EXPANDEDFired when the user expands a graph, stats section, or parlay leg.
| Payload field | Type | Description |
|---|---|---|
flowId | string | number | ID of the flow whose graph was expanded |
flowType | string | Widget type |
{ "type": "GRAPH_EXPANDED", "payload": { "flowId": "abc-123", "flowType": "factflow-base" }, "timestamp": 1711800000000 }GRAPH_COLLAPSED
GRAPH_COLLAPSEDFired when the user collapses a previously expanded graph or parlay leg.
| Payload field | Type | Description |
|---|---|---|
flowId | string | number | ID of the flow whose graph was collapsed |
flowType | string | Widget type |
{ "type": "GRAPH_COLLAPSED", "payload": { "flowId": "abc-123", "flowType": "factflow-base" }, "timestamp": 1711800000000 }PAGE_LOADED
PAGE_LOADEDFired when the widget loads a new page of content via infinite scroll or pagination.
| Payload field | Type | Description |
|---|---|---|
page | number | Page number that was loaded |
itemCount | number | Total items now in the feed |
newItemCount | number | Items added in this page |
{ "type": "PAGE_LOADED", "payload": { "page": 2, "itemCount": 40, "newItemCount": 20 }, "timestamp": 1711800000000 }
InfoThe initial server-rendered page does not emit a
PAGE_LOADEDevent. Only subsequent client-side page fetches emit this event. Pages that return only already-displayed items are skipped silently, sopagevalues are monotonically increasing but not necessarily consecutive - treat this event as "new content arrived", not as a backend page counter.
FILTER_CHANGED
FILTER_CHANGEDFired when the user changes a filter in the carousel filter bar.
| Payload field | Type | Description |
|---|---|---|
filters | object | Current filter selections keyed by category (context, wager_type, market_type, probability, bet_type) |
factFlowDisplayType | string | Current fact flow display variant (base, expanded) |
useCartoonImages | boolean | Whether cartoon-jersey images are active |
productMode | string | Current product mode (sportsbook, dfs, prediction_market) |
widgetMode | string | Current 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
}
InfoFilters set via iframe parameters do not emit a
FILTER_CHANGEDevent 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>
CautionUsing
"*"as the target origin means any page can receive your widget's events if they embed it. Always set theparentOriginquery 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
- Integration Recipes - production patterns for empty/error handling, theming, and analytics
- Iframe Parameters - configure filters, language, and entity scoping
- Widget Overview - introduction and flow types
- Presets - theme management and deployment
Updated about 15 hours ago

