Skip to content

Integrating DATA.BET Widgets

If you have DATA.BET SPA integrated there is no need to set up widgets additionally.

Before integrating the widgets, it is required to integrate Odds Feed first.

The integration of widgets consists of two parts: back-end and front-end. First, on the back-end side, must generate a token to access widgets. Then, on the front-end side, you need to add the script and widget elements to the page.

This section covers the front-end part of the integration.

Importing the script

Add one of the following scripts to your page, depending on the environment you are working in:

1
<script type="module" src="https://widgets-static.int.databet.cloud/bootstrap.js">
1
<script type="module" src="https://widgets-static.databet.cloud/bootstrap.js">

Adding widget elements

After importing the script, you can add databet-widget elements on your page.

Attributes are as follows:

Examples of adding databet-widget element:

1
<databet-widget type="multi" token="<JWT_TOKEN>" locale="en"></databet-widget>
1
2
3
4
5
6
7
<databet-widget type="scoreboard" token="<JWT_TOKEN>" locale="en"></databet-widget>
<databet-widget type="pitch_tracker" token="<JWT_TOKEN>" locale="en"></databet-widget>
<databet-widget type="games_statistics" token="<JWT_TOKEN>" locale="en"></databet-widget>
<databet-widget type="teams_statistics" token="<JWT_TOKEN>" locale="en"></databet-widget>
<databet-widget type="h2h_statistics" token="<JWT_TOKEN>" locale="en"></databet-widget>
<databet-widget type="genius" token="<JWT_TOKEN>" locale="en"></databet-widget>
<databet-widget type="stream" token="<JWT_TOKEN>" locale="en"></databet-widget>

To determine whether a widget type is available for the sport event, check extensions list app.data.bet/widget in event extensions_updated. In order to display Multi Widget, ensure there is at least one value in the list.

To manage widget types enabled for your account, refer to STM Widget Configuration or contact DATA.BET Integration Manager.

Supported locales

The current list of supported locales is represented in the table below:

Code Language
bg Bulgarian
cs Czech
da Danish
de German
el Greek
en English
es Spanish
et Estonian
fi Finnish
fr French
hu Hungarian
it Italian
ja Japanese
ms Malay
nl Dutch
no Norwegian
pl Polish
pt Portuguese
ro Romanian
ru Russian
th Thai
tl Tagalog
tr Turkish
uk Ukrainian
vi Vietnamese
zh Chinese

The list may expand over time. If you need additional locales, please contact your DATA.BET Integration Manager.

Configuring paywall UI for video stream widget

Video stream widget may be protected by paywall. You can configure the UI indicating your users what steps they need to take to access the widget. The UI will be displayed in the next cases:

  • user is not logged in (claim sub in the JWT is empty)
  • paywall is not passed (see claims paywall_passed and paywall_preference in the JWT)

To configure the paywall UI, add a child element to databet-widget with attribute slot equal to paywall.

Example of configuring the paywall UI:

1
2
3
4
5
6
<databet-widget type="multi" token="<JWT_TOKEN>" locale="en">
    <div slot="paywall">
        <p>Log in to watch stream</p>
        <button>Log in</button>
    </div>
</databet-widget>

If paywall element is not provided, the default paywall UI will be displayed. Examples:

Paywall Stub - not logged in

Paywall Stub - not passed paywall

Note

After the user completes the paywall requirements, make sure to provide him with a new JWT token with the paywall_passed claim set to true.

Handling login button click

In order to improve UX, you may opt to having a button in the default paywall UI when user is not logged in.

Example of Log in button

Log in button

The button can be enabled by your integration manager.

To handle the click, listen for widget:login event on the databet-widget element or its parents (the event is bubbled). Example:

1
2
3
4
5
<databet-widget id="databetwidget" type="multi" token="<JWT_TOKEN>" locale="en"></databet-widget>
<script>
  const widget = document.getElementById('databetwidget');
  widget.addEventListener('widget:login', () => {/** handle click here */});
</script>

Customizing the widget

DATA.BET Widgets can be customized to meet your design requirements.

Restricting the size

By default, the widget stretches to 100% of the width and height of the parent element. You can restrict the parent element's size or directly style the widget.

Warning

Setting min-height or max-height instead of height will not make the widget's height responsive.

Some examples of widget styling:

1
2
3
4
databet-widget {
  width: 550px;
  height: 320px;
}
1
2
3
4
5
6
<databet-widget
  type="multi"
  token="<JWT_TOKEN>"
  locale="en"
  style="width: 550px; height: 320px;"
></databet-widget>
1
2
3
<div style="width: 550px; height: 320px;">
  <databet-widget type="multi" token="<JWT_TOKEN>" locale="en"></databet-widget>
</div>

