Getting Started

Everything you need to render your first widget — installation, configuration, the two ways to load widgets, and authentication.

Installation

The widget library is available in three integration approaches. Pick the one that fits your page.

ApproachBest for
Auto-Update Loader ✅ RecommendedAlways getting the latest version automatically, optimal performance
Direct IIFE BuildBroad browser support, multiple widgets per page
Direct ESM BuildPerformance optimization, 1–2 widgets per page

Approach 1: Auto-Update with Loader (Recommended)

Best for: Always getting the latest version automatically, optimal performance.

The loader script automatically:

  • Fetches the latest widget version via manifest.json
  • Detects browser capabilities and loads the best build (ESM or IIFE)
  • Provides a fallback if the manifest fetch fails
<!-- Step 1: Load the tiny loader script -->
<script src="https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@latest/fu-widgets-loader.js"></script>

<!-- Step 2: Use the loader -->
<script>
	FuWidgetLoader.load({
		onReady: function (FuWidget, manifest) {
			console.log("Widget ready! Version:", manifest.version);

			// Load your widget
			FuWidget.loadWidget({
				clientId: "your-client-id",
				apiKey: "your-api-key",
				configId: "your-config-id",
				contents: [
					{
						id: "your-content-id",
						type: "classic-quiz",
						container: "widget-container",
					}
				]
			});
		},
		onError: function (error) {
			console.error("Failed to load widget:", error);
		},
	});
</script>

<div id="widget-container"></div>

Pros:

  • ✅ Always gets the latest version automatically (CDN cache is purged on publish)
  • ✅ Perfect cache busting (hashed filenames on widgets)
  • ✅ Smart build selection (ESM vs IIFE)
  • ✅ Fallback to known version if manifest fails

Approach 2: Direct Load with Versioned URL (Simple)

Best for: Full control over version, simple integration.

Option 1: IIFE Build (Traditional)

Best for: Backward compatibility, traditional script loading, works everywhere.

<script src="https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.iife.js"></script>

Pros:

  • ✅ Works in all browsers (no ES6 module support needed)
  • ✅ Simple script tag loading
  • ✅ Backward compatible with existing implementations

Cons:

  • ❌ Larger initial bundle size
  • ❌ All widgets loaded upfront

Option 2: ESM Build With Code Splitting

Best for: Performance optimization, single-widget pages.

<script type="module" src="https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.es.js"></script>

Pros:

  • ✅ Code splitting — widgets loaded on-demand
  • ✅ 80% smaller initial load
  • ✅ Better caching (shared chunks cached separately)
  • ✅ Faster page load times

Cons:

  • ❌ Requires a modern browser with ES6 module support
📘

Important: Replace <version> with the latest version number. You can check the latest version on the npm registry page for fansunited-widgets-cdn. The current example uses version 0.0.95, but newer versions may be available.

Which one should I use?

  • Use IIFE if you need broad browser support or have multiple widgets on the same page.
  • Use ESM if you have 1–2 widgets per page and want optimal performance.

Basic Configuration

Initialize the widget library with FuWidget.init(). Only language and fansUnited are required — everything else is optional and sets global defaults that individual widgets can override.

📘

Most options below are documented in full on the Features and Widgets pages. This section is the single-glance reference for what init() accepts.

