---
title: "Swipe Bet"
canonical_url: "https://apidocs.sportradar.com/resources/widgets/docs/bet-recommendation/swipe-bet"
markdown_url: "https://apidocs.sportradar.com/resources/widgets/docs/bet-recommendation/swipe-bet.md"
last_updated: "2026-06-18T07:06:52Z"
---

# Swipe Bet

**Swipe Bet** widget is a front-end widget that provides a **personalized betting experience** and a fun way for punters to create multi bets. The main feature is the user interface that allows punters to quickly add selections to the bet slip by swiping event cards to the right and removing events they are not interested in by swiping left.

- Punters past activity
- General betting trends
- Overall betting volume
- Seasonal variations
- New discovery and engagement patterns

> **Info**
>
> **Personalization requirement**
> To enable personalized recommendations (the `recommended` type), you must provide a unique `user` identifier. When no user ID is provided, the widget will fall back to `popular` recommendations.

## Key Capabilities

### Gamified Interface

Fun, "swipe-to-bet" interaction style that increases user session duration and engagement.

### Smart Recommendations

Tailors a list of high-relevance betting opportunities based on individual punter history.

### Discovery Flow

Surface interesting matches and markets that users might otherwise overlook in traditional menus.

### Live & Pre-match

Supports real-time swiping for live events and discovery for upcoming pre-match fixtures.

### Lean Integration

Minimal technical overhead required to embed this innovative betting feature.

### Design Liberty

Complete control over visual elements and interaction cues to fit your platform's USP.

## UI and Placement Options

### Intuitive Gestures

Tinder-like navigation: swipe right to add to bet slip, swipe left to dismiss and see the next idea.

### Modal Experience

Opens in an elegant overlay, allowing users to browse bets without losing their place on your site.

### Multi-Bet Builder

Seamlessly combine selections from multiple swiped cards into a high-value accumulator.

### Universal Trigger

Place the launch button anywhere—from the home page to specific tournament or profile sections.

## Settings and Customization

- Mode: user select, single, or multi bet
- Recommendation type: recommended (default), popular, or trending
- Time span: live, upcoming, or both (default)
- Sport filter: single sport or multiple sports
- Widget name
- Mobile/desktop mode

## Styling Options

- Elements (container, cards, buttons): colors, rounded corners, icons
- Text: font styles and colors

## Requirements

- Access to the client API

- Required endpoints: User ID, Odds, Bet slip content, combined odds for multi bets

- Requires an adapter to be registered via `SIR('registerAdapter', '{ADAPTER_NAME}')`. See the adapter overview: <https://apidocs.sportradar.online/resources/widgets/docs/adapter/Overview>

- **widget-name**: `betRecommendation.swipeBet`

**Environment Requirements**

## Technical Requirements:

- JavaScript enabled
- XMLHttpRequest support for data fetching
- CSS3 support for styling and animations

**Supported Sports**

## Main Configurable Features

