Skip to main content

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;
}
});
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 }

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)
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.


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, multi)
{
"type": "FILTER_CHANGED",
"payload": {
"filters": {
"context": ["fact"],
"wager_type": ["singles"],
"market_type": [],
"probability": [],
"bet_type": ["overs"]
},
"factFlowDisplayType": "base"
},
"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 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>
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>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