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.
requestName string required
The type of data being requested (e.g., bet details, user balance).
args Record<string, any> required
Arguments specific to the request.
callback function required
Function to execute when data is available or updated.
Request names and arguments vary by widget. Refer to individual module documentation for specific requirements.
Adapter Registration:
function onRequest(requestName, args, callback) {
switch (requestName) {
case 'someFeature':
// Store callback reference for potential subscriptions
// Implement data fetching and processing logic
// Execute callback with results or errors
break;
default:
// Handle unknown requests - you can:
// - Ignore silently for unimplemented features
// - Send to analytics/monitoring system
// - Log for debugging during development
}
}
SIR('registerAdapter', onRequest);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
Complete Integration Example with Mock Data:
<!-- Load the SIR widget loader script -->
<script>
/**
* 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(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/<CLIENT_ID>/widgetloader', 'SIR', {
language: 'en'
});
// Mock data for demonstration - replace with real API calls
const mockData = {
betShare: [
{
id: 'bet_123',
title: 'Great bet!',
stake: 50,
odds: 2.5,
selections: [
{
eventId: 'event_456',
marketId: 'market_789',
outcomeId: 'outcome_101',
eventName: 'Team A vs Team B',
marketName: 'Match Winner',
outcomeName: 'Team A'
}
]
}
],
matchEventMarkets: {
goal: {
markets: [
{
id: 'goal_market',
name: 'Goal Scorer',
outcomes: [
{ id: 'player_1', name: 'Player A', odds: 3.0 },
{ id: 'player_2', name: 'Player B', odds: 3.5 }
]
}
]
},
card: {
markets: [
{
id: 'card_market',
name: 'Card Type',
outcomes: [
{ id: 'yellow', name: 'Yellow Card', odds: 2.0 },
{ id: 'red', name: 'Red Card', odds: 5.0 }
]
}
]
}
},
outcomes: [
// IMPORTANT: The event.id, market.id, and outcome.id in each outcome object
// MUST match the corresponding IDs provided in the 'args' parameter of the 'outcomes' request.
// Only outcomes with matching IDs will be displayed in the widget.
// If an outcome's IDs don't match any in args, it will be ignored.
{
// Response/data to outcome 0, if the id's wont match then they wont be displayed
event: {
id: '51096239', // This needs to match eventId from one of the outcomes recieved in args!
date: '14/2/2025', // date of the event as it will be displayed, for tournament level chat
teams: [
{
id: '1w', // will be sent on click
name: 'Team A' // Home Team Name to be displayed
},
{
id: '2w', // will be sent on click
name: 'Team B' // Away team name to be displayed
}
],
liveCurrentTime: 'First half', // current time/period in the event that will be display after date in tournament level chat
isLive: true, // Will display LIVE indicator for tournament level chat
sport: {
// not used in VS, will be sent on click
id: '3d', // will be sent on click
name: 'Soccer' // sport name
},
category: {
// not used in VS, will be sent on click
id: '2f', // will be sent on click
name: 'U18' // category of sport event
},
tournament: {
// not used in VS, will be sent on click
id: '123g', // will be sent on click
externalId: '1a', // will be sent on click
name: 'Tournament A' // tournament name
},
result1: {
result: [1, 3] // home team score, away team score - displayed in tournament level chat
}
},
market: {
id: 19, // This needs to match marketId from one of the outcomes recieved in args!
// specifier: {
// value: 'total=0.5', // This needs to match specifier from one of the outcomes recieved in args!
// displayValue: 'Total Goals Over 1.5' // displayed in the widget
// },
externalId: '23d', // will be sent on click
name: 'Over/Under', // displayed in the widget
status: {
isActive: true
}
},
outcome: {
id: 12, // This needs to match outcomeId from one of the outcomes recieved in args!
externalId: '12a', // will be sent on click
name: 'Over 1.5', // displayed in the widget
oddsDecimal: 2.05, // if odds field not provided, this will be used to display odds, if odds field is provided this is used to display changes in odds
odds: '1/2', // if provided, odds will be displayed like this
status: {
isActive: true // if false, it will display "temporarily unavailable" instead of odds
},
isSelected: true
}
},
{
// Response/data to outcome 0, if the id's wont match then they wont be displayed
event: {
id: '51096239', // This needs to match eventId from one of the outcomes recieved in args!
date: '14/2/2025', // date of the event as it will be displayed, for tournament level chat
teams: [
{
id: '1w', // will be sent on click
name: 'Team A' // Home Team Name to be displayed
},
{
id: '2w', // will be sent on click
name: 'Team B' // Away team name to be displayed
}
],
liveCurrentTime: 'First half', // current time/period in the event that will be display after date in tournament level chat
isLive: true, // Will display LIVE indicator for tournament level chat
sport: {
// not used in VS, will be sent on click
id: '3d', // will be sent on click
name: 'Soccer' // sport name
},
category: {
// not used in VS, will be sent on click
id: '2f', // will be sent on click
name: 'U18' // category of sport event
},
tournament: {
// not used in VS, will be sent on click
id: '123g', // will be sent on click
externalId: '1a', // will be sent on click
name: 'Tournament A' // tournament name
},
result1: {
result: [1, 3] // home team score, away team score - displayed in tournament level chat
}
},
market: {
id: 1, // This needs to match marketId from one of the outcomes recieved in args!
// specifier: {
// value: 'total=0.5', // This needs to match specifier from one of the outcomes recieved in args!
// displayValue: 'Total Goals Over 1.5' // displayed in the widget
// },
externalId: '23d', // will be sent on click
name: 'Over/Under', // displayed in the widget
status: {
isActive: true
}
},
outcome: {
id: 1, // This needs to match outcomeId from one of the outcomes recieved in args!
externalId: '12a', // will be sent on click
name: 'Over 1.5', // displayed in the widget
oddsDecimal: 2.05, // if odds field not provided, this will be used to display odds, if odds field is provided this is used to display changes in odds
odds: '1/2', // if provided, odds will be displayed like this
status: {
isActive: true // if false, it will display "temporarily unavailable" instead of odds
}
}
}
// {
// reapeat for each outcome recieved in args that you want to display
// same format as above, needs to match another outcome recieved in args
// }
// If you don't wish to show outcomes recieved in args, then don't include them in the response.
]
};
/**
* Adapter request handler - replace mock data with real API calls
* @param {string} requestName - Type of data being requested
* @param {object} args - Request-specific arguments
* @param {function} callback - Callback to execute with data or errors
* @returns {function|undefined} Unsubscribe function for real-time updates
*/
function onRequest(requestName, args, callback) {
switch (requestName) {
case 'virtualStadium.betShare':
// TODO: Replace with real API call
// fetch('/api/bets/shared', {
// method: 'GET',
// headers: { 'Authorization': `Bearer ${userToken}` }
// })
// .then(response => response.json())
// .then(data => callback(null, data))
// .catch(error => callback(error));
// Mock response for now
setTimeout(() => {
callback(null, { bets: mockData.betShare });
}, 100);
break;
case 'virtualStadium.matchEventMarkets':
// Return markets for the requested event types
const response = {};
response.matchEventMarkets = mockData.matchEventMarkets;
callback(response);
break;
case 'outcomes':
// Return outcomes for Bet Insights
// Filter outcomes to only include those that match the IDs in args.outcomes
console.log("Outcomes request args:", args);
if (args.outcomes && Array.isArray(args.outcomes)) {
const requestedOutcomes = args.outcomes;
const matchingOutcomes = mockData.outcomes.filter(mockOutcome => {
return requestedOutcomes.some(requested => {
return (
mockOutcome.event.id === requested.eventId &&
mockOutcome.market.id === requested.marketId &&
mockOutcome.outcome.id === requested.outcomeId
);
});
});
console.log("Returning matching outcomes:", matchingOutcomes);
callback(null, { outcomes: matchingOutcomes });
} else {
// If no args.outcomes provided, return all mock outcomes
callback(null, { outcomes: mockData.outcomes });
}
break;
default:
break;
}
}
// Register adapter before adding widgets
SIR('registerAdapter', onRequest);
// Add Virtual Stadium widget
SIR('addWidget', '.sr-widget', 'virtualstadium', {
jwt: 'your-jwt-token', // TODO: Replace with real JWT token
channelId: 'your-channel-id' // TODO: Replace with real channel ID
enableBetShare: true,
enableFlashBet: true,
enableBetInsights: true,
});
</script>
<!-- Widget container -->
<div class="sr-widget">Widget should load here.</div>