See the [Swipe Bet widget demo](https://widgets.sir.sportradar.com/bet-recommendation#widget:\(name:betRecommendation.swipeBet\)).
Illustrations of main interaction modes and configurations with relevant property values below.

**Mobile View**

![Mobile View](https://apidocs.sportradar.com/resources/widgets/static/img/swipebet/mobile.png)

Full-screen mobile interface with swipe gestures for rapid bet selection.

**Desktop View**

![Desktop View](https://apidocs.sportradar.com/resources/widgets/static/img/swipebet/default.png)

Modal dialog on desktop with click-based interaction and bet slip summary.

See [Swipe Bet widget demo](https://widgets.sir.sportradar.com/bet-recommendation#widget:\(name:betRecommendation.swipeBet\))

## API Reference

### Basic Properties

| Property                  | Type             | Default       | Description                                                         |
| ------------------------- | ---------------- | ------------- | ------------------------------------------------------------------- |
| `user`                    | `string\|number` | `0`           | User identifier for personalized recommendations.                   |
| `buttonTitle`             | `string\|false`  | `"Swipe Bet"` | Text displayed on button trigger.                                   |
| `productTitle`            | `string`         | "SWIPE BET"   | Title displayed in widget header.                                   |
| `headerIcon`              | `string`         | `undefined`   | Custom icon URL for widget header.                                  |
| `buttonIcon`              | `string\|false`  | `undefined`   | Custom icon URL for button trigger.                                 |
| `maxSelectionsMultiMode`  | `number`         | `12`          | Maximum selections in multi bet mode (1-20).                        |
| `maxSelectionsSingleMode` | `number`         | `12`          | Maximum selections in single bet mode (1-20).                       |
| `isMobile`                | `boolean`        | `false`       | Rendering mode (mobile or desktop).                                 |
| `betSlipMode`             | `array<string>`  | `["multi"]`   | Available betting modes: `"single"`, `"multi"`, or both.            |
| `outcomeNamePosition`     | `string`         | `"start"`     | Outcome label position: `"start"`, `"end"`, `"top"`, or `"bottom"`. |
| `customLoaderImg`         | `string`         | `undefined`   | URL for custom loading image.                                       |
| `customLoaderMs`          | `number`         | `250`         | Duration to display custom loader (ms).                             |
| `onItemClick`             | `function`       | `undefined`   | Callback for user interactions.                                     |

**Extended Properties**

| Property          | Type      | Default      | Description                                                                                                                                                                                                                                                    |
| ----------------- | --------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filters`         | `object`  | **Required** | Configuration object for sport and time filters. See below for detailed structure.                                                                                                                                                                             |
| `disableLazyLoad` | `boolean` | `false`      | Controls lazy loading behavior. When `true`, widget content loads immediately on page load instead of deferring until user interaction. Set `true` for standalone mode or when immediate display required. Default `false` optimizes initial page performance. |
| `sportsMapping`   | `object`  | `undefined`  | Maps client's sport identifiers to Sportradar sport IDs. Object with keys as client sport IDs (string/number) and values as Sportradar sport IDs. Example: `{101: 1, 102: 2}` maps client IDs 101/102 to soccer/basketball.                                    |
| `customJerseys`   | `object`  | `undefined`  | Custom team jersey/logo images for match cards. Nested object structure: `{sportId: {tournamentId: {teams: {teamId: {home: url, away: url}}, default: {home: url, away: url}}}}`. Allows branded team visuals per sport and tournament.                        |
| `debug`           | `boolean` | `false`      | Enables debug mode with console logging for development. When `true`, logs recommendation fetch, swipe actions, bet selections, errors. Use for troubleshooting integration issues.                                                                            |

### Filters Object

The `filters` object controls available sports and time ranges for bet recommendations.

| Property          | Type                    | Default      | Description                                                                                                                                                                                                                                                                           |
| ----------------- | ----------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sport`           | `object`                | **Required** | Sport filter configuration.                                                                                                                                                                                                                                                           |
| `sport.available` | `array<string\|number>` | `[]`         | **Required.** Array of Sportradar sport IDs to include in recommendations. Empty array shows all available sports. Example: `[1, 2, 5]` for soccer, basketball, tennis. See [Sports Reference](https://apidocs.sportradar.com/resources/widgets/docs/tutorials/references/sports.md). |
| `time`            | `object`                | **Required** | Time filter configuration.                                                                                                                                                                                                                                                            |
| `time.available`  | `string`                | `undefined`  | Time/status filter option. Values: `"live"` (live events only), `"not_started"` (upcoming events only). When omitted, shows both live and upcoming events.                                                                                                                            |
| `time.range`      | `number`                | `undefined`  | Time range in hours for upcoming events. Range: 0-72 hours. Example: `24` shows events starting within next 24 hours. Only applicable when `time.available` is `"not_started"` or omitted.                                                                                            |

#### Filters Example

```javascript
{
  sport: {
    available: [1, 2, 5, 23]  // Soccer, Basketball, Tennis, Volleyball
  },
  time: {
    available: "not_started",  // Upcoming events only
    range: 24                   // Next 24 hours
  }
}
```

## Theming

Theming customization allows to tailor the appearance of Bet Recommendation widgets to meet specific needs and preferences. In the context of the Bet Recommendation widget, customization refers to the ability to modify the default styling of the widget by applying custom CSS properties to the various HTML elements that make up the widget.

Widget comes with pre-existing styling but can be customized by applying custom CSS properties to its different HTML elements. The widget's custom class selectors and supported CSS properties are listed below.

All custom classes must be nested within the `.sr-bb.sr-swipebet` selector class. This ensures that the custom styles only apply to that widget and not to other elements on the page.

**Custom class selectors**

| CSS class                                 | Supported CSS properties                                                       |
| ----------------------------------------- | ------------------------------------------------------------------------------ |
| `srct-br-container`                       | `background-color`, `font-family`                                              |
| `srct-br-header`                          | `background-color`, `color`, `border-radius`                                   |
| `srct-br-header__title`                   | `font-size`, `color`                                                           |
| `srct-br-header__divider`                 | `border-color`                                                                 |
| `srct-br-content__title`                  | `font-weight`                                                                  |
| `srct-br-footer`                          | `font-size`, `color`, `background-color`, `border-radius`                      |
| `srct-br-loading`                         | `background-color`, `border-radius`                                            |
| `srct-br-navigation`                      | `background-color`, `color`                                                    |
| `srct-br-card`                            | `background-color`, `color`, `border-radius`                                   |
| `srct-br-card__divider`                   | `border-color`                                                                 |
| `srct-br-outcome`                         | `background-color`, `color`, `border-radius`, `border`                         |
| `srct-br-outcome--selected`               | `background-color`, `color`, `border-radius`, `border`                         |
| `srct-br-outcome--disabled`               | `background-color`, `color`, `border-radius`                                   |
| `srct-br-outcome__name`                   | `font-size`, `color`, `opacity`                                                |
| `srct-br-outcome__value`                  | `font-size`, `color`, `font-weight`                                            |
| `srct-br-eventinfo`                       | `font-size`, `color`                                                           |
| `srct-br-eventinfo__icon`                 | `color`                                                                        |
| `srct-br-eventinfo__time`                 | `font-size`, `color`                                                           |
| `srct-br-eventinfo__status`               | `font-size`, `color`                                                           |
| `srct-br-eventinfo__name`                 | `font-size`, `color`                                                           |
| `srct-br-scoreboard`                      | `font-size`, `color`                                                           |
| `srct-br-scoreboard__teams`               | `font-size`, `color`                                                           |
| `srct-br-scoreboard__scores`              | `font-size`, `color`                                                           |
| `srct-br-customjersey`                    | `mask-image`                                                                   |
| `srct-br-widgetbutton`                    | `font-size`, `color`, `border-radius`, `padding`, `background-color`           |
| `srct-br-widgetbutton__icon`              | `height`, `width`                                                              |
| `srct-br-closebutton`                     | `font-size`, `color`, `border-radius`, `background-color`                      |
| `srct-br-thumb`                           | `background-color`, `border-radius`                                            |
| `srct-br-thumb--like`                     | `background-color`, `border-radius`                                            |
| `srct-br-thumb--dislike`                  | `background-color`, `border-radius`                                            |
| `srct-br-thumb--active`                   | `background-color`, `border-radius`                                            |
| `srct-br-thumb--disabled`                 | `background-color`, `border-radius`                                            |
| `srct-br-thumb__icon`                     | `color`                                                                        |
| `srct-br-thumb__icon--active`             | `color`                                                                        |
| `srct-br-thumb__icon--disabled`           | `color`                                                                        |
| `srct-br-thumb__icon--like`               | `color`                                                                        |
| `srct-br-thumb__icon--dislike`            | `color`                                                                        |
| `srct-br-bet-counter`                     | `color`, `background-color`                                                    |
| `srct-br-marketselectionbutton`           | `font-size`, `color`, `border-radius`, `border-color`, `background-color`      |
| `srct-br-marketselectionbutton--disabled` | `font-size`, `color`, `border-radius`, `border-color`, `background-color`      |
| `srct-br-market__name`                    | `font-size`, `color`                                                           |
| `srct-br-market__change-text`             | `font-size`, `color`                                                           |
| `srct-br-market__change-icon`             | `width`, `height`                                                              |
| `srct-br-betslipbutton`                   | `font-size`, `color`, `border`, `border-radius`, `background-color`, `opacity` |

## Integration

> **Warning**
>
> **Mandatory Configuration**
> The `filters` property is required for this widget to function correctly. It defines the recommendation logic and basic data constraints.

![Bet Recommendation technical guide](https://apidocs.sportradar.com/resources/widgets/static/img/betRecommendationTechnicalGuide.jpg)

Demo: [Open demo](https://widgets.sir.sportradar.com/bet-recommendation#widget:\(name:betRecommendation.swipeBet\))

### Widget Setup

**Button Mode**

Basic swipe bet widget - button mode

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-1", "betRecommendation.swipeBet", {
  productTitle: "SWIPE BET",
  isMobile: false,
  filters: {
    sport: {
      available: [1, 2, 5],
    },
    time: {
      available: "not_started",
      range: 24,
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="SWIPE BET"
  data-is-mobile="false"
  data-filters='{"sport": {"available": [1, 2, 5]}, "time": {"range": 24}}'
></div>
```

**Mobile Standalone**

Mobile standalone full-screen experience

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-2", "betRecommendation.swipeBet", {
  productTitle: "QUICK BET",
  isMobile: true,
  isStandalone: true,
  disableLazyLoad: true,
  filters: {
    sport: {
      available: [1, 2],
    },
    time: {
      available: "not_started",
      range: 12,
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="QUICK BET"
  data-is-mobile="true"
  data-is-standalone="true"
  data-disable-lazy-load="true"
  data-filters='{"sport": {"available": [1, 2]}, "time": {"available": "not_started", "range": 12}}'
></div>
```

**Personalized**

> **Info**
>
> **Personalization requirement**
> To enable personalized recommendations (the `recommended` type), you must provide a unique `user` identifier. When no user ID is provided, the widget will fall back to `popular` recommendations.

Personalized recommendations with user ID

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-3", "betRecommendation.swipeBet", {
  productTitle: "YOUR BETS",
  isMobile: false,
  user: "user123",
  filters: {
    sport: {
      available: [1, 2, 5, 23],
    },
    time: {},
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="YOUR BETS"
  data-is-mobile="false"
  data-user="user123"
  data-filters='{"sport": {"available": [1, 2, 5, 23]}, "time": {}}'
></div>
```

**Single Mode**

Single bet mode only

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-4", "betRecommendation.swipeBet", {
  productTitle: "SINGLE BETS",
  isMobile: false,
  betSlipMode: ["single"],
  maxSelectionsSingleMode: 10,
  filters: {
    sport: {
      available: [1],
    },
    time: {
      available: "live",
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="SINGLE BETS"
  data-is-mobile="false"
  data-bet-slip-mode='["single"]'
  data-max-selections-single-mode="10"
  data-filters='{"sport": {"available": [1]}, "time": {"available": "live"}}'
></div>
```

**Multi/Dual Mode**

Multi bet (accumulator) mode with dual modes on desktop

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-5", "betRecommendation.swipeBet", {
  productTitle: "BET BUILDER",
  isMobile: false,
  betSlipMode: ["multi", "single"],
  maxSelectionsMultiMode: 8,
  maxSelectionsSingleMode: 10,
  filters: {
    sport: {
      available: [1, 2],
    },
    time: {
      range: 48,
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="BET BUILDER"
  data-is-mobile="false"
  data-bet-slip-mode='["multi", "single"]'
  data-max-selections-multi-mode="8"
  data-max-selections-single-mode="10"
  data-filters='{"sport": {"available": [1, 2]}, "time": {"range": 48}}'
></div>
```

**Custom Branding**

Custom branding with icons and button text

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-6", "betRecommendation.swipeBet", {
  productTitle: "QUICK BET",
  isMobile: false,
  buttonTitle: "Try Quick Bet",
  buttonIcon: "https://example.com/icons/quick-bet.svg",
  headerIcon: "https://example.com/icons/header-logo.svg",
  filters: {
    sport: {
      available: [1, 2, 5],
    },
    time: {
      range: 24,
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="QUICK BET"
  data-is-mobile="false"
  data-button-title="Try Quick Bet"
  data-button-icon="https://example.com/icons/quick-bet.svg"
  data-header-icon="https://example.com/icons/header-logo.svg"
  data-filters='{"sport": {"available": [1, 2, 5]}, "time": {"range": 24}}'
></div>
```

**Custom Loader**

Custom loader branding

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-7", "betRecommendation.swipeBet", {
  productTitle: "SWIPE BET",
  isMobile: true,
  isStandalone: true,
  customLoaderImg: "https://example.com/loaders/spinner.gif",
  customLoaderMs: 500,
  filters: {
    sport: {
      available: [1, 2],
    },
    time: {},
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="SWIPE BET"
  data-is-mobile="true"
  data-is-standalone="true"
  data-custom-loader-img="https://example.com/loaders/spinner.gif"
  data-custom-loader-ms="500"
  data-filters='{"sport": {"available": [1, 2]}, "time": {}}'
></div>
```

**Analytics**

Analytics integration with click handlers

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-8", "betRecommendation.swipeBet", {
  productTitle: "SWIPE BET",
  isMobile: false,
  onItemClick: function (target, data) {
    console.log("Interaction:", target, data);

    // External event click - navigate to event page
    if (target === "externalEvent") {
      gtag("event", "swipe_bet_event_click", {
        event_id: data.externalEvent.id,
        sport: data.externalEvent.sportId,
      });

      // Example navigation
      // window.location.href = `/matches/${data.externalEvent.id}`;
    }

    // Single outcome selection
    else if (target === "externalOutcome") {
      gtag("event", "swipe_bet_selection", {
        event_id: data.externalEvent?.id,
        outcome_id: data.externalOutcome.id,
        odds: data.externalOutcome.odds,
      });

      // Add to bet slip example
      // addToBetSlip({ eventId: data.externalEvent.id, outcomeId: data.externalOutcome.id, odds: data.externalOutcome.odds });
    }

    // Multiple outcomes selected at once
    else if (target === "externalOutcomes") {
      // data is an array of outcomes with context
      console.log("Multiple outcomes selected", data);
    }

    // Bet slip mode change (value: 'multi'|'single')
    else if (target === "betSlipMode") {
      console.log("Bet slip mode changed", data); // { value: 'single' }
    }

    // Open native BetSlip integration
    else if (target === "goToBetSlip") {
      // Navigate to bet slip page or open your own bet slip UI
      window.location.href = "/betslip";
    }
  },
  filters: {
    sport: {
      available: [1, 2, 5],
    },
    time: {
      range: 24,
    },
  },
});
```

**Custom Jerseys**

Custom team jerseys per tournament

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-9", "betRecommendation.swipeBet", {
  productTitle: "SWIPE BET",
  isMobile: false,
  customJerseys: {
    1: {
      // Soccer
      "sr:tournament:17": {
        // Premier League
        teams: {
          "sr:competitor:42": {
            home: "https://example.com/jerseys/arsenal-home.png",
            away: "https://example.com/jerseys/arsenal-away.png",
          },
        },
        default: {
          home: "https://example.com/jerseys/default-home.png",
          away: "https://example.com/jerseys/default-away.png",
        },
      },
    },
  },
  filters: {
    sport: {
      available: [1],
    },
    time: {},
  },
});
```

**Sports Mapping**

Sports mapping for client sport IDs

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-10", "betRecommendation.swipeBet", {
  productTitle: "SWIPE BET",
  isMobile: false,
  sportsMapping: {
    101: 1, // Client sport 101 = Soccer
    102: 2, // Client sport 102 = Basketball
    105: 5, // Client sport 105 = Tennis
  },
  filters: {
    sport: {
      available: [101, 102, 105],
    },
    time: {
      range: 24,
    },
  },
});
```

**Live Only**

Live events only - in-play betting

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-11", "betRecommendation.swipeBet", {
  productTitle: "LIVE SWIPE BET",
  isMobile: false,
  betSlipMode: ["single"],
  filters: {
    sport: {
      available: [1, 2, 5],
    },
    time: {
      available: "live",
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="LIVE SWIPE BET"
  data-is-mobile="false"
  data-bet-slip-mode='["single"]'
  data-filters='{"sport": {"available": [1, 2, 5]}, "time": {"available": "live"}}'
></div>
```

**Outcome Position**

Desktop modal with outcome position customization

**JavaScript**

```javascript
SIR("addWidget", "#swipe-bet-12", "betRecommendation.swipeBet", {
  productTitle: "SWIPE BET",
  isMobile: false,
  outcomeNamePosition: "top",
  filters: {
    sport: {
      available: [1, 2],
    },
    time: {
      range: 24,
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.swipeBet"
  data-product-title="SWIPE BET"
  data-is-mobile="false"
  data-outcome-name-position="top"
  data-filters='{"sport": {"available": [1, 2]}, "time": {"range": 24}}'
></div>
```

### onItemClick

The `onItemClick` callback is fired whenever the user interacts with the widget. The first argument is a `target` string that identifies the interaction type; the second argument is a `data` object containing contextual information.

| `target` value          | Trigger                                                        | Key `data` properties                                         |
| :---------------------- | :------------------------------------------------------------- | :------------------------------------------------------------ |
| `"externalOutcome"`     | User clicks a single outcome button                            | `externalEvent`, `externalMarket`, `externalOutcome`          |
| `"externalOutcomes"`    | User clicks multiple outcomes at once (e.g. combo card)        | Array of `{ externalEvent, externalMarket, externalOutcome }` |
| `"externalEvent"`       | User clicks an event header/card                               | `externalEvent`                                               |
| `"externalCompetition"` | User clicks a competition/league name                          | `externalCompetition`                                         |
| `"goToBetSlip"`         | User clicks the "Go to Bet Slip" button (swipeBet only)        | —                                                             |
| `"betSlipMode"`         | Bet slip mode changes between single and multi (swipeBet only) | `value: "single" \| "multi"`                                  |

```javascript
SIR("addWidget", "#sr-widget", "betRecommendation.markets", {
  onItemClick: function (target, data) {
    if (target === "externalOutcome") {
      // Add single outcome to bet slip
      const { externalEvent, externalMarket, externalOutcome } = data;
      betSlip.add({
        eventId: externalEvent.id,
        marketId: externalMarket.id,
        outcomeId: externalOutcome.id,
      });
    } else if (target === "externalEvent") {
      // Navigate to event/match detail page
      window.location.href = `/matches/${data.externalEvent.id}`;
    } else if (target === "externalCompetition") {
      // Navigate to competition/league page
      window.location.href = `/league/${data.externalCompetition.tournament.id}`;
    }
  },
  filters: { recommendationType: { available: "popular" } },
});
```

For the full flow after a selection is emitted — trusting snapshot odds, re-pricing against your platform, validating availability, and accepting or rejecting the bet — see [Handling Selections and Odds](https://apidocs.sportradar.com/resources/widgets/docs/tutorials/handling-selections-and-odds.md).

Note: Widgets support callbacks on outcome clicks — the `onItemClick` handler receives `target === "outcome"` and a `data` object containing `externalEvent`, `externalMarket` and `externalOutcome`. Use this for custom outcome callbacks (e.g., add-to-betslip, analytics, modals).

The widget also exposes `onTrack` for event tracking analytics. See the [tracking guide](https://apidocs.sportradar.online/resources/widgets/docs/tutorials/tracking-guide) for details.

### Total Odds Sync

The `onTotalOddsChanged` adapter method lets you provide pre-calculated total odds for combo tickets displayed in the widget (used by My Combo and Swipe Bet). When a user's selection changes, the widget calls this method with the current set of outcomes and expects you to return the computed total odds.

Register it alongside your adapter:

```javascript
function onTotalOddsChanged(args, callback) {
  // Replace with your own odds calculation implementation
  const oddsResponse = getOdds(args);
  callback(oddsResponse);
}

SIR("registerAdapter", "{ADAPTER_NAME}", {
  onTotalOddsChanged: onTotalOddsChanged,
});
SIR("addWidget", "#sr-widget", "betRecommendation.myCombo");
```

**OddsRequest** — passed to `onTotalOddsChanged` as `args`:

| Property | Type             | Description                                               |
| :------- | :--------------- | :-------------------------------------------------------- |
| `odds`   | `Array<Outcome>` | Array of outcomes whose combined odds should be returned. |

**Outcome** (item in `odds` array):

| Property    | Type     | Description             |
| :---------- | :------- | :---------------------- |
| `eventId`   | `string` | Sportradar event ID.    |
| `outcomeId` | `string` | Sportradar outcome ID.  |
| `marketId`  | `string` | Sportradar market ID.   |
| `specifier` | `string` | Market specifier value. |

**OddsResponse** — returned via `callback`:

| Property | Type                      | Description                                         |
| :------- | :------------------------ | :-------------------------------------------------- |
| `odds`   | `Array<OutcomesResponse>` | Array of outcome groups with calculated total odds. |

**OutcomesResponse** (item in response `odds` array):

| Property    | Type               | Description                                                                           |
| :---------- | :----------------- | :------------------------------------------------------------------------------------ |
| `outcomes`  | `Array<Outcome>`   | The outcomes this total odds value applies to. Do not mutate these objects.           |
| `totalOdds` | `string \| number` | Calculated total odds for the combination. Use a string for display-formatted values. |

### Bet Slip Sync

To keep the widget's selected-outcome state in sync with your own bet slip (i.e. show outcomes as selected when they were added outside the widget), use `registerOnBetSlipChange` inside `registerAdapter`.

```javascript
// 1. Track your bet slip state
let changeCallback;
let betSlipState = { betslip: [], combinedOddsValue: undefined };

// 2. Notify the widget whenever the bet slip changes
function onBetSlipChanged(callback) {
  changeCallback = callback;
  changeCallback && changeCallback(betSlipState); // push current state immediately
}

// 3. Handle outcome clicks from the widget
function onItemClick(target, data) {
  if (target === "externalOutcome") {
    const newBet = {
      externalEventId: data.externalEvent.id,
      externalMarketId: data.externalMarket.id,
      externalOutcomeId: data.externalOutcome.id,
      externalSpecifierId: data.externalMarket.specifier
        ? data.externalMarket.specifier.value
        : undefined,
    };
    betSlipState = {
      betslip: [...betSlipState.betslip, newBet],
      combinedOddsValue: "14.52", // replace with your own calculation
    };
    changeCallback && changeCallback(betSlipState);
  }
}

// 4. Register adapter with bet slip sync + add the widget
SIR("registerAdapter", "{ADAPTER_NAME}", { onBetSlipChanged });
SIR("addWidget", "#sr-widget", "betRecommendation.markets", { onItemClick });
```

Until a custom adapter is developed, use the `mockData` adapter for local testing:

```javascript
SIR("registerAdapter", "mockData", { onBetSlipChanged });
```

### Adapter Setup

An **adapter** is a software component developed by the Sportradar engineering team that bridges the Bet Recommendation widgets and your platform's API. It retrieves data from your API and feeds it to the widget, ensuring seamless communication between the two systems.

SIR Widgets supports two types of adapter implementations:

1. [Self-Service Adapter](https://apidocs.sportradar.com/resources/widgets/docs/adapter/SelfServiceImplementation.md)
2. [Hosted Adapter](https://apidocs.sportradar.com/resources/widgets/docs/adapter/HostedImplementation.md)

Before adapter development begins, confirm and align your API contract with the Sportradar engineering team. Integration requires two SIR calls:

| SIR method                    | Purpose                                                               |
| :---------------------------- | :-------------------------------------------------------------------- |
| `SIR('registerAdapter', ...)` | Configure the adapter that retrieves and displays data from your API. |
| `SIR('addWidget', ...)`       | Mount the widget on the page.                                         |

```javascript
(function (a, b, c, d, e, f, g, h, i) {
  a[e] ||
    ((i = a[e] =
      function () {
        (a[e].q = a[e].q || []).push(arguments);
      }),
    (i.l = 1 * new Date()),
    (i.o = f),
    (g = b.createElement(c)),
    (h = b.getElementsByTagName(c)[0]),
    (g.async = 1),
    (g.src = d),
    g.setAttribute("n", e),
    h.parentNode.insertBefore(g, h));
})(
  window,
  document,
  "script",
  "https://widgets.sir.sportradar.com/sportradar/widgetloader",
  "SIR",
  { language: "en" },
);

SIR("registerAdapter", "{ADAPTER_NAME}");
SIR("addWidget", "#sr-widget", "betRecommendation.markets");
```

#### Adapter Endpoint Requirements

##### Mandatory Endpoints

- `eventMarkets` or (`market` + `availableMarketsForEvent`)
- `event`

##### Optional Endpoints

- `filterMarkets` - narrows markets selection for widget specific use
- `betSlipSelection` - visual representation of markets already in betslip
- `cashBackSelections` - adds visual representation of cash back markets

#### Adapter Implementation Template

This is an example of an adapter implementing all endpoints. It is intended to be a copy/paste template, where only data fetching and transformation need to be implemented. When implementing an adapter, implement only the endpoints which are required by the widget being integrated, and discard the rest. For each endpoint, only the `getData${ENDPOINT_NAME}()` and `transormData${ENDPOINT_NAME}()` functions need to be implemented.

***Expand Adapter Template Code***

```html
<script type="text/javascript">
// Widget loader script from Step 3 
ndatory 
// -------- Data + Transform functions --------

async function getDataMarket(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataMarket(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    market: {
      id: data.marketId,
      name: data.marketName,
      outcomes: data.outcomes.map((outcome) =>  { id: outcome.id, name: outcome.name, odds: outcome.odds } )
    },
    event: { id: data.eventId, type: data.eventType }
  };
  */

  return {
    market: {
      id: "sr:market:1",
      name: "Match Winner",
      outcomes: [
        { id: "1", name: "Home", odds: 2.5 },
        { id: "X", name: "Draw", odds: 3.0 },
        { id: "2", name: "Away", odds: 3.2 }
      ]
    },
    event: "sr:match:12345"
  };
}

async function getDataAvailableMarketsForEvent(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataAvailableMarketsForEvent(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    selection: data.map((selection) => { type: selection.type, event: selection.event, market: selection.market})
  }; 
  */

  return {
    selection: [
      {
        type: "uf",
        event: "61513908",
        market: "1",
      },
    ],
  };
}

async function getDataEventMarkets(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataEventMarkets(data, args) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  let markets = []

  function mapMarkets(market){
    return {
      id: market.id,
      status: market.status,
      name: market.name,
      outcomes: market.outcomes.map((outcome) => {id: outcome.id, name: outcome.name , odds: { type: outcome.odds.type, value: outcome.odds.value}, status: outcome.status})
    };
  }

  return data.forEach(mapMarkets);
  */

  return {
    event: args.selection.event,
    markets: [
      {
        id: "1",
        status: "active",
        name: "1x2",
        outcomes: [
          {
            id: "1",
            name: "Tenhaisen",
            odds: { type: "decimal", value: "1.88" },
            status: "active",
          },
          {
            id: "2",
            name: "draw",
            odds: { type: "decimal", value: "3.85" },
            status: "active",
          },
          {
            id: "3",
            name: "Hoftenstain",
            odds: { type: "decimal", value: "3.7" },
            status: "active",
          },
        ],
      },
    ],
  };
}

async function getDataEvent(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataEvent(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    event: {
      id: data.event,
      date: {
        displayValue: data.displayTime,
        startTime: data.dateTime,
      },
      sport: {
        id: data.sport.id,
        name: data.sport.name,
      },
      category: {
        id: data.category.id,
        name: data.category.country,
      },
      tournament: {
        id: data.tournament.id,
        name: data.tournament.name,
      },
      teams: data.teams.map((team) => {id: team.id, name: team.name}),
      isLive: data.isLive,
    },
  }; 
  
  */
  return {
    event: {
      id: args.selection.event,
      date: {
        displayValue: "14/01/26, 19:30",
        startTime: "2026-01-14T19:30:00.000Z",
      },
      sport: {
        id: "1",
        name: "Soccer",
      },
      category: {
        id: "30",
        name: "Germany",
      },
      tournament: {
        id: "42",
        name: "Liga Supreme",
      },
      teams: [
        { id: "1270229", name: "Tenhaisen" },
        { id: "31531", name: "Hoftenstain" },
      ],
      isLive: false,
    },
  };
}

async function getDataFilterMarkets(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataFilterMarkets(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    selection: data.selection.map((market) => {type: market.type, event: market.event, market: market.id})
  }
  */
  return {
    selection: [
      {
        type: "uf",
        event: "61513908",
        market: "1",
      },
    ],
  };
}

async function getDataBetSlipSelection(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataBetSlipSelection(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    selection: data.selection.map((market) => {event: market.event, market: market.market, outcome: market.outcome, type: market.type}),
  };
  */

  return {
    selection: [
      {
        event: "61513908",
        market: "1",
        outcome: "1",
        type: "uf",
      },
    ],
  };
}

async function getDataCashBackSelections(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataCashBackSelections(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    events: data.events.map((event) => {event: event.id, type: event.type}),
  }
  */

  return {
    events: [
      {
        event: "56418457",
        type: "uf",
      },
    ],
  };
}

async function getDataTickets(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataTickets(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    tickets: data.tickets.map((ticket) => {
      ticketId: ticket.id,
      bets: ticket.bets.map((bet) => {
        betId: bet.id,
        selections: bet.selections.map((selection) => {
          type: selection.type,
          eventId: selection.eventId,
          marketId: selection.marketId,
          outcomeId: selection.outcomeId,
          odds: { type: selection.odds.type, value: selection.odds.value },
        }),
        odds: { type: bet.odds.type, value: bet.odds.value },
        stake: bet.stake.map((stake) => {
          type: stake.type,
          currency: stake.currency,
          amount: stake.amount,
          mode: stake.mode
        }),
      }),
      version: ticket.version,
    }),
  };
  */

  return {
    tickets: [
      {
        ticketId: "ticket_123456",
        bets: [
          {
            betId: "bet_001",
            selections: [
              {
          type: "uf",
          productId: "1",
          eventId: "sr:match:12345",
          marketId: "1",
          outcomeId: "1",
          odds: { type: "decimal", value: "2.10" },
          context: {
            eventName: "Manchester United vs Liverpool",
            marketName: "1x2",
            outcomeName: "Home",
            tournament: "Premier League",
            sportName: "Soccer",
            categoryName: "England",
            isLive: false,
            eventStartTime: "1738519200000",
            sportId: "1"
          }
        }
            ],
            odds: { type: "decimal", value: "2.10" },
            stake: [{
              type: "cash",
              currency: "USD",
              amount: "10.00",
              mode: "total"
            }],
            payout: [{ type: "cash", currency: "USD", amount: "21.00" }]
          }
        ],
        version: "2.0"
      }
    ]
  };
}

async function getDataMatchEventSuggestedSelection(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataMatchEventSuggestedSelection(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    selections: data.selections.map((selection) => {
      event: selection.event,
      market: selection.market,
      outcome: selection.outcome,
      type: selection.type,
      specifiers: selection.specifiers
    })
  };
  */

  return {
    selections: [
      { event: "sr:match:12345", market: "1", outcome: "1", type: "uf" },
      { event: "sr:match:12345", market: "18", specifiers: "total=2.5", type: "uf" }
    ]
  };
}

async function getDataRecommendedSelections(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataRecommendedSelections(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    selection: data.selections.map((selection) => {
      event: selection.event,
      market: selection.market,
      outcome: selection.outcome,
      type: selection.type,
      specifiers: selection.specifiers
    })
  };
  */

  return {
    selection: [
      { event: "sr:match:12345", market: "1", outcome: "1", type: "uf" },
      { event: "sr:match:67890", market: "18", specifiers: "total=2.5", outcome: "13", type: "uf" }
    ]
  };
}

async function getDataCalculateCustomBetXML(args) {
  // Here fetch data from your data source and return it
  return {};
}

function transformDataCalculateCustomBetXML(data) {
  // Here transform your data into data structure exemplified by the object below.
  /*
  // Illustration how data transformation might work from client data to Adapter types
  return {
    payload: '<xml>data</xml>',
  }
  */
  return {
    payload: `<filtered_calculation_response generated_at="2025-04-16T13:29:08+00:00">
                <calculation odds="27.50303106727931" probability="0.027418715351118873" harmonization="false"/>
                <available_selections>
                  <event id="sr:match:12345678">
                    <markets>
                      <market id="65" specifiers="hcp=0:2" conflict="false">
                        <outcome id="1711" conflict="true"/>
                        <outcome id="1712" conflict="true"/>
                        <outcome id="1713" conflict="false"/>
                      </market>
                      ...
                    </markets>
                  </event>
                </available_selections>
              </filtered_calculation_response>`
  };
}

// -------- Adapter --------

const adapter = {
  config: {},
  endpoints: {
    market: (args, callback) => {
      getDataMarket(args)
        .then(data => transformDataMarket(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    availableMarketsForEvent: (args, callback) => {
      getDataAvailableMarketsForEvent(args)
        .then(data => transformDataAvailableMarketsForEvent(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    eventMarkets: (args, callback) => {
      getDataEventMarkets(args)
        .then(data => transformDataEventMarkets(data, args))
        .then(result => callback(undefined, result));
      return () => {};
    },

    event: (args, callback) => {
      getDataEvent(args)
        .then(data => transformDataEvent(data, args))
        .then(result => callback(undefined, result));
      return () => {};
    },

    filterMarkets: (args, callback) => {
      getDataFilterMarkets(args)
        .then(data => transformDataFilterMarkets(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    betSlipSelection: (args, callback) => {
      getDataBetSlipSelection(args)
        .then(data => transformDataBetSlipSelection(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    cashBackSelections: (args, callback) => {
      getDataCashBackSelections(args)
        .then(data => transformDataCashBackSelections(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    tickets: (args, callback) => {
      getDataTickets(args)
        .then(data => transformDataTickets(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    matchEventSuggestedSelection: (args, callback) => {
      getDataMatchEventSuggestedSelection(args)
        .then(data => transformDataMatchEventSuggestedSelection(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    recommendedSelections: (args, callback) => {
      getDataRecommendedSelections(args)
        .then(data => transformDataRecommendedSelections(data))
        .then(result => callback(undefined, result));
      return () => {};
    },

    calculateCustomBetXML: (args, callback) => {
      getDataCalculateCustomBetXML(args)
        .then(data => transformDataCalculateCustomBetXML(data))
        .then(result => callback(undefined, result));
      return () => {};
    },
  },
};
</script>
```

### Data Types

The following data types are provided by the adapter and are also available in the `onItemClick` callback payload.

**Event**

| Property                          | Type                | Required | Description                                                      |
| :-------------------------------- | :------------------ | :------- | :--------------------------------------------------------------- |
| `id`                              | `string \| number`  | Yes      | Sportradar event ID.                                             |
| `externalId`                      | `string \| number`  | —        | Client-side event ID.                                            |
| `date`                            | `string`            | Yes      | Formatted date string displayed in the widget.                   |
| `sport.id`                        | `string \| number`  | Yes      | Sport ID. Use `sportsMapping` if not using Sportradar sport IDs. |
| `sport.name`                      | `string`            | Yes      | Sport name.                                                      |
| `category.id`                     | `string \| number`  | —        | Category ID.                                                     |
| `category.name`                   | `string`            | Yes      | Category name (e.g. "England").                                  |
| `tournament.id`                   | `string \| number`  | —        | Tournament/league ID.                                            |
| `tournament.name`                 | `string`            | Yes      | Tournament name.                                                 |
| `teams`                           | `Array<{id, name}>` | Yes      | Home and away competitors.                                       |
| `isLive`                          | `boolean`           | Yes      | Whether the event is currently live.                             |
| `liveCurrentTime`                 | `string`            | Yes      | Live time display (e.g. `"2nd set"`, `"45'"`).                   |
| `result1` / `result2` / `result3` | `result`            | —        | Score columns: `{ result: [homeScore, awayScore] }`.             |

**Market**

| Property          | Type               | Required | Description                                            |
| :---------------- | :----------------- | :------- | :----------------------------------------------------- |
| `id`              | `string \| number` | Yes      | Market ID.                                             |
| `name`            | `string`           | —        | Market name (e.g. `"Match Winner"`).                   |
| `status.isActive` | `boolean`          | —        | When `false`, see [Widget Behavior](#widget-behavior). |

**Outcome**

| Property          | Type               | Required | Description                                                                        |
| :---------------- | :----------------- | :------- | :--------------------------------------------------------------------------------- |
| `id`              | `string \| number` | Yes      | Outcome ID.                                                                        |
| `name`            | `string`           | Yes      | Outcome name (e.g. `"Home"`, `"Draw"`).                                            |
| `odds`            | `string \| number` | Yes      | Odds value. Use a `number` type to enable odds-change indicators (up/down arrows). |
| `specifier.value` | `string \| number` | —        | Additional specifier (e.g. handicap value `"-2.50"`).                              |
| `status.isActive` | `boolean`          | —        | When `false`, see [Widget Behavior](#widget-behavior).                             |

## Tips

- When `betSlipMode` is configured with multiple modes (e.g. `['multi', 'single']`), only the first mode is active on mobile — tabs are not displayed in mobile mode.
