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.
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 '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;
}
});
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
}
| 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
Fired 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 }
BET_CLICKED
Fired 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) |
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
}
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 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
Fired 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
Fired 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 }
The initial server-rendered page does not emit a PAGE_LOADED event. Only subsequent client-side page fetches emit this event.
FILTER_CHANGED
Fired 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, multi) |
{
"type": "FILTER_CHANGED",
"payload": {
"filters": {
"context": ["fact"],
"wager_type": ["singles"],
"market_type": [],
"probability": [],
"bet_type": ["overs"]
},
"factFlowDisplayType": "base"
},
"timestamp": 1711800000000
}
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 Betflow 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>
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>Betflow Widget Integration</title>
</head>
<body>
<iframe
id="betflow-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>
<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 '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
- Iframe Parameters — configure filters, language, and entity scoping
- Widget Overview — introduction and flow types
- Presets — theme management and deployment