FuWidget.init({
	// Required: Widget UI language
	language: "en", // "bg", "en", "ro", "pt", "sr", "fr", "de", "it", "fr-be", "pl", "pt-br", "sk", "el"

	// Required: Fans United API configuration
	fansUnited: {
		apiKey: "your-fu-api-key",
		clientId: "your-client-id",
		environment: "prod", // "dev", "prod", "staging", "watg", "yolo", "cska"
		lang: "en", // Content language: "bg", "en", "ro", "el", "sk", "pt", "sr", "hu", "sv", "es", "fr", "nl", "de", "it"
		idSchema: "native", // "native", "enetpulse", "sportradar", "sportal365", "api_football"
		errorHandlingMode: "standard", // "default", "standard"
		// Authentication options (choose one if needed):
		tokenString: "your-auth-token", // Optional: Direct token authentication
		tokenCookieName: "your-token-cookie-name", // Optional: Cookie-based authentication
	},

	// Optional: Firebase configuration (required if not using tokenString or tokenCookieName)
	firebase: {
		apiKey: "your-firebase-api-key",
		authDomain: "your-app.firebaseapp.com",
		projectId: "your-project-id",
		appId: "your-app-id",
		measurementId: "your-measurement-id", // Optional: Google Analytics measurement ID
	},

	// Optional: Theme configuration
	theme: {
		mode: "light", // "light" or "dark"
		primaryColor: {
			50: "#fef1f2",
			100: "#fee4e6",
			200: "#fecdd3",
			300: "#fda4af",
			400: "#fb7185",
			500: "#f43f5e",
			600: "#e11d48",
			700: "#be123c",
			800: "#9f1239",
			900: "#881337",
		},
	},

	// Optional: Lead collection configuration
	leads: {
		defaultFields: ["fullName", "email"], // Available: "fullName", "firstName", "lastName", "email", "gender", "country", "phoneCountryCode", "phoneNumber"
		position: "after", // "before" or "after"
		campaignId: "your-campaign-id",
		campaignName: "Your Campaign Name",
		phoneCountryCode: "+1", // Default phone country code
		syncWithProfile: false, // Sync anonymous profiles to database after lead submission
	},

	// Optional: Default layout template for widgets
	layoutTemplate: "STANDARD", // "STANDARD", "SPLIT", "OVERLAY"

	// Optional: Default options layout
	optionsLayout: "column", // "twoByTwo", "row", "column"

	// Optional: Default image position
	imagePosition: "left", // "left" or "right"

	// Optional: Show answer explanations in classic and personality quiz widgets
	showAnswerExplanations: true,

	// Optional: Show prediction details in match quiz and event widget
	showPredictionDetails: false,

	// Optional: Show countdown timer in match quiz and event game widget
	showCountdown: false,

	// Optional: Show team labels in match quiz widget
	showTeamLabels: true,

	// Optional: Show points in match quiz and event widget
	showPoints: false,

	// Optional: Default variant for Chance Game widget
	chanceGameVariant: "prize_wheel", // "prize_wheel", "penalty_shootout", "pick_one_of_x"

	// Optional: Advanced theming options (see Features → Theming)
	themeOptions: {
		// Custom theme options for polls and quizzes
	},

	// Optional: Default placeholder image URL
	defaultImagePlaceholderUrl: "https://example.com/placeholder.jpg",

	// Optional: List widget configuration
	list: {
		classicQuizUrl: "/quiz/{CONTENT_ID}",
		personalityQuizUrl: "/personality-quiz/{CONTENT_ID}",
		eitherOrUrl: "/either-or/{CONTENT_ID}",
		pollUrl: "/poll/{CONTENT_ID}",
		matchQuizUrl: "/match-quiz/{CONTENT_ID}",
	},

	// Optional: URL patterns for content types
	urls: {
		classicQuizUrl: "/quiz/{CONTENT_ID}",
		personalityQuizUrl: "/personality-quiz/{CONTENT_ID}",
		eitherOrUrl: "/either-or/{CONTENT_ID}",
		pollUrl: "/poll/{CONTENT_ID}",
		matchQuizUrl: "/match-quiz/{CONTENT_ID}",
	},

	// Optional: Sign-in CTA configuration
	signInCTA: {
		defaultLabel: "Sign in to participate",
		onClick: () => {
			window.location.href = "/signin";
		},
		url: "https://yourdomain.com/signin", // Optional: Direct URL for the CTA
		target: "_self", // Optional: Link target (_self, _blank)
	},

	// Optional: Additional CTA configuration
	additionalCTA: {
		defaultLabel: "Learn More",
		onClick: () => {
			console.log("Additional CTA clicked");
		},
		url: "https://yourdomain.com/info", // Optional: Direct URL for the CTA
		target: "_blank", // Optional: Link target (_self, _blank)
	},

	// Optional: Rules display configuration
	rulesDisplay: {
		type: "modal", // "modal" or "link"
		url: "https://example.com/rules", // Required when type is "link"
		target: "_blank", // Optional: Link target (_self, _blank, _parent, _top)
	},

	// Optional: Player of the Match configuration
	playerOfTheMatch: {
		authRequirement: "REGISTERED", // "REGISTERED" or null - Require authentication for POTM voting
	},

	// Optional: Discussion widget configuration
	discussion: {
		defaultSort: "LATEST", // "OLDEST", "LATEST", "INTERACTED", "POPULAR"
		postsPerPage: 10,
		showReactions: true,
		showReplies: true,
		showReportPost: true,
		showModeratedPosts: false,
		infiniteScroll: false,
		allowAnonymous: false,
	},

	// Optional: Either/Or widget configuration
	eitherOr: {
		showStandings: true
	},

	// Optional: Leaderboard widget configuration
	leaderboard: {
		pageSize: 10,
	},

	// Optional: Predictor widget configuration
	predictor: {
		tabs: ["play", "leaderboard", "private-leagues", "rules", "prizes"], // Optional: tabs to enable (default: all tabs)
		matchCardBgImageUrl: "https://example.com/stadium-bg.jpg", // Optional: background image for match prediction cards
		consents: [], // Optional: consent definitions required before predicting (see Widgets → Predictor)
		betslip: {
			trigger: "predictions-only", // "predictions-only" or "odds-only" — when betslip becomes available in Predictor
			position: "bottom-right", // Betslip position (see Widgets → Betslip for all options)
			currency: "USD",
		},
	},

	// Optional: Betslip widget configuration
	betslip: {
		position: "bottom-right", // Where the widget appears on the page (see Widgets → Betslip)
		maxSelections: 10, // Maximum number of selections allowed
		stakePresets: [5, 10, 20, 50], // Quick-stake preset buttons shown to the user
		oddsPollingInterval: 5000, // How often to refresh odds, in milliseconds
		currency: "USD", // Currency label shown for stake and potential winnings
		ctaUrlTemplate: "https://bookmaker.com/betslip?s={SELECTIONS}", // URL for the place-bet CTA button
		brandingLogoUrl: "https://example.com/brand-logo.png", // Bookmaker or sponsor logo
	},
});