The restrictions for minimal widget size are as follows:

Type Width Height
Multi widget (type="multi") 300 px 260 px
Single widget (other types) 300 px 225 px

Custom CSS variables

You can customize the appearance using custom CSS variables.

General layout customization

CSS Variables for general layout customizations are as follows:

  • --widget-gap-between-widgets: The gap between widgets in Multi Widget layout.
  • --widget-border-radius: The border radius of widget containers.

Some examples of layout customization:

--widget-gap-between-widgets: 10px vs 30px

Gap between widgets 10px

Gap between widgets 30px

--widget-border-radius: 0px vs 20px

Widget border radius 0px

Widget border radius 20px

Common color customization

CSS Variables for common color customizations are as follows:

  • --widget-primary-color: The primary color of the widget.
  • --widget-secondary-color (deprecated: use --widget-overlay-bg-color instead): The secondary color of the widget.
  • --widget-accent-color: The accent color of the widget.
  • --widget-accent-secondary-color: The secondary accent color, used for highlights that pair with the main accent.
  • --widget-bg-color: The background color of the widget.
  • --widget-single-bg-color: Background color override for single-widget layouts. Falls back to --widget-bg-color when unset.
  • --widget-overlay-bg-color: The background color of overlays, tooltips, and tab selectors.
  • --widget-text-button-color: Text color used inside buttons.
  • --widget-error-color: Color for error states and warning messages.
  • --widget-success-color: Color for success states and positive confirmations.

Some examples:

--widget-primary-color: #4e4eee (Counter-Strike Scoreboard)

palette example

--widget-secondary-color: #4e4eee (Dota 2)

palette example

--widget-accent-color: #4e4eee (Counter-Strike Scoreboard)

palette example

--widget-bg-color: #4e4eee (Counter-Strike Scoreboard)

palette example

--widget-bg-color: #8080ff (Dota 2 Pitch Tracker)

palette example

--widget-overlay-bg-color

Example widget-overlay-bg-color "(unset)"

Example widget-overlay-bg-color "dark blue"

Example widget-overlay-bg-color "black"

Sports-specific color customization

CSS Variables for sports-specific color customizations are as follows:

  • --widget-first-team-color: The first team's color in the widget.
  • --widget-second-team-color: The second team's color in the widget.

Definitions of the first and second teams depend on the sport:

Sport first team second team
Counter-Strike CT T
Valorant Defender Attacker
Dota 2 Radiant Dire
League of Legends Blue Red

Some examples of color customization:

--widget-first-team-color: #4e4eee (Counter-Strike)

palette example

--widget-first-team-color: #8080ff (Dota 2)

palette example

--widget-second-team-color: #4e4eee (Counter-Strike)

palette example

--widget-second-team-color: #8080ff (Dota 2)

palette example

Pitch Tracker color customization

CSS Variables for Pitch Tracker-specific color customizations are as follows:

  • --widget-map-color: The background color of the map in the Pitch Tracker widget.
--widget-map-color: black vs darkslategray

Example of black map color

Example of darkslategray map color

In-Stream Betting color customization

CSS Variables for In-Stream Betting-specific color customizations are as follows:

  • --widget-stream-betting-bg-color: The background color of the In-Stream Betting overlay.

Adding a custom tab to Multi Widget

To add a custom tab to Multi Widget, add element with attribute slot prefixed with tab-. This utilizes Web Component Slot element. Example of adding a custom tab:

1
2
3
<databet-widget type="<WIDGET_TYPE>" token="<JWT_TOKEN>" locale="en">
  <div slot="tab-<tab-slot-name>">OWN_CONTENT</div>
</databet-widget>
Example of custom tab

palette example

Configuring order of widgets in Multi Widget

To configure the order of widgets in Multi Widget, add attribute order to the databet-widget element. The value of the attribute is a comma-separated list of widget types (no spaces between values).

If some of the provided widget types are missing in the Multi Widget, they are ignored.

If Multi Widget contains widget types that not specified in the attribute, they go after the specified ones in the default order. The default order is not guaranteed and may always be changed.

Example of configuring the order of widgets in Multi Widget:

1
<databet-widget type="multi" token="<JWT_TOKEN>" order="stream,scoreboard,pitch_tracker" locale="en"></databet-widget>

In the example above, widgets will be arranged in the following order:

  • Video Stream (if available)
  • Scoreboard (if available)
  • Pitch Tracker (if available)
  • ... remaining widgets in the default order

Excluding widgets from Multi Widget

To exclude some widgets from Multi Widget, add attribute exclude to the databet-widget element. The value of the attribute is a comma-separated list of widget types (no spaces between values).

This may be useful if you want to display Multi Widget and arrange a separate widget type somewhere else on the page.

Example of excluding widgets from Multi Widget:

