The Virtual Stadium adapter is the core component you implement to handle all communication between Virtual Stadium widgets and your betting platform. It processes data requests from widgets and integrates with your backend systems. Custom Adapter gives you full control over the integration, allowing you to implement and host all adapter functionality on your own infrastructure. This approach provides maximum flexibility for custom betting workflows while maintaining compatibility with Virtual Stadium's modular architecture.

Before beginning custom adapter implementation, complete these foundational steps:
Phase 1: Initiation (1-2 Days)
Phase 2: Technical Kickoff Meeting
Phase 3: Post-Kickoff Setup
View Complete Integration Timeline →
The custom adapter implementation follows a structured timeline where you handle all development and hosting:
Module Implementation:
Register your adapter before adding Virtual Stadium widgets to the page. The adapter function handles all communication between widgets and your backend.
Base Adapter Registration Example →
Each module requires specific API endpoints and data formats. See individual module documentation for detailed requirements.
Bet Share - Social betting features
Flash Bet - Live betting features
Bet Insights - AI-powered suggestions
Expand Example Code
Before using the example code, create a test channel. See the channel creation documentation. To test Flash Bet, activate the Testing Channel and follow the testing channel instructions.
In the code below, replace the following placeholders:
<your-channel-id> with your channel ID<CLIENT_ID> with your client ID<your-jwt-token> with your newly generated JWT token<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Self-Service Adapter Demo</title>
<style>
body {
display: flex;
justify-content: center;
}
.widgets {
max-width: 620px;
width: 100%;
}
#sr-widget {
border: rgba(0, 0, 0, 0.12) solid 1px;
}
</style>
<!-- Load the SIR widget loader script -->
<script type="text/javascript">
/**
* TODO(developer): Replace <CLIENT_ID> with your actual client ID
* provided by Sportradar before running this code.
*/
(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
)})(window,document,"script","https://widgets.sir.sportradar.com/<CLIENT_ID>/widgetloader","SIR", {
language: 'en' // SIR global options
});
// Mock ticket data for demonstration
const singleBet = {
id: 'sr:bet-share-event:single',
betSlipId: 'sr:bet-share-event:single',
betType: 'single',
currency: 'USD',
combinedOdds: {
decimalValue: 2.55,
displayValue: '2.55'
},
stake: {
value: '50.00'
},
payout: {
value: '127.50'
},
cashOut: {
value: '21.83'
},
bets: [
{
id: 'sr:bet:single',
betType: '',
markets: [
{
id: 'sr:single-market:1234',
name: 'Moneyline | 1st Quarter',
outcomes: [
{
id: 'sr:single-outcome:56789',
name: 'Monaco',
odds: 2.55
}
]
}
],
odds: {
decimalValue: 2.55,
displayValue: ''
},
event: {
id: 'sr:single-event:56789',
name: 'Metz vs Monaco',
teams: [
{ id: 'sr:competitor:102', name: 'Metz' },
{ id: 'sr:competitor:201', name: 'Monaco' }
]
}
}
]
};
// Mock ticket data for demonstration
const multiBet1 = {
id: 'sr:bet-share-event:multibet1',
betSlipId: 'sr:bet-share-event:multibet1',
betType: 'multibet',
currency: 'USD',
combinedOdds: {
decimalValue: 31.63,
displayValue: '31.63'
},
stake: {
value: '20.50'
},
payout: {
value: '648.41'
},
cashOut: {
value: '221.83'
},
bets: [
{
id: 'sr:bet1:multibet1',
betType: '',
markets: [
{
id: 'sr:multibet1-bet1-market:2134',
name: 'Moneyline | Full Time',
outcomes: [
{
id: 'sr:multibet1-bet1-outcome:645389',
name: 'Metz',
odds: '1.45'
}
]
}
],
event: {
id: 'sr:multibet1-bet1-event:98765',
teams: [
{ id: 'sr:competitor:102', name: 'Metz' },
{ id: 'sr:competitor:201', name: 'Monaco' }
],
sport: { id: '1' }
}
},
{
id: 'sr:bet2:multibet1',
betType: '',
markets: [
{
id: 'sr:multibet1-bet2-market:12435',
name: 'Total - Full Time',
outcomes: [
{
id: 'sr:multibet1-bet2-outcome:77965',
name: 'Over 225.5',
oddsDecimal: 2.05
}
]
}
],
event: {
id: 'sr:multibet1-bet2-event:98765945',
teams: [
{ id: 'sr:competitor:102', name: 'Portland' },
{ id: 'sr:competitor:201', name: 'Memphis' }
],
sport: { id: '2' }
}
},
{
id: 'sr:bet3:multibet1',
betType: '',
markets: [
{
id: 'sr:multibet1-bet3-market:4432',
name: 'Both teams to score',
outcomes: [
{
id: 'sr:multibet1-bet3-outcome:98765',
name: 'Yes',
oddsDecimal: 3
}
]
}
],
event: {
id: 'sr:multibet1-bet3-event:98765945',
teams: [
{ id: 'sr:competitor:002', name: 'Nantes' },
{ id: 'sr:competitor:001', name: 'Nice' }
],
sport: { id: '1' }
}
},
{
id: 'sr:bet4:multibet1',
betType: '',
markets: [
{
id: 'sr:multibet1-bet4-market:4321',
name: 'Total | Full Time',
outcomes: [
{
id: 'sr:multibet1-bet4-outcome:67898',
name: 'Over 2.5',
oddsDecimal: 1.55
}
],
odds: {
// it is used as bet odds - see "use market odds as bet odds" comment in convertAdapterToBetSharePayload.ts
decimalValue: 3.55,
displayValue: ''
}
}
],
event: {
id: 'sr:multibet1-bet4-event:98765945',
teams: [
{ id: 'sr:competitor:002', name: 'Lazio' },
{ id: 'sr:competitor:001', name: 'Juventus' }
],
sport: { id: '1' }
}
}
]
};
// Mock data for demonstration - replace with real API calls
const adapter = {
endpoints: {
event: (args, callback) => {
console.log(args);
callback(undefined, {
event: {
id: args.selection.event,
isLive: false,
date: {
displayValue: "2026-02-02 18:00",
startTime: "2026-02-02T18:00:00Z",
},
sport: { id: "sr:sport:1", name: "Soccer" },
category: { id: "sr:category:1", name: "France" },
tournament: { id: "sr:tournament:17", name: "League 1" },
teams: [
{ id: "sr:competitor:1", name: "Monaco" },
{ id: "sr:competitor:2", name: "Metz" },
],
},
});
return () => {};
},
availableMarketsForEvent: (args, callback) => {
console.log('availableMarketsForEvent: ',args);
callback(undefined, {
event: {
id: args.selection.event,
type: 'uf'
},
markets: [
{ id: "1", name: "1x2" },
{ id: "18", name: "Total Goals", specifiers: "total=0.5" },
{ id: "18", name: "Total Goals", specifiers: "total=1.5" },
],
});
return () => {};
},
eventMarkets: (args, callback) => {
console.log('eventMarkets:', args);
callback(undefined, {
event: {
id: args.selection.event,
type: 'uf'
},
markets: [
{
id: "1",
name: "1x2",
status: "active",
outcomes: [
{
id: "1",
name: "Home",
status: "active",
odds: { type: 'decimal', value: "1.85" },
},
{
id: "X",
name: "Draw",
status: "active",
odds: { type: 'decimal', value: "3.40" },
},
{
id: "2",
name: "Away",
status: "active",
odds: { type: 'decimal', value: "4.20" },
},
],
},
{
id: "18",
name: "Total Goals",
specifiers: "total=1.5",
status: "active",
outcomes: [
{
id: "12",
name: "Over 2.5",
status: "active",
odds: { type: 'decimal', value: "1.95" },
},
{
id: "13",
name: "Under 2.5",
status: "active",
odds: { type: 'decimal', value: "1.85" },
},
],
},
],
});
return () => {};
},
matchEventSuggestedSelection: (args, callback) => {
console.log('matchEventSuggestedSelection: ',args);
callback(undefined, {
selections: [
{
type: "uf",
event: args.event.event,
market: "1",
},
],
});
return () => {};
},
tickets: (args, callback) => {
console.log(args);
callback(undefined, {
bets: [singleBet, multiBet1]
});
return () => {};
},
},
};
// Register adapter before adding widgets
SIR("registerAdapter", adapter);
// Add Virtual Stadium widget
SIR('addWidget', '#sr-widget', 'virtualstadium', {
jwt: '<your-jwt-token>', // TODO: Replace <your-jwt-token> with real JWT token
channelId: '<your-channel-id>', // TODO: Replace <your-channel-id> with real channel ID
enableFlashBet: true,
enableBetInsights: true,
testMarkets: '1|18;total=0.5,total=1.5' // ONLY FOR TESTING PURPOSES
});
</script>
</head>
<body>
<div class="widgets">
<!-- Widget container -->
<div id="sr-widget">Widget will load here...</div>
</div>
</body>
</html>