Betslip Widget — Integration Guide
This guide is for frontend developers who already have the SMP widgets running on their site (script tag loaded, sdkOptions wired up) and now want to drop in the Betslip widget, configure it, and drive it from page code.
The Betslip is a standalone overlay widget: it mounts once per page, listens for selection events from the sport widgets (Livescore, Single Event, Odds, Multisport, etc.), and renders a floating betslip in a corner of the viewport.
1. Mount the widget
Add a single host element anywhere in your page. The widget renders into its own shadow DOM — placement of this element is irrelevant because the UI is position-fixed.
<div
data-widget-id="betslip"
data-widget-type="betslip"
data-widget-sport="multisport"
data-theme="light"
data-refresh-time="fast"
></div>
Required attributes
| Attribute | Value | Notes |
|---|---|---|
data-widget-id | "betslip" | Tells the SDK which widget to render into this host. |
data-widget-type | "betslip" | |
data-widget-sport | See list below | Any supported sport key works — the betslip is sport-agnostic, the value is used only for analytics/subscriber payloads. |
Supported data-widget-sport values (from packages/constants/sportType.ts):
| Value | Sport |
|---|---|
"football" | Football |
"basketball" | Basketball |
"tennis" | Tennis |
"ice-hockey" | Ice hockey |
"multisport" | Multi-sport (recommended for cross-sport pages) |
Optional attributes
| Attribute | Description |
|---|---|
data-theme | "light" | "dark" | "client". Defaults to "light". |
data-refresh-time | Odds polling cadence. Named preset, one of "super_fast" (15s), "fast" (30s), "medium" (60s), "slow" (180s), "super_slow" (600s), "never". Defaults to "never". |
data-labels | Text labels dictionary (see section 3.2). |
data-betslip-config | Runtime config object (see section 3.1) — preferred configuration path. |
data-currency-symbol-position | "prefix" | "suffix". |
data-date-format | Day.js format string for event dates. |
Only one betslip host is allowed per page. If multiple elements with
data-widget-id="betslip" are found, the SDK logs a console warning and mounts
only the first.
2. Load the SDK
The betslip ships its own SDK bundle and its own global:
window.smpBetslipWidgets. Wait for its registration event, then call
LoadSmpWidget with your options.
<script type="module" src="https://widgets.sportal365.com/betslip.19.6.0.js"></script>
<script>
window.addEventListener('smpBetslipWidgetsRegistered', () => {
window.smpBetslipWidgets.LoadSmpWidget({
sdkOptions: {
dataConfigLang: 'en',
dataConfigTimezone: 'Europe/Sofia',
},
widgetAttributes: {
'data-betslip-config': {
enabled: true,
position: 'bottom-right',
maxSelections: 10,
betType: 'multiples',
quickStakes: '5,10,20',
},
'data-labels': {
betslip_title: 'Betslip',
betslip_continue_to: 'Continue to',
betslip_age_restriction: '18+',
betslip_responsible_gambling: 'Gamble Responsibly',
betslip_responsible_gambling_url: 'https://www.begambleaware.org',
},
},
onLoaded: () => {
window.smpBetslipWidgets.subscribe('betslipSelectionAdd', (p) => console.log('added', p));
window.smpBetslipWidgets.subscribe('betslipSelectionRemove', (p) => console.log('removed', p));
window.smpBetslipWidgets.subscribe('bettingLogo', (p) => console.log('CTA click', p));
},
});
});
</script>
The Betslip SDK is independent from the sport SDKs (smpFootballWidgets, smpMultisportWidgets, …), so you call both — your sport SDK to render the odds widgets that produce selections, and smpBetslipWidgets to mount the betslip overlay that consumes them. They communicate via window custom events (see section 4); there is no direct SDK-to-SDK coupling.
Enabling the betslip on sport widgets
For a sport widget to emit selection-add events into the betslip, add
data-betslip-enabled="true" on that widget's host element. This works on any
widget that renders odds cells — Livescore, Single Event, Odds, Multisport,
etc.
<!-- Event-scoped odds widget feeding the betslip -->
<div
data-widget-id="odds"
data-match-id="7834698"
data-widget-type="event"
data-widget-sport="football"
data-odds-pre-event-only="true"
data-betslip-enabled="true"
></div>
<!-- Tournament livescore also feeding the same betslip -->
<div
data-widget-id="livescore"
data-widget-type="tournament"
data-widget-sport="football"
data-odds-display="true"
data-betslip-enabled="true"
…
></div>
You can enable data-betslip-enabled on any number of sport widgets on the
same page — clicks on any of them feed the single shared betslip. Without the
flag, clicking an odds cell navigates directly to the bookmaker instead of
adding to the slip.
3. Configure the widget
There are three configuration surfaces, merged in this order (later wins):
- Built-in defaults (ship with the widget).
sdkOptions.dataConfigBetslip— deprecated legacy fallback, still read for backward compatibility. Do not use for new integrations. Will be removed in a future major version.widgetAttributes['data-betslip-config']— canonical path. Per-widget attribute owned by the betslip host, so host-page code doesn't have to couple to sport SDK options.
3.1 data-betslip-config
data-betslip-configinterface BetslipSDKConfig {
enabled?: boolean; // false → widget does not render
position?: 'bottom-right' | 'bottom-left';
pageType?: string; // current page type for `allowedPages` gating
allowedPages?: string; // CSV of page types where betslip is allowed
widgetSources?: string; // CSV of `data-widget-id`s allowed as event sources
maxSelections?: number; // default: 10
betType?: 'singles' | 'multiples'; // default: 'multiples'
quickStakes?: string; // CSV of stake presets, e.g. '5,10,20'
ageRestrictionDisplay?: 'visible' | 'hidden';
widgetFilter?: {
mode: 'allowlist' | 'blocklist';
identifiers: string[]; // `data-widget-id` values to match against event source
};
compliance?: {
ageRestriction?: string;
responsibleGamblingText?: string;
responsibleGamblingUrl?: string;
disclaimer?: string;
licenseDisplay?: string | null;
};
}
Notes:
enabled: falsedisables the widget entirely — the component returnsnulland does not listen for selection events.pageType+allowedPages: if the host page type is not in the allow-list, the widget does not render. LeavepageTypeunset (or omitallowedPages) to render everywhere.widgetFilterscopes which source widgets can add selections. Withmode: 'allowlist'andidentifiers: ['football-livescore'], selection events from other sources are silently dropped. Withmode: 'blocklist', listed sources are ignored.quickStakesis a CSV string, not an array — e.g.'5,10,20'.
3.2 data-labels
data-labelsAll user-facing copy is driven by the labels dictionary. Any key you omit falls
back to the built-in default. Full set (from
apps/betslip/src/widgets/Betslip/constants.ts):
{
// Chrome / layout
betslip_title: 'Betslip',
betslip_continue_to: 'Continue to',
betslip_combined_odds: 'Combined Odds:',
betslip_total_stake: 'Total Stake:',
betslip_potential_win: 'Potential Win:',
betslip_stake: 'Stake:',
betslip_currency: '\u20AC', // € by default
betslip_quick_stake_1: '5',
betslip_quick_stake_2: '10',
betslip_quick_stake_3: '20',
// Banner / status
betslip_opposing_outcomes: 'Opposing outcomes restricted.',
betslip_max_selections: 'Maximum selections reached.',
betslip_settled_info: 'Some of your selections have been settled.\nThey are excluded from the Total Stake calculations.',
betslip_all_settled: 'All selections have been settled.\nAdd new selections to continue.',
betslip_provider_switch_title: 'One provider at a time.',
betslip_provider_switch_description: 'Switching providers will clear your selections.',
betslip_provider_switch_confirm: 'Switch provider',
betslip_provider_switch_cancel: 'Cancel',
betslip_finished: 'Finished',
betslip_suspended: 'Suspended',
// Footer / compliance
betslip_age_restriction: '18+',
betslip_terms: 'T&C Apply',
betslip_responsible_gambling: 'Gamble Responsibly',
betslip_responsible_gambling_url: '', // set to enable the footer link
// Market-type keys (rendered on selection cards via labels[generic.marketType])
'1x2': '1X2',
'FIRST HALF': 'First Half',
'SECOND HALF': 'Second Half',
OVER_UNDER: 'Over/Under',
DOUBLE_CHANCE: 'Double Chance',
BOTH_TO_SCORE: 'Both Teams to Score',
DRAW_NO_BET: 'Draw No Bet',
FIRST_TEAM_TO_SCORE: 'First Team to Score',
CORRECT_SCORE: 'Correct Score',
FIRST_HALF_GOALS: 'First Half Goals',
FIRST_PLAYER_TO_SCORE: 'First Player to Score',
PLAYER_TO_SCORE_DURING_GAME: 'Player to Score During Game',
PLAYER_TO_RECEIVE_CARD: 'Player to Receive Card',
FIRST_HALF_AND_FINAL_RESULT: 'First Half and Final Result',
// Bare selection values (when the API returns selection: 'over' / 'under' / 'yes' / 'no')
over: 'Over',
under: 'Under',
yes: 'Yes',
no: 'No',
}
Market-type keys are also used to translate the market label shown on each
selection card — the card renders labels[generic.marketType] ?? generic.marketName,
so the API-provided marketName is used as a fallback when no label override
exists.
Labels need to be configured on BOTH SDKs. Market-type and selection-name
labels shown inside the odds widget (sport SDK) and inside the betslip (betslip SDK) read their own
data-labelsdictionaries independently. For consistent rendering pass the same keys to bothsmpFootballWidgets.LoadSmpWidget(...)andsmpBetslipWidgets.LoadSmpWidget(...).
3.3 Theming
data-theme:"light"|"dark"|"client".- Pass
themesinLoadSmpWidgetto override CSS tokens for theclienttheme. The shape is{ light?: { colors?: {...}, fonts?: {...} }, dark?: {...}, client?: {...} }. - Bookmaker branding colors (tier 2 of the cascade) are applied on top of the theme automatically when a selection is added; manual
themesoverrides (tier 3) still win.
4. Controlling the widget
The betslip is primarily event-driven. It listens for typed CustomEvents
on window from the sport widgets — and you can dispatch the same events from
your own code to drive the widget programmatically.
4.1 Add a selection to the betslip
Dispatch smpBetslipSelectionAdd:
window.dispatchEvent(
new CustomEvent('smpBetslipSelectionAdd', {
bubbles: true,
detail: {
source: {
widgetId: 'my-custom-source', // must pass widgetFilter if configured
widgetType: 'multisport',
widgetSport: 'football',
widgetCuid: 'any-unique-id',
},
selection: {
generic: {
eventId: 'match_123', // required — dedupe key
competition: 'premier-league',
competitionName: 'Premier League',
competitionLogo: 'https://…/logo.png',
participants: {
home: { name: 'Arsenal', logo: 'https://…/arsenal.png' },
away: { name: 'Chelsea', logo: 'https://…/chelsea.png' },
},
eventDate: '2026-05-12T19:45:00Z',
marketType: '1x2',
marketName: 'Full Time Result',
selection: '1',
selectionName: 'Arsenal',
// optional
sport: 'football',
isLive: false,
score: { home: '0', away: '0' },
},
bookmakerSpecific: {
bookmaker: 'bet365', // required
bookmakerName: 'Bet365',
bookmakerLogo: 'https://…/bet365.png',
bookmakerColor: '#027B5B',
bookmakerTextColor: '#FFFFFF',
bookmakerEventId: 'b365_evt_123',
bookmakerMarketId: 'b365_m_1',
bookmakerSelectionId: 'b365_s_1',
odds: 2.35,
oddsMovement: 'none', // 'up' | 'down' | 'none'
deepLinkCompatible: true,
fallbackEventUrl: 'https://bet365.com/event/123',
// optional — needed for deep-linking on CTA click
betslipId: 'b365_betslip_1',
betslipUrlTemplate: 'https://bet365.com/bet?ids=$betslipIds$&stake=$stake$',
betslipDelimiter: ',',
homepageUrl: 'https://bet365.com',
},
},
},
}),
);
Behavior:
- First selection → betslip opens in the expanded state.
- Subsequent adds while the betslip is closed → open it in the collapsed state; collapsed/expanded adds respect the user's current UI state.
- Duplicate selection (same eventId + marketType + selection) → ignored.
- Over maxSelections → rejected, max-selections banner shown.
- Opposing selection on the same market → rejected, opposing-outcomes banner shown for 5 seconds.
- Different bookmaker than the current one → provider-switch prompt shown; the selection is queued until the user confirms or cancels.
4.2 Settle a selection
The betslip does not expose "remove by eventId" directly. The settle event
marks selections for a given eventId as status: 'settled' — they stay visible
on the card list (greyed out) but drop out of combined-odds math. Dispatch
smpBetslipSelectionSettle:
window.dispatchEvent(
new CustomEvent('smpBetslipSelectionSettle', {
bubbles: true,
detail: { eventId: 'match_123' },
}),
);
Settle does not clear selections. To fully empty the betslip
programmatically, either dispatch close via the close UI state (user clicks
the X button) or clear the sessionStorage key smp-betslip-state and
re-render. There is no public SDK method for "clear all" at this time.
4.3 Open / close the widget
The widget's open/closed state is tied to its selection state:
| Action | Resulting UI state |
|---|---|
| No selections | Closed (nothing rendered on screen). |
Dispatch smpBetslipSelectionAdd (1st) | Expanded. |
Dispatch smpBetslipSelectionAdd (Nth) | Stays in current state; reopens collapsed if closed. |
| User clicks the toggle (chevron) | Expanded ↔ Collapsed. |
| User clicks close (X) | Closed and all selections cleared. |
| Last selection removed (manually or by settle) | Betslip stays mounted; closes only when user closes it. |
There is no public SDK method to force open/close. The UI is driven
entirely by selection state and user interaction. You "open" the betslip by
dispatching a selection, and "close" it by having the user click X.
To disable the widget on a given route, set
data-betslip-config.enabled: false and call RefreshWidget (or reload with
the new config) — the widget returns null and detaches listeners.
5. Subscribing to widget events
Use smpBetslipWidgets.subscribe(event, callback) to observe user interactions. The callback payload shape mirrors the sport SDKs' ISubscriberInfo contract: { dataWidgetId, dataWidgetSport, clickType, info, dataWidgetCuid }.
| Event | When it fires | Key fields in info |
|---|---|---|
betslipSelectionAdd | A selection is accepted into the betslip. | id, name, entity_type, market, link |
betslipSelectionRemove | A selection is removed (manual / settled / bookmaker-swap). | Same shape as add. |
bettingLogo | CTA "Continue to <bookmaker>" is clicked (conversion event). | id, name, link |
onUserInteraction | Granular UI action. | name is one of: betslip_expanded, betslip_collapsed, betslip_closed, betslip_quick_stake, betslip_provider_switch_confirm, betslip_provider_switch_cancel. |
bettingLogomeans two different things across the two SDKs. The sport SDK (smpFootballWidgets.subscribe('bettingLogo', …)) fires when a user clicks an odds cell or a bookmaker logo in an odds widget — the "click through to bookmaker" intent before any betslip interaction. The betslip SDK (smpBetslipWidgets.subscribe('bettingLogo', …)) fires when the user clicks "Continue to<bookmaker>" on the betslip CTA — the conversion event after building a slip. Subscribe to both if you need analytics for both journeys; the buses are independent and publish to the SDK that rendered the originating widget.
6. SDK reference
window.smpBetslipWidgets:
| Method | Purpose |
|---|---|
LoadSmpWidget(data) | Mount the betslip (and any other discovered hosts). Call once after the smpBetslipWidgetsRegistered event. |
AddSmpWidget(data, element) | Mount into a specific host without re-rendering the rest. |
RefreshWidget(elementId) | Re-render a single widget by instance cuid (the data-id attribute the SDK sets on each mounted host). |
RefreshAllWidgets() | Re-render every mounted widget. |
subscribe(event, cb) | Register an event callback (see section 5). |
AreWidgetsRendered(cuids) | Returns true once every instance cuid in the list has rendered. Expects the data-id values, not data-widget-id strings. Grab them from onLoaded's widgets list. |
ExtractOddsCount() | Debug helper — returns the accumulated odds count (inherited from the shared SDK pattern). |
LoadSmpWidget / AddSmpWidget data shape:
interface BetslipLoadSmpData {
sdkOptions: {
dataConfigLang: string;
dataConfigTimezone: string;
/** @deprecated use widgetAttributes['data-betslip-config'] */
dataConfigBetslip?: BetslipSDKConfig;
};
widgetAttributes?: {
'data-betslip-config'?: BetslipSDKConfig;
'data-labels'?: Record<string, string>;
// …any other widget-level attribute
};
attributeOverrides?: Record<string, unknown>;
themes?: {
light?: { colors?: Record<string, string>; fonts?: Record<string, string> };
dark?: { colors?: Record<string, string>; fonts?: Record<string, string> };
client?: { colors?: Record<string, string>; fonts?: Record<string, string> };
};
onLoaded?: (widgets: Array<{ root: unknown; id: string; widgetId?: string }>) => void;
getAuthToken?: () => string;
signOut?: () => string;
}7. Troubleshooting
- Widget does not render. Check
enabledis notfalse, the host element exists and hasdata-widget-id="betslip", and (if you setpageType) that the current page type is inallowedPages. - Clicking odds does nothing. The source sport widget needs
data-betslip-enabled="true"on its host element. - Selections come through but are filtered out. Check
widgetFilter.modeandidentifiersagainst thesource.widgetIdyou dispatch. - Two betslips stacked. Only one
data-widget-id="betslip"host is supported per page — remove duplicates. The SDK logs a console warning when it detects more than one. - State persists across reloads. The betslip persists to
sessionStorageundersmp-betslip-state. Clear it if you need a clean slate during development. AreWidgetsRendered(['betslip'])never returns true. The method expects instance cuids (data-idattribute the SDK sets after mount), notdata-widget-idvalues. Capture the cuids from theonLoadedcallback's widgets list and pass those.
Updated about 13 hours ago