1
2
3
4
5
6
<databet-widget type="multi" token="<JWT_TOKEN>" exclude="stream,pitch_tracker" locale="en"></databet-widget>
<!-- this goes somewhere else on the page: -->
<div>
    <databet-widget type="stream" token="<JWT_TOKEN>" locale="en"></databet-widget>
    <databet-widget type="pitch_tracker" token="<JWT_TOKEN>" locale="en"></databet-widget>
</div>

Note

Excluding widgets this way is a front-end customization. To disable widget types for your account, refer to STM Widget Configuration or contact DATA.BET Integration Manager.

Configuring Multi Widget side-by-side layout

Multi Widget supports side-by-side layout (see example). To enable it, set attribute sideBySideBreakpoint (in pixels). The minimal recommended value is 760. When Multi Widget reaches the breakpoint width, its layout splits into two columns.

Example of configuring Multi Widget layout breakpoint:

1
<databet-widget type="multi" token="<JWT_TOKEN>" sideBySideBreakpoint="760" locale="en"></databet-widget>

The widget that goes to the left side is the first one from the Multi Widget. To configure it, use order customization.

Customizing Genius Widget Game Tracker

Genius Widget Game Tracker can be customized adding its own attribtes to the databet-widget element:

  • geniusLayout - layout of the Genius widget. Allowed values: regular, sideBySide, standalone
  • geniusWidget - widget type when geniusLayout is standalone (otherwise must not be specified).

Supported values for geniusLayout depend on the sport:

Sport regular sideBySide standalone
American Football
Basketball
Football
Tennis

The geniusWidget attribute can only be used when the geniusLayout is set to standalone. This allows you to specify the desired Genius widget type manually.

Sport pitchTracker playByPlay teamStats
American Football
Basketball
Football
Tennis

Example of adding databet-widget with custom Genius setup:

1
2
3
4
5
6
<databet-widget
  type="genius"
  token="<JWT_TOKEN>"
  geniusLayout="standalone"
  geniusWidget="teamStats"
></databet-widget>

Some visual examples:

American Football with regular layout

American Football

American Football with standalone layout and playByPlay widget

American Football

American Football with standalone layout and teamStats widget

American Football

JSON customization

To make customization more flexible, DATA.BET Widget allows passing configuration as a JSON string via the widgetconfig attribute. Currently, only Multi Widget can be customized using this approach.

The supported configuration schema is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type WidgetConfig = {
  multiWidgetTab?: {
    css?: {
      fontFamily?: string
      height?: string
      text?: {
        color?: WidgetConfigColor
        fontSize?: string
      }
      background?: {
        color?: WidgetConfigColor
      }
      icon?: {
        color?: WidgetConfigColor
      }
      border?: {
        border?: "top" | "bottom" | "left" | "right" | "all"
        radius?: string[]
        width?: string
        color?: WidgetConfigColor
      }
    }
    labels?: Record<string, string>
  }
}

interface WidgetConfigColor {
  default?: string
  hover?: string
  active?: string
}

In the schema above, the only property that requires explanation is labels. It is a dictionary where the key is the widget type (e.g., scoreboard, pitch_tracker, etc.), and the value is the label that will be displayed on the corresponding tab.

Note

If an optional property is set, it will override the css variable if provided.

Example of passing JSON customization to Multi Widget:

1
2
3
4
5
<databet-widget
  type="multi"
  token="<JWT_TOKEN>"
  widgetconfig='{"multiWidgetTab":{"css":{"fontFamily":"Arial, sans-serif"}},"labels":{"scoreboard":"Match Insights","stream":"Live Map"}}'
></databet-widget>

In the example above, the Multi Widget tabs will use Arial, sans-serif font family, the "Scoreboard" tab will be labeled as "Match Insights", and the "Stream" tab will be labeled as "Live Map":

Example of JSON customization above

Example - JSON customization

Observing widget state

The widget reports changes in its state by emitting lifecycle events with type widget:state_changed. Each instance of databet-widget emits its own events. The events are bubbled, so you can also listen for them on any parent element (up to window).

The widget-related payload is stored in the detail property of the event (as per CustomEvent). The payload contains the following properties:

  • type - indicates the lifecycle event type.
    • mounted - the widget is mounted (fired once after the widget is added to the DOM).
    • error - an error occurred (bad widget configuration, network error, etc.).
    • auth_failed - authentication failed (e.g., invalid or expired token).
    • content_missing - no requested widget types are available for the sport event (attributes type and exclude are respected). If the widget is configured correctly, this typically occurs when a requested widget has just become unavailable before the extension app.data.bet/widget is updated (see Adding widget elements). In general, widget availability is eventually consistent with the feed. You may want to handle this event to hide the widget in such cases.
    • loaded - the widget data has been loaded.
    • list_updated - the list of available widget types has been updated (available for Multi Widget).
    • tab_switched - the active tab in Multi Widget has been switched.
    • pip_changed - the Picture-in-Picture mode has been changed.
    • stream_betting_changed - the In-Stream Betting widget has been opened or closed.
    • unmounted - the widget is unmounted (fired once after the widget is removed from the DOM).
  • message - a textual description of the event.
  • data (optional) - contains additional information depending on the event type.