Widget Loading Approaches

There are two ways to load and initialize widgets.

ApproachHow it worksBest for
Init + Data AttributesInitialize once, then add data-component elementsMultiple widgets, static HTML, SSR
loadWidget() APIFetch config from the API and render dynamicallySingle dynamic widget, SPAs

Approach 1: Traditional Init + Data Attributes

Initialize the widget library once, then add widget elements with data attributes to your HTML.

Step 1: Load and initialize the widget library

<script src="https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.iife.js"></script> <!-- or fu-widgets.es.js for ESM -->
<script>
	FuWidget.init({
		fansUnited: {
			apiKey: "your-api-key",
			clientId: "your-client-id",
			environment: "prod",
			lang: "en",
		},
		firebase: {
			apiKey: "your-firebase-api-key",
			authDomain: "your-app.firebaseapp.com",
			projectId: "your-project-id",
			appId: "your-app-id",
		},
		language: "en",
	});
</script>

Step 2: Add widget elements with data attributes

<div data-component="fu-widget" data-content-type="classic-quiz" data-content-id="your-quiz-id"></div>

<div data-component="fu-widget" data-content-type="poll" data-content-id="your-poll-id"></div>

Best for:

  • Multiple widgets on the same page
  • Static HTML pages
  • Server-side rendered content

Approach 2: API Config Fetching with loadWidget()

This approach fetches the widget configuration from the API and loads a single widget dynamically. No need to call FuWidget.init() first.

IIFE Version:

<script src="https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.iife.js"></script>
<script>
	// Load widget with API config fetching
	FuWidget.loadWidget({
		// Required: API credentials
		apiKey: "your-api-key",
		clientId: "your-client-id",
		configId: "your-config-id",

		// Required: Widgets content
		contents: [
			{
				id: "your-content-id",
				type: "classic-quiz", // or "poll", "personality-quiz", etc.
				container: "widget-container", // ID or HTMLElement
			}
		],

		// Optional: Config overrides
		configOverrides: {
			language: "en",
			imagePosition: "left",
		},
	});
</script>

<!-- Container where widget will be rendered -->
<div id="widget-container"></div>

ESM Version (with Code Splitting):

<script type="module" src="https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.es.js">
	FuWidget.loadWidget({
		apiKey: "your-api-key",
		clientId: "your-client-id",
		configId: "your-config-id",
		contents: [
			{
				id: "your-content-id",
				type: "classic-quiz",
				container: "widget-container",
			}
		],
		configOverrides: {
			language: "en",
		},
	});
</script>

<div id="widget-container"></div>

OR using an import:

<script type="module">
	import FuWidget from "https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.es.js";

	FuWidget.loadWidget({
		apiKey: "your-api-key",
		clientId: "your-client-id",
		configId: "your-config-id",
		contents: [
			{
				id: "your-content-id",
				type: "classic-quiz",
				container: "widget-container",
			},
		],
		configOverrides: {
			language: "en",
		},
	});
</script>

<div id="widget-container"></div>

Multiple Widgets with Same Config:

