By completing this tutorial, you will:
tickets adapter endpointtickets request handler that returns TicketResponseV2 via callbackticketsFetchMore (Option B)ticketsFetchMore onAction when using paginationBefore implementing the tickets endpoint, ensure you have:
The tickets endpoint is one of several adapter endpoints. Implement additional endpoints (event, eventMarkets, betSlipSelection, etc.) as needed for your widget features.
This tutorial reflects behaviour verified in the widgets repo (MR 15860):
| Source | What it proves |
|---|---|
src/types/adapter/v2Requests.ts | TicketsRequest: endCustomerId, optional events, channelId only — no operatorId, no page |
src/models/adapter/adapterProxy.ts | Widget stack calls your tickets(args, callback) once; you call callback to deliver data |
src/buildingblocks/virtualStadium/.../userBetSlips.ts | VS single tickets() invocation; pagination via ticketsFetchMore |
src/buildingblocks/centralHub/.../userBetSlips.ts | CH uses the same tickets() + ticketsFetchMore contract |
userBetSlips.spec.ts | Option A single emission and Option B pagination tested |
The tickets endpoint retrieves a user's placed bets and returns them as v2 Ticket objects inside a TicketResponseV2. This powers bet-sharing UIs — for example the Virtual Stadium chat bet-share picker and Central Hub share-betslip screens.
There are two implementation methods. Both use the same tickets(args, callback) signature and may return an unsubscribe function for cleanup when the picker closes, but they differ in how data is delivered:
The widget stack invokes your tickets(args, callback) once. You (your adapter code, or your embed-page handler after onAction ticketsFetchMore) call callback(undefined, data) to push data to the widget. The widget never calls callback.
Page 2+ is not fetched by invoking tickets(args, callback) again. Store the callback from the initial tickets() call and call it with { newTickets, ... } after your embed page handles onAction ticketsFetchMore.
See the Tickets Function reference for flow diagrams and field details.
Return the user's full open-ticket list in a single callback. The widget renders everything immediately with no pagination.
Register your adapter with the SIR global function before adding any widgets. Ensure only one adapter is registered per page load.
const adapter = {
endpoints: {
// tickets endpoint added in the next steps
}
};
SIR('registerAdapter', adapter);Add the tickets handler. Store nothing yet — just wire the callback pattern.
adapter.endpoints.tickets = (args, callback) => {
// args.endCustomerId — the logged-in user's ID
// args.events — optional filter by match events on the current channel
// args.channelId — optional Virtual Stadium channel ID
return () => {
// Cleanup: cancel any in-flight fetch when the picker closes
};
};Retrieve the user's complete open-ticket list. Apply optional events filtering if provided.
adapter.endpoints.tickets = (args, callback) => {
fetchAllUserTickets(args.endCustomerId, args.events, (error, allTickets) => {
if (error) {
return callback(error);
}
// Continue to Step 4
});
return ()
Transform your backend data into v2 Ticket objects and emit the full list.
adapter.endpoints.tickets = (args, callback) => {
fetchAllUserTickets(args.endCustomerId, args.events, (error, allTickets) => {
if (error) {
return callback(error);
}
const tickets = allTickets.map
Each ticket must include ticketId, version: "2.0", and at least one Bet with selections, odds, and stake. Provide TicketSelectionContext on each selection so tickets remain readable after market data ages out.
Open the bet-share picker (Virtual Stadium chat or Central Hub). All tickets should appear immediately without pagination.
function mapBackendTicketToV2Ticket(backendTicket) {
return {
ticketId: backendTicket.id,
version: "2.0",
bets: backendTicket
Return the first batch with pageSize, hasMore, and nextCursor. When the widget needs the next page, it fires onAction ticketsFetchMore; you load the next batch and call the stored callback with { newTickets, ... }.
Same as Track A Step 1–2, but store the callback reference so the embed page can push subsequent pages.
let ticketsCallback = null;
let currentCustomerId = null;
const adapter = {
endpoints: {
tickets: (args, callback) => {
ticketsCallback = callback;
currentCustomerId = args.endCustomerId;
// Continue in Step 2
return () => {
Signal paginated mode by including pageSize and hasMore on the first callback.
tickets: (args, callback) => {
ticketsCallback = callback;
currentCustomerId = args.endCustomerId;
fetchTicketPage(args.endCustomerId, null, args.events, (error, page) => {
if (error) {
Create a function the onAction handler can call. It fetches the next page and pushes newTickets on the stored callback.
function loadMoreTickets(cursor) {
if (!ticketsCallback || !currentCustomerId) {
return;
}
fetchTicketPage(currentCustomerId, cursor, null, (error, page) => {
if (error) {
return ticketsCallback(error);
}
ticketsCallback
Wire the widget's ticketsFetchMore onAction to your loadMoreTickets function.
SIR('addWidget', '#sr-vs-widget', 'virtualStadium', {
onAction: function(action) {
if (action.type === 'ticketsFetchMore') {
loadMoreTickets(action.data?.cursor);
}
}
});On the last batch, return hasMore: false (and omit nextCursor). The widget stops requesting more pages.
ticketsCallback(undefined, {
newTickets: lastPageItems.map(mapBackendTicketToV2Ticket),
hasMore: false
});Open the bet-share picker. Confirm:
ticketsFetchMore), the next batch loads and appends.hasMore: false is returned.<script>
let ticketsCallback =
Deliver data on the same callback from the initial tickets() call — never invoke tickets() again for page 2+.
| Field | Option A | Option B | Verified |
|---|---|---|---|
tickets | Single emission with the full list | First page only | Option A: yes |
newTickets | Not used — return everything in tickets | Next pagination page | Option B in VS: yes |
pageSize, hasMore, nextCursor | Not used | First-page pagination signalling | Option B: yes |
BetShareResponse ({ bets: BetSlip[] }) is deprecated. New Virtual Stadium integrations must return TicketResponseV2. The legacy format remains supported only for existing V1 adapter integrations.
| Option A — Non-paginated | Option B — Paginated | |
|---|---|---|
| When to use | Small ticket lists; backend returns everything at once | Large ticket histories; backend loads in batches |
| First callback | { tickets: Ticket[] } — complete list | { tickets, pageSize, hasMore, nextCursor } — first batch |
| Further callbacks | Usually none — single { tickets } emission is tested and recommended | You call the stored callback with { newTickets, hasMore?, nextCursor? } after onAction ticketsFetchMore |
| Pagination | N/A | Widget fires onAction ticketsFetchMore when the next page is needed |