The payload schema is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
enum WidgetEventType {
  MOUNTED = 'mounted',
  ERROR = 'error',
  AUTH_FAILED = 'auth_failed',
  CONTENT_MISSING = 'content_missing',
  LOADED = 'loaded',
  LIST_UPDATED = 'list_updated',
  TAB_SWITCHED = 'tab_switched',
  PIP_CHANGED = 'pip_changed',
  STREAM_PIP_CHANGED = 'stream_pip_changed',
  STREAM_BETTING_CHANGED = 'stream_betting_changed',
  UNMOUNTED = 'unmounted',
}

type WidgetEvent = {
  type: WidgetEventType
  message: string
}

type WidgetEventTabChanged = {
  type: WidgetEventType.TAB_SWITCHED
  data: {
    tabKey: string
  }
}

type WidgetEventPiPChanged = {
  type: WidgetEventType.PIP_CHANGED
  data: {
    isPiPOpen: boolean
  }
}

type WidgetEventStreamPiPChanged = {
  type: WidgetEventType.STREAM_PIP_CHANGED
  data: {
    isStreamPiPOpen: boolean
  }
}

type WidgetEventStreamBettingChanged = {
  type: WidgetEventType.STREAM_BETTING_CHANGED
  data: {
    isStreamBettingOpen: boolean
  }
}

type Detail = WidgetEvent & (WidgetEventTabChanged | WidgetEventPiPChanged | WidgetEventStreamPiPChanged | WidgetEventStreamBettingChanged)

Example of listening for the events in a React component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { useEffect } from 'react'

const handleWidgetEvent = (event: CustomEvent<Detail>) => {
  const { detail } = event
  console.log(`widget state changed: ${detail.type}`, detail.message, detail.data)
}

export function Widget() {
  useEffect(() => {
    const target = document.getElementById('some-widget-id')
    if (!target) return

    target.addEventListener('widget:state_changed', handleWidgetEvent as EventListener)
    return () => target.removeEventListener('widget:state_changed', handleWidgetEvent as EventListener)
  }, [])

  return <databet-widget id="some-widget-id" type="multi" token="<JWT_TOKEN>" />
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect, useRef } from 'react'

const handleWidgetEvent = (event: CustomEvent<Detail>) => {
  const { detail } = event
  console.log(`widget state changed: ${detail.type}`, detail.message, detail.data)
}

export function Widget() {
  const widgetRef = useRef<HTMLElement>(null)

  useEffect(() => {
    const target = widgetRef.current
    if (!target) return

    target.addEventListener('widget:state_changed', handleWidgetEvent as EventListener)
    return () => target.removeEventListener('widget:state_changed', handleWidgetEvent as EventListener)
  }, [])

  return <databet-widget ref={widgetRef} type="multi" token="<JWT_TOKEN>" />
}

In-Stream Betting

The In-Stream Betting widget extends the Video Stream widget with a betting interface that overlays the broadcast. The widget keeps no betting state of its own — it renders exactly what the client pushes and forwards user actions back as events. The client owns all betting state — bet placement, settlement, balance, and currency.

How it works

Communication runs entirely on CustomEvents dispatched on the widget's root element (<databet-widget>). Every event is created with bubbles: true, composed: true: bubbles lets the event travel up the DOM so any ancestor can listen for it, and composed: true lets it cross the widget's Shadow DOM boundary into your page. Without these flags, listeners outside the widget would never see the event.

Details

