---
title: "My Combo"
canonical_url: "https://apidocs.sportradar.com/resources/widgets/docs/bet-recommendation/my-combo"
markdown_url: "https://apidocs.sportradar.com/resources/widgets/docs/bet-recommendation/my-combo.md"
last_updated: "2026-08-11T13:11:13Z"
---

# My Combo

**My Combo** widget is a front-end widget that provides a **personalized betting experience** and a simplified way to discover and place multi bets. The main feature of the widget is the user interface that allows punters to quickly add multi bet combinations to the bet slip, find new ones or create their own on the fly.

- 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

### Simple Interface

Intuitive UI that simplifies the discovery and placement of multi-leg accumulators.

### AI Personalization

Tailors the most popular multi-bet combinations to each punter's personal preferences.

### Discovery Engine

Surface high-engagement betting opportunities that users might have otherwise missed.

### Live & Pre-match

Full support for real-time in-play accumulators and upcoming pre-match event bundles.

### Seamless Integration

Rapid client-side setup with minimal configuration needed for basic deployment.

### Full Customization

Extensive styling and configuration options to ensure the widget feels native to your brand.

## UI and Interaction Options

### Flexible Layouts

Switch between focused single-card views or high-density horizontal carousels.

### Instant Bet Slip

One-click integration to add entire multi-leg combos directly to the operator's bet slip.

### Smart Discovery

Users can "Load New Combo" or refresh individual legs to find the perfect bet on the fly.

### Interactive Editing

Optional support for adding or removing legs directly within the combo card interface.

## Settings and Customization

- Version: mixed match multi bets or same match multi bets (same match depends on API readiness)
- Recommendation type: recommended (default) or popular (when user ID is not available)
- Sport filter
- Time span: live, pre-match, or hours until kickoff
- Widget name
- Add/remove legs toggle
- Load new combo button
- Load different leg button (not compatible with add/remove legs)
- Ticket length: maximum ticket length (default 4, minimum 2)
- Maximum combined odds threshold
- 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.myCombo`

**Environment Requirements**

## Technical Requirements:

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

**Supported Sports**

## Main Configurable Features

See the [My Combo widget demo](https://widgets.sir.sportradar.com/bet-recommendation#widget:\(name:betRecommendation.myCombo\)).
Illustrations of main layout variants and bet types with relevant property values below.

**Single Card Layout**

![Single Card Layout](https://apidocs.sportradar.com/resources/widgets/static/img/mycombo/layoutSingle.png)

Single combo card showing multi-leg accumulator with combined odds and add to bet slip button.

**Carousel Layout**

![Carousel Layout](https://apidocs.sportradar.com/resources/widgets/static/img/mycombo/layoutCarousel.png)

Multiple combo cards in horizontal carousel with navigation arrows for browsing options.

**Combo Card Details**

![Combo Card Details](https://apidocs.sportradar.com/resources/widgets/static/img/mycombo/layoutSingle.png)

Detailed combo card showing team names, markets, outcomes, individual odds, and total odds.

## API Reference

### Basic Properties

| Property             | Type             | Default      | Description                                                    |
| -------------------- | ---------------- | ------------ | -------------------------------------------------------------- |
| `user`               | `string\|number` | `0`          | User identifier for personalized combo recommendations.        |
| `productTitle`       | `string`         | `undefined`  | Title text displayed in widget header.                         |
| `showHeader`         | `boolean`        | `true`       | Controls visibility of widget header.                          |
| `showRefreshButton`  | `boolean`        | `false`      | Enables refresh functionality for new combos.                  |
| `numberOfCards`      | `number`         | `10`         | Number of combo cards for carousel layout (2-20).              |
| `layout`             | `string`         | **Required** | Display mode: `"single"` or `"carousel"`.                      |
| `editLegAction`      | `string`         | `undefined`  | Enables leg editing: `"replace"` or `undefined`.               |
| `betType`            | `string`         | `"mixed"`    | Combo type: `"mixed"` (cross-event) or `"same"` (bet builder). |
| `minTicketLength`    | `number`         | `3`          | Minimum number of legs per combo (2-12).                       |
| `maxTicketLength`    | `number`         | `4`          | Maximum number of legs per combo (2-12).                       |
| `maxAllowedOdds`     | `number`         | `undefined`  | Maximum allowed combined odds.                                 |
| `minRequiredLegOdds` | `number`         | `undefined`  | Minimum required combined odds.                                |
| `eventId`            | `string\|number` | `undefined`  | Event identifier (required when `betType: "same"`).            |
| `onItemClick`        | `function`       | `undefined`  | Callback for combo card interactions.                          |
| `debug`              | `boolean`        | `false`      | Enables debug mode with console logging.                       |

**Extended Properties**

| Property        | Type            | Default      | Description                                                                                                                                                                                                                                                                                |
| --------------- | --------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `buttonIcon`    | `string\|false` | `undefined`  | Custom icon URL for "Add to Bet Slip" button on combo cards. When provided, displays custom icon alongside button text. When `false`, hides icon entirely (text-only button). When `undefined`, uses default plus icon.                                                                    |
| `filters`       | `object`        | **Required** | Configuration object for sport and league filters. See below for detailed structure.                                                                                                                                                                                                       |
| `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. Allows using client's sport taxonomy in filter configurations. |

