API Reference

The FuWidget.betslip API, shared prediction market types, and browser support.

Betslip API

The FuWidget.betslip namespace exposes methods for controlling the Betslip widget from your own page code. This is the primary way to connect your odds display to the betslip — for example, wiring up "Add to Betslip" buttons next to odds on your page.

// Add a selection
FuWidget.betslip.setSelection("fb:m:328701:FT_1X2:1");

// Remove a selection
FuWidget.betslip.removeSelection("fb:m:328701:FT_1X2:1");

// React to every state change (e.g. update a selection counter badge)
const unsubscribe = FuWidget.betslip.subscribe(function (state) {
	console.log(state); // { selections: [...], totalOdds: number, stake: number }
});

// Stop listening when no longer needed
unsubscribe();

Available Methods

MethodDescription
setSelection(selectionId: string)Adds a selection. If the same event + market already exists, the outcome is replaced.
removeSelection(selectionId: string)Removes a selection by its exact ID. No-op if not present.
getState()Returns a synchronous snapshot of the current state. See note below.
subscribe(listener: (state) => void): () => voidSubscribes to state changes. Returns an unsubscribe function.
📘

Note on getState(): The betslip loads odds asynchronously after mount. Calling getState() immediately will return the current selections but totalOdds will not yet reflect live data. Use subscribe to react to state once odds have been fetched. getState() is most useful inside event handlers (e.g. on a button click) where the component has already been running for some time.

Usage with loadWidget()

FuWidgetLoader.load({
	onReady: function (FuWidget) {
		FuWidget.loadWidget({
			apiKey: "your-api-key",
			clientId: "your-client-id",
			configId: "your-config-id",
			contents: [{ type: "betslip", container: "betslip-container" }],
		});

		// Wire up your own "Add to Betslip" buttons
		document.querySelectorAll("[data-selection-id]").forEach((btn) => {
			btn.addEventListener("click", () => {
				FuWidget.betslip.setSelection(btn.getAttribute("data-selection-id"));
			});
		});
	},
});

Usage with FuWidget.init()

FuWidget.init({ /* ... config */ });

// Anywhere on the page after init
FuWidget.betslip.setSelection("fb:m:328701:FT_1X2:1");
📘

Important: Pre-mount calls are queued automatically — you can safely call setSelection before the Betslip component has finished rendering and the selection will be applied once it mounts.

📘

See Widgets → Betslip for the widget markup, attributes, and label customisation.


Predictor Analytics API

The Predictor widget emits a typed event for every meaningful step of the user journey — viewed the widget, started a prediction, submitted it, joined a private league, clicked an odd. 24 events in total. Use them for funnels, conversion goals, and remarketing audiences.

FuWidget.predictor.subscribe registers a listener and returns an unsubscribe function:

const unsubscribe = FuWidget.predictor.subscribe(function (event) {
	console.log(event.name, event.params, event.context);
});

// Stop listening when no longer needed
unsubscribe();

Why named events rather than click tracking

Every widget renders inside a shadow root, which is what keeps its styles off your page. The trade-off is that a tag manager's built-in no-code triggers — All Clicks, Form Submission, Element Visibility — cannot see into it:

  • A click inside a shadow root reaches document retargeted to the shadow host, so the tag manager sees one anonymous <div> for every click anywhere in the widget, with empty Click Text, Click Classes, and Click ID.
  • Native submit events do not cross a shadow boundary at all, so a Form Submission trigger never fires.

That is inherent to the CSS isolation, not a misconfiguration. Named events are the answer — and they survive redesigns that would break any selector-based trigger.

Event Shape

{
	widget: "predictor",    // which widget emitted it — check this first
	name: "prediction_submitted",
	params: { /* per-event, flat primitives only */ },
	context: {              // identical on every event from one widget instance
		entityId: "your-predictor-template-id",
		template: "standard",       // or "embed"
		language: "en",
		userIsLoggedIn: true,
		templateTitle: "Premier League Predictor"  // null until the template request settles
	}
}

widget is always "predictor" on this API — FuWidget.predictor.subscribe filters to the Predictor for you. It is on the payload because more widgets will emit later, and a handler that checks it first keeps working unchanged when they do.

Three properties you can rely on:

  • Flat primitives only — no nested objects or arrays. Multi-value fields are comma-joined strings, because tag managers and GA4 handle array parameters poorly.
  • Every key an event defines is always present, null rather than absent.
  • No personal data. No profile ID, email, display name, token, or private-league invitation code. The only user-level fact is context.userIsLoggedIn.
🚧

Do not enrich events with user identity. Once an event reaches dataLayer, every third-party tag on the page can read it. The payloads are deliberately anonymous.

Available Events

Every event also carries context.

GroupEventParams
Lifecyclewidget_view
tab_viewtab
matchweek_changedirection, group_id
Predictionprediction_startedmatch descriptors + market
prediction_submittedmatch descriptors + markets, market_count, is_edit
prediction_failedmatch descriptors + markets, market_count, is_edit
prediction_editedmatch descriptors + market
prediction_removedmatch descriptors + market
Gatessign_in_prompt_shownmatch descriptors + trigger
sign_in_clicklocation
consent_gate_shown / consent_accepted / consent_declinedconsent_ids
Engagementodds_clickmatch_id, outcome, odd_value, operator
leaderboard_page_changepage, total_pages
embed_success_cta_clickmatch_id, cta_label, url
Private leaguesprivate_league_created / _joined / _view / _invite_shared / _invitation_accepted / _invitation_declined / _left / _deletedleague_id, league_name

Match descriptors are match_id, home_team, home_team_id, away_team, away_team_id, kickoff_at.