In-Stream Betting Lifecycle

  1. User opens In-Stream Betting
    1. The user opens the In-Stream Betting overlay on top of the video stream.
    2. The widget emits widget:state_changed with { type: 'stream_betting_changed', data: { isStreamBettingOpen: true } }.
    3. The client dispatches widget:instream_betting:config once — quick-pick stakes, currency, click-confirmation countdown, auth flag.
    4. The client dispatches widget:instream_betting:data with the initial snapshot — competitors, scores, market groups, odds.
    5. The client keeps sending widget:instream_betting:data on every change of odds / markets / scores. Stable IDs (group.id, market.id, odd.id) ensure the widget does minimal re-renders.
  2. User picks an outcome
    1. The user picks an outcome and confirms the stake (after the optional countdown specified in betClickConfirmationCountdownMs).
    2. The widget emits widget:instream_betting:bet_placed with the chosen marketId, oddId, stake, and oddValue at click time.
    3. The client places the bet on its backend.
    4. The client dispatches widget:instream_betting:bet_updated with status PLACED (bet was accepted) or FAILED (with an optional statusReason).
  3. Bet settles
    1. When the bet resolves on the backend, the client dispatches widget:instream_betting:bet_updated with status SETTLED_WIN or SETTLED_LOSS, including the final stake and refund.
    2. Settlement events can be sent at any time — even when the widget is closed. The toast notification is shown the next time the user opens In-Stream Betting.
  4. User closes In-Stream Betting
    1. The user closes the overlay.
    2. The widget emits widget:state_changed with { type: 'stream_betting_changed', data: { isStreamBettingOpen: false } }.
    3. The client stops the data loop to save bandwidth.

Browser tab visibility

Switching to another browser tab and back does not affect the lifecycle — the widget stays mounted and keeps its internal state, so the client does not need to re-emit config or data on tab return. A fresh config + data dispatch is only required when the widget transitions from closed (isStreamBettingOpen: false) back to open.

The available channels are:

Direction Event Purpose
Client → Widget widget:instream_betting:config quick-pick stakes, currency, timeouts
Client → Widget widget:instream_betting:data competitors, scores, markets, odds
Client → Widget widget:instream_betting:bet_updated bet placement result and settlement
Client ← Widget widget:instream_betting:bet_placed user picked an outcome and confirmed the stake
Client ← Widget widget:instream_betting:error invalid data / config payload

Lifecycle

The widget broadcasts open/close changes via the global widget:state_changed channel with type stream_betting_changed (see Observing widget state). Use it to know when to start and stop emitting data updates.

Quick start

A minimal client integration that wires up all five channels:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const widgetRoot = document.querySelector('databet-widget')

