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

AttributeValueNotes
data-widget-id"betslip"Tells the SDK which widget to render into this host.
data-widget-type"betslip"
data-widget-sportSee list belowAny 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):

ValueSport
"football"Football
"basketball"Basketball
"tennis"Tennis
"ice-hockey"Ice hockey
"multisport"Multi-sport (recommended for cross-sport pages)

Optional attributes

AttributeDescription
data-theme"light" | "dark" | "client". Defaults to "light".
data-refresh-timeOdds polling cadence. Named preset, one of "super_fast" (15s), "fast" (30s), "medium" (60s), "slow" (180s), "super_slow" (600s), "never". Defaults to "never".
data-labelsText labels dictionary (see section 3.2).
data-betslip-configRuntime config object (see section 3.1) — preferred configuration path.
data-currency-symbol-position"prefix" | "suffix".
data-date-formatDay.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):

  1. Built-in defaults (ship with the widget).
  2. sdkOptions.dataConfigBetslipdeprecated legacy fallback, still read for backward compatibility. Do not use for new integrations. Will be removed in a future major version.
  3. 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

interface 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: false disables the widget entirely — the component returns null and does not listen for selection events.
  • pageType + allowedPages: if the host page type is not in the allow-list, the widget does not render. Leave pageType unset (or omit allowedPages) to render everywhere.
  • widgetFilter scopes which source widgets can add selections. With mode: 'allowlist' and identifiers: ['football-livescore'], selection events from other sources are silently dropped. With mode: 'blocklist', listed sources are ignored.
  • quickStakes is a CSV string, not an array — e.g. '5,10,20'.

3.2 data-labels

All 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-labels dictionaries independently. For consistent rendering pass the same keys to both smpFootballWidgets.LoadSmpWidget(...) and smpBetslipWidgets.LoadSmpWidget(...).


3.3 Theming

  • data-theme: "light" | "dark" | "client".
  • Pass themes in LoadSmpWidget to override CSS tokens for the client theme. 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 themes overrides (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:

ActionResulting UI state
No selectionsClosed (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 }.

EventWhen it firesKey fields in info
betslipSelectionAddA selection is accepted into the betslip.id, name, entity_type, market, link
betslipSelectionRemoveA selection is removed (manual / settled / bookmaker-swap).Same shape as add.
bettingLogoCTA "Continue to <bookmaker>" is clicked (conversion event).id, name, link
onUserInteractionGranular UI action.name is one of: betslip_expanded, betslip_collapsed, betslip_closed, betslip_quick_stake, betslip_provider_switch_confirm, betslip_provider_switch_cancel.

bettingLogo means 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:

MethodPurpose
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 enabled is not false, the host element exists and has data-widget-id="betslip", and (if you set pageType) that the current page type is in allowedPages.
  • 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.mode and identifiers against the source.widgetId you 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 sessionStorage under smp-betslip-state. Clear it if you need a clean slate during development.
  • AreWidgetsRendered(['betslip']) never returns true. The method expects instance cuids (data-id attribute the SDK sets after mount), not data-widget-id values. Capture the cuids from the onLoaded callback's widgets list and pass those.