📘

Join on the IDs, not the names. match_id and the team IDs are canonical and provider-agnostic (fb:m:*, fb:t:*) — stable across languages and across whichever football data provider is behind your account. match_id is the same value the prediction itself is posted with, so an event joins 1:1 with its prediction record. home_team / away_team are localised by the widget's language and exist for readable reports only.

Notes on firing — these look like bugs otherwise:

  • widget_view fires once per entityId, held until the template request settles. Before that the widget is a skeleton.
  • prediction_started fires once per match, on the user's first answer, however much they then change it.
  • A partially-failed multi-market submit emits both prediction_submitted and prediction_failed, each listing only its own markets.
  • sign_in_click does not fire for a sign-in CTA you replace with your own component — that component owns its own clicks.

Late subscribers

subscribe replays recent events to a new listener — the last 50, within the last five minutes. A script that attaches after the widget has mounted still receives widget_view and everything else it missed.

This matters because the widget bundle loads asynchronously: a tag manager's Page View tag routinely runs before FuWidget exists. The replay buffer means your listener does not have to win that race, so funnel denominators don't under-count.

📘

Subscribe-only, by design. There is no way to emit an event from page code. If there were, anything on the page could forge a prediction_submitted into your analytics — fake conversions and poisoned campaign reporting.

Google Tag Manager

Paste this into a Custom HTML tag firing on Page View. It needs no changes to your site's source, so marketing can ship it without a deploy:

<script>
(function () {
	window.dataLayer = window.dataLayer || [];
	function attach() {
		if (!window.FuWidget || !window.FuWidget.predictor) return false;
		window.FuWidget.predictor.subscribe(function (e) {
			window.dataLayer.push({ event: "fu_" + e.name, fansUnited: Object.assign({}, e.params, e.context) });
		});
		return true;
	}
	if (attach()) return;
	var t = setInterval(function () { if (attach()) clearInterval(t); }, 200);
	setTimeout(function () { clearInterval(t); }, 15000);
})();
</script>

The polling guard is there because the bundle loads asynchronously and a Page View tag can fire first. It does not have to win the race — whatever it misses is replayed once it attaches.

Then create a Custom Event trigger on fu_prediction_submitted, a GA4 Event tag, and Data Layer Variables for the parameters you want (fansUnited.match_id, fansUnited.markets, fansUnited.userIsLoggedIn). A single trigger matching ^fu_ with {{Event}} as the tag's event name forwards the whole catalogue at once.

Two things to get right:

  • The fu_ prefix is added by the snippet, not by the widget. dataLayer is shared with everything else on your page, so the naming inside your container is yours to choose.
  • GTM merges pushes rather than replacing them. If one push sets match_id and a later event omits that key, GTM still reports the stale value — the most common integration bug in this area. Nesting everything under a single fansUnited key, as above, sidesteps it entirely: each push replaces the whole object. The alternative is to null-pad the full key set on every push.

Verify in the browser console:

window.dataLayer.filter((e) => String(e.event).startsWith("fu_"));

Zero-configuration alternative

If you would rather not maintain the snippet, set data-predictor-analytics="true" on the widget element (or predictor.analytics: true globally) and the widget pushes to dataLayer itself, in exactly the shape above — event: "fu_<name>" with the payload under fansUnited. See Widgets → Predictor → Analytics.

The snippet is still the better fit when marketing needs to change the mapping without touching the page source.

Programmatic listener

When you own the FuWidget.init() call, callbacks.onEvent receives the identical stream:

FuWidget.init({
	// ... other config
	callbacks: {
		onEvent: (event) => {
			analytics.track(event.name, { ...event.params, ...event.context });
		},
	},
});

All three sinks can be used together. onEvent is scoped to the widgets that init call renders, whereas subscribe sees every Predictor on the page.

📘

Unlike subscribe, onEvent does not replay: it is attached before the widget mounts, so there is nothing to catch up on.


Market Types

Both prediction widgets (Match Prediction and Team Next Match Prediction) support various market types that can be specified using the data-market attribute.

1X2 Markets

For 1X2 markets, the widget displays three prediction options: Home win, Draw, and Away win.

Supported 1X2 markets:

  • FT_1X2 — Full-time match result (default)
  • HT_1X2 — Half-time match result

Yes/No Markets

For Yes/No markets, the widget displays two prediction options: Yes and No.

Supported Yes/No markets:

  • BOTH_TEAMS_SCORE — Whether both teams will score
  • PENALTY_MATCH — Whether there will be a penalty in the match
  • RED_CARD_MATCH — Whether there will be a red card in the match

Other Available Markets

Both widgets accept every market the Prediction API supports (32 in total) — see Widgets → Event Card → Predictions for the complete list. Beyond the ones above these include:

  • Player-specific markets (PLAYER_SCORE, PLAYER_YELLOW_CARD, etc.)
  • Over/under markets for goals and corners
  • Correct score markets
  • DOUBLE_CHANCE and HT_FT

Each of these widgets offers one market. To put several markets on a single card, use the Event Card widget with data-event-card-predictions-markets.

📘

Note: Some markets may be unavailable for certain competitions or matches, depending on data provider coverage.

📘

The Match Quiz widget supports an even wider set of markets — see its Supported Markets subsection on the Widgets page.


Browser Support

The widget library supports the following browsers:

  • Chrome (latest 2 versions)
  • Firefox (latest 2 versions)
  • Safari (latest 2 versions)
  • Edge (latest 2 versions)
  • iOS Safari (latest 2 versions)
  • Android Chrome (latest 2 versions)

Did this page help you?