widgetRoot.addEventListener('widget:state_changed', (event) => {
  const { type, data } = event.detail
  if (type !== 'stream_betting_changed') return

  if (data.isStreamBettingOpen) {
    pushConfig()
    pushData()
  } else {
    stopDataLoop()
  }
})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function pushConfig() {
  widgetRoot.dispatchEvent(
    new CustomEvent('widget:instream_betting:config', {
      detail: {
        suggestedBets: [
          { value: '1',   type: 'PRESET' },
          { value: '50',  type: 'PRESET' },
          { value: '200', type: 'PRESET' },
        ],
        betClickConfirmationCountdownMs: 2000,
        currency: 'USD',
        isAuth: true,
      },
      bubbles: true,
      composed: true,
    })
  )
}
1
2
3
4
5
6
7
8
9
function pushData() {
  widgetRoot.dispatchEvent(
    new CustomEvent('widget:instream_betting:data', {
      detail: getCurrentMarketSnapshot(),    // your code
      bubbles: true,
      composed: true,
    })
  )
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
widgetRoot.addEventListener('widget:instream_betting:bet_placed', async (event) => {
  const { id, marketId, oddId, stake, oddValue } = event.detail

  try {
    await placeBetOnBackend({ marketId, oddId, stake, oddValue })

    widgetRoot.dispatchEvent(
      new CustomEvent('widget:instream_betting:bet_updated', {
        detail: { betId: id, marketId, oddId, status: 'PLACED', oddValue },
        bubbles: true,
        composed: true,
      })
    )
  } catch (error) {
    widgetRoot.dispatchEvent(
      new CustomEvent('widget:instream_betting:bet_updated', {
        detail: {
          betId: id,
          marketId,
          oddId,
          status: 'FAILED',
          statusReason: error.message,
          oddValue,
        },
        bubbles: true,
        composed: true,
      })
    )
  }
})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function notifySettlement(bet) {
  widgetRoot.dispatchEvent(
    new CustomEvent('widget:instream_betting:bet_updated', {
      detail: {
        betId: bet.id,
        marketId: bet.marketId,
        oddId: bet.oddId,
        status: bet.won ? 'SETTLED_WIN' : 'SETTLED_LOSS',
        oddValue: bet.oddValue,
        stake: `${bet.stake} USD`,
        refund: `${bet.payout} USD`,
        sportEventName: bet.eventName,
        marketName: bet.marketName,
        oddName: bet.oddName,
      },
      bubbles: true,
      composed: true,
    })
  )
}

Localization and formatting are entirely on the client side

The widget never translates, formats, or rounds any client-supplied string. Every user-facing value — title, prefix, headers, label, currency, userBalance, oddName, marketName, statusReason, sportEventName, market names in the Layout examples below, etc. — is rendered exactly as provided. Localize text, format odds (decimal vs US, comma vs dot, …), and format monetary values on your side before dispatching config / data / bet_updated.

Event payloads

Defines quick-pick stake buttons, currency, and click-confirmation timing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type EventConfig = {
  suggestedBets: SuggestedBet[]              // quick-pick buttons
  betClickConfirmationCountdownMs: number    // ms for user to confirm (default: 2000)
  currency?: string                          // displayed as-is (default: not shown)
  isAuth: boolean                            // false → bet placement is blocked,
                                             // the widget shows a restriction with a warning
}

type SuggestedBet = {
  value: string
  type: 'PRESET'                             // a fixed stake button defined by the client
}

Full snapshot of competitors and markets. The widget reuses stable IDs for minimal re-renders.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type EventData = {
  competitors: Competitor[]
  marketGroups: MarketGroup[]
  userBalance?: string                       // displayed as-is
}

type Competitor = {
  id: string
  name: string
  logo: string                               // url to the logo image
  scores?: { type: 'TOTAL' | 'CURRENT'; value: string }[]
}

type MarketGroup = {
  id: string                                 // stable key for enter/exit animation
  title: string                              // rendered as-is — localize on client side
  headers?: string[]                         // optional column headers row
  oddDirection?: 'ROW' | 'COLUMN'            // label/value layout (default: COLUMN)
  markets: MarketRow[]
}

type MarketRow = {
  id: string                                 // sent back as `EventBetPlaced.marketId`
  prefix?: string                            // optional leading cell (e.g. "Round 1")
  odds: Odd[]
}

type Odd = {
  id: string                                 // sent back as `EventBetPlaced.oddId`
  label?: string                             // small muted label ('1' | 'x' | '+1.5' | …);
                                             // when omitted, only `value` is rendered
  value: string                              // coefficient, displayed as-is
  active: boolean                            // false → cell is locked and non-clickable
}

Emitted by the widget when the user picks an outcome and confirms the stake.

1
2
3
4
5
6
7
type EventBetPlaced = {
  id: string                                 // `${marketId}_${oddId}` — dedupe key
  marketId: string                           // matches MarketRow.id from your `data` event
  oddId: string                              // matches Odd.id
  stake: string                              // user's selected stake
  oddValue: string                           // coefficient shown at click time
}

Sent in response to bet_placed (PLACED / FAILED), or asynchronously later for settlement (SETTLED_WIN / SETTLED_LOSS).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type EventBetUpdated = {
  betId: string                              // echo `EventBetPlaced.id`
  oddId: string
  oddName?: string                           // shown in toast
  marketId: string
  marketName?: string                        // shown in toast
  status: 'FAILED' | 'PLACED' | 'SETTLED_WIN' | 'SETTLED_LOSS'
  statusReason?: string                      // human-readable reason on FAILED
  oddValue: string                           // coefficient at the moment of placement
  stake?: string                             // amount placed (e.g. '30.00 USD')
  refund?: string                            // payout/refund (e.g. '45.00 USD')
  sportEventName?: string                    // shown in toast
}

Emitted when validation fails on incoming data or config. There is no fallback — the affected section renders nothing until the client sends a valid payload.

1
type EventError = { message: string }

Layout examples

The widget composes the visual layout from headers, oddDirection, market.prefix, and odd.label. The recipes below cover the most common market types:

In-Stream Betting — 1X2

oddDirection: 'COLUMN' (default), no headers. The odd.label carries '1', 'x', '2' and renders above the coefficient.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  id: '1x2',
  title: '1X2',
  oddDirection: 'COLUMN',
  markets: [
    {
      id: '20',
      odds: [
        { id: '1', label: '1', value: '2.51', active: true },
        { id: '2', label: 'X', value: '3.51', active: true },
        { id: '3', label: '2', value: '2.51', active: true },
      ],
    },
  ],
}

In-Stream Betting — Total

headers: ['Over', 'Under'], oddDirection: 'ROW'. The odd.label is the threshold ('1.5', '2.5', …) and renders beside the coefficient.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
  id: 'total-goals',
  title: 'TOTAL GOALS',
  headers: ['Over', 'Under'],
  oddDirection: 'ROW',
  markets: [
    {
      id: '398t1_5',
      odds: [
        { id: '1', label: '1.5', value: '1.26', active: true },
        { id: '2', label: '1.5', value: '3.46', active: true },
      ],
    },
    {
      id: '398t2_5',
      odds: [
        { id: '1', label: '2.5', value: '1.86', active: true },
        { id: '2', label: '2.5', value: '1.85', active: true },
      ],
    },
    {
      id: '398t3_5',
      odds: [
        { id: '1', label: '3.5', value: '3.20', active: true },
        { id: '2', label: '3.5', value: '1.30', active: true },
      ],
    },
    {
      id: '398t4_5',
      odds: [
        { id: '1', label: '4.5', value: '6.01', active: true },
        { id: '2', label: '4.5', value: '1.10', active: true },
      ],
    },
  ],
}