<script type="module">
	import FuWidget from "https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.es.js";

	FuWidget.loadWidget({
		apiKey: "your-api-key",
		clientId: "your-client-id",
		configId: "your-config-id",
		contents: [
			{
				id: "quiz-1",
				type: "classic-quiz",
				container: "widget-1",
			},
			{
				id: "quiz-2",
				type: "personality-quiz",
				container: "widget-2",
			},
		],
		configOverrides: {
			language: "en",
		},
	});
</script>

<div id="widget-1"></div>
<div id="widget-2"></div>

Without Container (Inserts before script tag):

<!-- Widget will be inserted here, before the script tag -->
<script type="module">
	import FuWidget from "https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.es.js";

	FuWidget.loadWidget({
		apiKey: "your-api-key",
		clientId: "your-client-id",
		configId: "your-config-id",
		contents: [
			{
				id: "your-content-id",
				type: "classic-quiz",
				// No container - widget is inserted before the script tag
			},
		],
		configOverrides: {
			language: "en",
		},
	});
</script>

Leaderboard Widget Example:

<script type="module">
	import FuWidget from "https://cdn.jsdelivr.net/npm/fansunited-widgets-cdn@<version>/fu-widgets.es.js";

	FuWidget.loadWidget({
		apiKey: "your-api-key",
		clientId: "your-client-id",
		configId: "your-config-id",
		contents: [
			{
				type: "leaderboard",
				entities: [
					{ id: "your-match-quiz-id", type: "MATCH_QUIZ" },
					{ id: "your-top-x-id", type: "TOP_X" },
				],
				container: "leaderboard-container",
			},
		],
	});
</script>

<div id="leaderboard-container"></div>

loadWidget() Options:

OptionTypeRequiredDescription
apiKeystringYesFans United API key for Client API authentication
clientIdstringYesYour Fans United client ID
configIdstringYesConfiguration ID to fetch from the API
contentsWidgetContent[]YesArray of widgets to load (see below)
configOverridesPartial<Config>NoOverride specific configuration options

WidgetContent Object:

PropertyTypeRequiredDescription
idstringNo*Content ID for the widget. Required for all widget types except leaderboard
typestringYesWidget type (e.g., "classic-quiz", "poll", "leaderboard")
containerstring | HTMLElementNoContainer element ID or HTMLElement where widget will be rendered
entitiesLeaderboardEntity[]No*Array of leaderboard entities. Required (instead of id) when type is "leaderboard"
📘

Note: Either id or entities must be provided — passing neither will throw an error.

Container Behavior:

  • If container is provided: the widget element is created and appended to the specified container.
  • If container is omitted: the widget element is inserted before the current script tag (standard for dynamic scripts).

Authentication

The widget supports multiple authentication methods for connecting to the Fans United API. They are processed in priority order: direct token → cookie token → Firebase anonymous.

Authentication Hierarchy

  1. If tokenString is provided, use it directly.
  2. If tokenString is not provided but tokenCookieName is, check for the cookie:
    • If the cookie exists, use its value as the token.
    • If the cookie does not exist, fall back to Firebase authentication.
  3. If neither tokenString nor tokenCookieName is provided, use Firebase authentication.

1. Direct Token Authentication

Provide a valid authentication token directly in the configuration. This is the highest priority method and will be used if provided.

FuWidget.init({
	fansUnited: {
		// ... other config
		tokenString: "your-auth-token", // Direct authentication token
	},
	// ... other config
});

2. Cookie-based Authentication

Use a token stored in a browser cookie. The widget looks for a cookie with the specified name and uses its value as the authentication token. This method is used if direct token authentication is not provided and the cookie exists.

FuWidget.init({
	fansUnited: {
		// ... other config
		tokenCookieName: "your-token-cookie-name", // Name of the cookie containing the token
	},
	// ... other config
});
📘

Note: When using tokenCookieName, the widget automatically re-fetches the cookie value for each API request, ensuring it always uses the latest token value.

3. Firebase Anonymous Authentication (Default)

If neither tokenString nor a valid tokenCookieName cookie is provided, the widget falls back to Firebase anonymous authentication. This is the default method and requires valid Firebase configuration.

FuWidget.init({
	fansUnited: {
		// ... other config
	},
	firebase: {
		apiKey: "your-firebase-api-key",
		authDomain: "your-app.firebaseapp.com",
		projectId: "your-project-id",
		appId: "your-app-id",
	},
	// ... other config
});
📘

See Features → Authentication Requirements for FREE / LEAD / REGISTERED content gating, and Features → Sign-in & Additional CTA for customising the sign-in prompt shown to anonymous users.