### Filters Object

The `filters` object controls which sports and leagues generate combo 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 combo generation. Empty array shows all available sports. Example: `[1, 2, 5]` for soccer, basketball, tennis. AI generates combos using events from specified sports only. See [Sports Reference](https://apidocs.sportradar.com/resources/widgets/docs/tutorials/references/sports.md).          |
| `league`           | `object`                | `undefined`  | League/tournament filter configuration.                                                                                                                                                                                                                                                                                                                      |
| `league.available` | `array<string\|number>` | `undefined`  | Array of Sportradar tournament/league IDs to include in combo generation. When provided, AI generates combos using only events from specified leagues. Example: `["sr:tournament:17", "sr:tournament:7"]` for Premier League and La Liga. See [Getting Identifiers](https://apidocs.sportradar.com/resources/widgets/docs/tutorials/getting-identifiers.md). |

#### Filters Example

```javascript
{
  sport: {
    available: [1, 2, 5]  // Soccer, Basketball, Tennis
  },
  league: {
    available: [
      "sr:tournament:17",   // Premier League
      "sr:tournament:34",   // Bundesliga
      "sr:tournament:132"   // NBA
    ]
  }
}
```

## 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-br-mycombo` 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-card`                                  | `background-color`, `color`, `border-radius`                                       |
| `srct-br-header`                                | `background-color`, `color`, `border-radius`, `font-style`                         |
| `srct-br-card__divider`                         | `border-color`                                                                     |
| `srct-br-eventinfo__icon`                       | `color`                                                                            |
| `srct-br-eventinfo__status`                     | `font-size`, `color`                                                               |
| `srct-br-eventinfo__name`                       | `font-size`, `color`                                                               |
| `srct-br-eventinfo__time`                       | `font-size`, `color`                                                               |
| `srct-br-marketselectionbutton-outcometeamname` | `font-style`, `font-size`, `color`                                                 |
| `srct-br-outcome__value`                        | `font-style`, `font-size`, `color`                                                 |
| `srct-br-outcome__name`                         | `font-style`, `font-size`, `color`                                                 |
| `srct-br-scoreboard`                            | `font-size`, `color`                                                               |
| `srct-br-scoreboard__teams`                     | `font-size`, `color`                                                               |
| `srct-br-scoreboard__datetime`                  | `font-size`, `color`                                                               |
| `srct-br-footer`                                | `font-size`, `font-style`, `color`, `background-color`, `border-radius`            |
| `srct-br-widgetbutton`                          | `font-size`, `font-style`, `color`, `border-radius`, `padding`, `background-color` |
| `srct-br-widgetbutton__icon`                    | `color`, `height`, `width`                                                         |

## 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.myCombo\))

### Widget Setup

**Single Combo**

Basic single combo card with mixed events for anonymous users.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-1", "betRecommendation.myCombo", {
  layout: "single",
  betType: "mixed",
  filters: {
    sport: {
      available: [1, 2],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="single"
  data-bet-type="mixed"
  data-filters='{"sport": {"available": [1, 2]}}'
></div>
```

**Carousel Layout**

Carousel layout with multiple combo options and 3-5 legs per ticket.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-2", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 10,
  minTicketLength: 3,
  maxTicketLength: 5,
  filters: {
    sport: {
      available: [1, 2, 5],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="carousel"
  data-bet-type="mixed"
  data-number-of-cards="10"
  data-min-ticket-length="3"
  data-max-ticket-length="5"
  data-filters='{"sport": {"available": [1, 2, 5]}}'
></div>
```

**Bet Builder**

Same-event bet builder with multiple markets from a single match.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-3", "betRecommendation.myCombo", {
  layout: "single",
  betType: "same",
  eventId: "67681902",
  minTicketLength: 3,
  maxTicketLength: 4,
  productTitle: "BET BUILDER",
  filters: {
    sport: {
      available: [1],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="single"
  data-bet-type="same"
  data-event-id="sr:match:12345"
  data-min-ticket-length="3"
  data-max-ticket-length="4"
  data-product-title="BET BUILDER"
  data-filters='{"sport": {"available": [1]}}'
></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 combo picks for logged-in users based on history.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-4", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  user: "user123",
  numberOfCards: 8,
  productTitle: "YOUR COMBO PICKS",
  filters: {
    sport: {
      available: [1, 2, 5, 23],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="carousel"
  data-bet-type="mixed"
  data-user="user123"
  data-number-of-cards="8"
  data-product-title="YOUR COMBO PICKS"
  data-filters='{"sport": {"available": [1, 2, 5, 23]}}'
></div>
```

**Odds Range**

Combo recommendations controlled by minimum and maximum odds ranges.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-5", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 10,
  minTicketLength: 3,
  maxTicketLength: 4,
  minRequiredLegOdds: 5, // Minimum 5.0 combined odds
  maxAllowedOdds: 50, // Maximum 50.0 combined odds
  filters: {
    sport: {
      available: [1],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="carousel"
  data-bet-type="mixed"
  data-number-of-cards="10"
  data-min-ticket-length="3"
  data-max-ticket-length="4"
  data-min-required-leg-odds="5"
  data-max-allowed-odds="50"
  data-filters='{"sport": {"available": [1]}}'
></div>
```

**Refreshable**

Daily combos with a refresh button for quick discovery of new options.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-6", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 5,
  showRefreshButton: true,
  productTitle: "DAILY COMBOS",
  filters: {
    sport: {
      available: [1, 2],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="carousel"
  data-bet-type="mixed"
  data-number-of-cards="5"
  data-show-refresh-button="true"
  data-product-title="DAILY COMBOS"
  data-filters='{"sport": {"available": [1, 2]}}'
></div>
```

**League Specific**

Combos generated exclusively from top soccer leagues.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-7", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 8,
  productTitle: "TOP LEAGUES COMBOS",
  filters: {
    sport: {
      available: [1],
    },
    league: {
      available: [
        "sr:tournament:17", // Premier League
        "sr:tournament:34", // Bundesliga
        "sr:tournament:7", // La Liga
      ],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="carousel"
  data-bet-type="mixed"
  data-number-of-cards="8"
  data-product-title="TOP LEAGUES COMBOS"
  data-filters='{"sport": {"available": [1]}, "league": {"available": ["sr:tournament:17", "sr:tournament:34"]}}'
></div>
```

**Editable Legs**

Custom combo configuration allowing users to replace individual legs.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-8", "betRecommendation.myCombo", {
  layout: "single",
  betType: "mixed",
  minTicketLength: 4,
  maxTicketLength: 6,
  editLegAction: "replace",
  productTitle: "CUSTOM COMBO",
  filters: {
    sport: {
      available: [1, 2],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="single"
  data-bet-type="mixed"
  data-min-ticket-length="4"
  data-max-ticket-length="6"
  data-edit-leg-action="replace"
  data-product-title="CUSTOM COMBO"
  data-filters='{"sport": {"available": [1, 2]}}'
></div>
```

**Click Handler**

Full interaction handling with event tracking for additions and refreshes.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-9", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 10,
  onItemClick: function (target, data) {
    console.log("My Combo interaction:", target, data);

    if (target === "addToBetSlip") {
      // User clicked add to bet slip button
      const selections = data.selections;
      const combinedOdds = data.combinedOdds;

      // Add entire combo to bet slip
      addComboToBetSlip(selections, combinedOdds);

      // Track analytics
      gtag("event", "combo_added", {
        leg_count: selections.length,
        combined_odds: combinedOdds,
        sport_ids: selections.map((s) => s.sportId),
      });
    } else if (target === "refresh") {
      // User clicked refresh button
      gtag("event", "combo_refreshed", {
        card_index: data.cardIndex,
      });
    } else if (target === "leg") {
      // User clicked individual leg (if clickable)
      gtag("event", "combo_leg_click", {
        event_id: data.selection.eventId,
        market: data.selection.marketName,
      });
    }
  },
  filters: {
    sport: {
      available: [1, 2, 5],
    },
  },
});
```

**Branded**

Custom branded widget with unique title and button icons.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-10", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 10,
  productTitle: "ACCA OF THE DAY",
  buttonIcon: "https://example.com/icons/add-combo.svg",
  showHeader: true,
  filters: {
    sport: {
      available: [1],
    },
  },
});
```

**High Value**

Focus on high-value combos with minimum 8.0 combined odds.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-11", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 5,
  minTicketLength: 3,
  maxTicketLength: 5,
  minRequiredLegOdds: 8,
  productTitle: "HIGH VALUE COMBOS",
  filters: {
    sport: {
      available: [1, 2],
    },
  },
});
```

**HTML (data attributes)**

```html
<div
  class="sr-widget"
  data-sr-widget="betRecommendation.myCombo"
  data-layout="carousel"
  data-bet-type="mixed"
  data-number-of-cards="5"
  data-min-ticket-length="3"
  data-max-ticket-length="5"
  data-min-required-leg-odds="8"
  data-product-title="HIGH VALUE COMBOS"
  data-filters='{"sport": {"available": [1, 2]}}'
></div>
```

**Advanced**

Advanced configuration with custom sport ID mapping and safe odds caps.

**JavaScript**

```javascript
SIR("addWidget", "#my-combo-13", "betRecommendation.myCombo", {
  layout: "carousel",
  betType: "mixed",
  numberOfCards: 8,
  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],
    },
  },
});
```

### 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

- `betSlipSelection` - visual representation of markets already in betslip

#### 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 `maxAllowedOdds` is exceeded, the widget trims legs starting from the highest-odds one and recalculates; this repeats until combined odds fall below the limit or the card drops below `minTicketLength`, in which case the card is discarded.
- Setting a high `minRequiredLegOdds` combined with a low `maxTicketLength` can result in no cards being returned. Use `debug: true` to inspect how thresholds are filtering combos.
- With `editLegAction: "replace"`, the replacement leg is constrained to the same sport, a similar odds range, and compatibility with the remaining legs.