In-Stream Betting — Handicap

headers: [teamA, teamB], oddDirection: 'ROW'. The odd.label carries the handicap value ('+1.5', '-1.0', …).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
  id: 'handicap',
  title: 'HANDICAP',
  headers: ['Bologna (Gruby)', 'Arsenal (Radikal)'],
  oddDirection: 'ROW',
  markets: [
    {
      id: '240h-1_5',
      odds: [
        { id: '1', label: '-1.5', value: '4.80', active: true },
        { id: '2', label: '+1.5', value: '1.15', active: true },
      ],
    },
    {
      id: '240h-1_0',
      odds: [
        { id: '1', label: '-1.0', value: '4.01', active: true },
        { id: '2', label: '+1.0', value: '1.20', active: true },
      ],
    },
    {
      id: '240h1_0',
      odds: [
        { id: '1', label: '+1.0', value: '1.20', active: true },
        { id: '2', label: '-1.0', value: '4.01', active: true },
      ],
    },
    {
      id: '240h1_5',
      odds: [
        { id: '1', label: '+1.5', value: '1.15', active: true },
        { id: '2', label: '-1.5', value: '4.80', active: true },
      ],
    },
  ],
}

In-Stream Betting — Next goal

Use this layout for next goal markets — who scores the 1st / 2nd / 3rd / … goal. headers: ['Goal', '1', 'NO', '2'], oddDirection: 'COLUMN'. Each row uses market.prefix to label the goal number on the left.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
  id: 'next-goal',
  title: 'NEXT GOAL',
  headers: ['Goal', '1', 'NO', '2'],
  oddDirection: 'COLUMN',
  markets: [
    {
      id: '356g1',
      prefix: '1',
      odds: [
        { id: '1', value: '1.97', active: true },
        { id: '2', value: '8.72', active: true },
        { id: '3', value: '1.97', active: true },
      ],
    },
    {
      id: '356g2',
      prefix: '2',
      odds: [
        { id: '1', value: '2.40', active: true },
        { id: '2', value: '3.46', active: true },
        { id: '3', value: '2.40', active: true },
      ],
    },
    {
      id: '356g3',
      prefix: '3',
      odds: [
        { id: '1', value: '3.43', active: true },
        { id: '2', value: '1.85', active: true },
        { id: '3', value: '3.43', active: true },
      ],
    },
    {
      id: '356g4',
      prefix: '4',
      odds: [
        { id: '1', value: '5.55', active: true },
        { id: '2', value: '1.30', active: true },
        { id: '3', value: '5.55', active: true },
      ],
    },
  ],
}

The widget renders every client-supplied string as-is, so the same layout-recipes work for any locale and any odds format just by translating and reformatting the payload. Below are the Total and Next goal examples with German strings and US moneyline odds (+142 / -277-style) instead of decimal (1.42).

In-Stream Betting — Total (DE)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  id: 'gesamtzahl-tore',
  title: 'Gesamtzahl tore',
  headers: ['Über', 'Unter'],
  oddDirection: 'ROW',
  markets: [
    {
      id: '398t6_5',
      odds: [
        { id: '1', label: '6.5', value: '-189', active: true },
        { id: '2', label: '6.5', value: '+139', active: true },
      ],
    },
    {
      id: '398t5_5',
      odds: [
        { id: '1', label: '5.5', value: '-120', active: true },
        { id: '2', label: '5.5', value: '+119', active: true },
      ],
    },
  ],
}

In-Stream Betting — Next goal (DE)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  id: 'nächstes-tor',
  title: 'Nächstes tor',
  headers: ['Goal', '1', 'Kein tor', '2'],
  oddDirection: 'column',
  markets: [
    {
      id: '356g1',
      prefix: '1',
      odds: [
        { id: '1', value: '-189', active: true },
        { id: '2', value: '+139', active: true },
        { id: '3', value: '+139', active: true },
      ],
    },
    {
      id: '356g2',
      prefix: '2',
      odds: [
        { id: '1', value: '-120', active: true },
        { id: '2', value: '-120', active: true },
        { id: '3', value: '+119', active: true },
      ],
    },
    {
      id: '356g3',
      prefix: '3',
      odds: [
        { id: '1', value: '+153', active: true },
        { id: '2', value: '-256', active: true },
        { id: '3', value: '+139', active: true },
      ],
    },
  ],
}

Only the strings (title, headers, label, decimal separators, value format) change between locales and odds-format conventions — oddDirection, market.prefix, IDs, and the overall structure stay identical. Use whatever odds format your audience expects — decimal (1.42), US moneyline (+142 / -277), fractional (5/4), Hong Kong (0.42), etc. The widget never reformats value; do it on the client side before dispatching data.


Quick reference:

Section headers oddDirection market.prefix odd.label
1X2 COLUMN '1', 'x', '2'
TOTAL ['Over','Under'] ROW '6.5', '5.5', …
HANDICAP [teamA, teamB] ROW '+1.5', '-1.0', …
NEXT GOAL ['Goal','1','NO','2'] COLUMN '1', '2', …

Warning

Section names above are examples, not an enum. Pass any localized string to title / headers / label — the widget renders them as-is.

Rules of thumb:

  • Use oddDirection: 'ROW' when the odd label sits beside the coefficient (handicap, totals).
  • Use oddDirection: 'COLUMN' (default) when it sits above (1/x/2 labels).
  • Use headers for a fixed label row above the markets.
  • Use market.prefix when each row has a left-most label cell (round number, map index). It renders before the odds and counts as one grid column.
  • Omit odd.label entirely if the coefficient speaks for itself.
Full EventData payload covering all layouts
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
{
  userBalance: '1450.50',
  competitors: [
    {
      id: 'team-1',
      name: 'Team 1',
      logo: 'https://.../team-1.png',
      scores: [
        { type: 'TOTAL',   value: '1' },
        { type: 'CURRENT', value: '2' },
      ],
    },
    {
      id: 'team-2',
      name: 'Team 2',
      logo: 'https://.../team-2.png',
      scores: [
        { type: 'TOTAL',   value: '3' },
        { type: 'CURRENT', value: '3' },
      ],
    },
  ],
  marketGroups: [
    // 1X2: 1 / Draw / 2, label above coefficient
    {
      id: '1x2',
      title: '1X2',
      oddDirection: 'COLUMN',
      markets: [
        {
          id: '103',
          odds: [
            { id: '1', label: '1', value: '3.10', active: true },
            { id: '2', label: 'Draw', value: '2.40', active: true },
            { id: '3', label: '2', value: '1.85', active: false },
          ],
        },
      ],
    },

    // TOTAL: header row, label beside coefficient
    {
      id: 'total',
      title: 'TOTAL',
      headers: ['Over', 'Under'],
      oddDirection: 'ROW',
      markets: [
        {
          id: '102t6_5',
          odds: [
            { id: '1', label: '6.5', value: '1.53', active: true },
            { id: '2', label: '6.5', value: '2.39', active: true },
          ],
        },
      ],
    },

    // HANDICAP: header row with team names
    {
      id: 'handicap',
      title: 'HANDICAP',
      headers: ['Team 1', 'Team 2'],
      oddDirection: 'ROW',
      markets: [
        {
          id: '101h-3_0',
          odds: [
            { id: '1', label: '+1.5', value: '1.53', active: true },
            { id: '2', label: '-1.0', value: '2.39', active: false },
          ],
        },
      ],
    },

    // NEXT GOAL: leading prefix cell per row
    {
      id: 'next-goal',
      title: 'NEXT GOAL',
      headers: ['Goal', '1', 'NO', '2'],
      oddDirection: 'COLUMN',
      markets: [
        {
          id: '100g1',
          prefix: '1',
          odds: [
            { id: '1', value: '1.53', active: true },
            { id: '2', value: '2.39', active: true },
            { id: '3', value: '2.39', active: true },
          ],
        },
        {
          id: '100g2',
          prefix: '2',
          odds: [
            { id: '1', value: '1.53', active: true },
            { id: '2', value: '2.39', active: true },
            { id: '3', value: '2.39', active: true },
          ],
        },

      ],
    },
  ],
}

Design principles

  • Render-only contract. The widget never interprets market types or infers business meaning from field values — it renders exactly what the client describes.
  • Ready-to-render strings. title, label, value are displayed as-is. Translate and format on the client side.
  • Stable keys at every level. Reuse the same group.id / market.id / odd.id across updates. Changing only value or active triggers a single-cell re-render.
  • The client owns all betting state. The widget never assumes a bet succeeded — it waits for bet_updated. The widget never tracks balance — push userBalance on every data update.
  • Always set bubbles: true, composed: true on every dispatched event, otherwise it will not cross the Shadow DOM.

Debug mode

Add the debugMode attribute to log every incoming event to the browser console:

1
<databet-widget debugMode="true" type="multi" token="<JWT_TOKEN>"></databet-widget>
1
2
3
[widget:instream_betting:config]       { suggestedBets: [...], ... }
[widget:instream_betting:data]         { competitors: [...], marketGroups: [...] }
[widget:instream_betting:bet_updated]  { betId: '...', status: 'PLACED', ... }

This is the fastest way to verify your payload reaches the widget correctly.