Booking Widget Analytics Events
The Resevu booking widget exposes browser-local lifecycle hooks that tenant websites can connect to their own Google Tag Manager, GA4, Meta Pixel, or other analytics setup.
The widget does not send analytics data to Resevu, Google, Meta, or another tenant automatically. It only dispatches CustomEvent objects on the window of the page where the widget is embedded. The host website decides whether and how to forward them.
Multi-Tenant Isolation
Every event includes the active tenant slug in event.detail.tenant.
Events are dispatched inside the tenant website's browser page. They do not travel to another website, another tenant, or another GTM container. Normal browser origin isolation also prevents one tenant website from reading another tenant website's window, dataLayer, or sessionStorage.
Always verify the tenant before forwarding an event:
const expectedTenant = 'your-tenant-slug';
window.addEventListener('resevu:booking-status', (event) => {
if (event.detail?.tenant !== expectedTenant) {
return;
}
// Forward the event to this tenant's analytics setup.
});
This check is recommended even when a page only embeds one Resevu widget. It prevents incorrect attribution if the page is misconfigured later.
Available Events
| Browser event | When it fires | Additional detail fields |
|---|---|---|
resevu:booking-opened |
The massage booking modal opens | booking_type |
resevu:booking-step |
The visible booking step changes | step, booking_type |
resevu:booking-checkout |
A booking was accepted and the customer is about to leave for payment | Booking checkout payload below |
resevu:booking-status |
The customer returns from payment with success, failed, or pending |
status plus the persisted booking checkout payload |
resevu:voucher-opened |
The gift-voucher modal opens | Optional value, currency |
resevu:voucher-checkout |
A voucher order was accepted and the customer is about to leave for payment | Voucher checkout payload below |
resevu:voucher-status |
The customer returns from voucher payment with success, failed, or pending |
status plus the persisted voucher checkout payload |
All events also include:
{
tenant: 'your-tenant-slug'
}
Booking Checkout Payload
resevu:booking-checkout can contain:
{
tenant: 'your-tenant-slug',
event_id: 'stable-browser-event-id',
booking_type: 'single', // or "duo"
service_id: 123,
service_name: 'Example treatment',
second_service_id: 456, // duo only
second_service_name: 'Second treatment', // duo only
duration_minutes: 60,
value: 75,
deposit_value: 25,
currency: 'EUR',
payment_type: 'deposit' // depends on tenant payment configuration
}
value is the total booking value. deposit_value is the calculated deposit amount. Optional fields can be absent when they are not available.
Voucher Checkout Payload
resevu:voucher-checkout can contain:
{
tenant: 'your-tenant-slug',
event_id: 'stable-browser-event-id',
value: 50,
currency: 'EUR'
}
Payment Status
Status events contain one of:
success
failed
pending
The checkout payload is stored temporarily in sessionStorage under a tenant-specific key. This allows the widget to reuse the same event_id, value, currency, and service details after the payment redirect.
The final status event is emitted only when the matching checkout payload is still available in the same browser tab. This avoids treating a copied success URL or a later page refresh as a new conversion.
Google Tag Manager Example
The following bridge converts Resevu hooks into tenant-owned dataLayer events. It deliberately forwards only non-personal fields.
(() => {
const expectedTenant = 'your-tenant-slug';
const allowedKeys = [
'tenant',
'status',
'step',
'event_id',
'booking_type',
'service_id',
'service_name',
'second_service_id',
'second_service_name',
'duration_minutes',
'value',
'deposit_value',
'currency',
'payment_type'
];
const cleanDetail = (detail) => {
if (!detail || detail.tenant !== expectedTenant) {
return null;
}
return allowedKeys.reduce((result, key) => {
if (detail[key] !== undefined && detail[key] !== '') {
result[key] = detail[key];
}
return result;
}, {});
};
const push = (name, detail) => {
const clean = cleanDetail(detail);
if (!clean) return;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ event: name, ...clean });
};
window.addEventListener('resevu:booking-checkout', (event) => {
push('booking_payment_redirect', event.detail);
});
window.addEventListener('resevu:booking-status', (event) => {
if (event.detail?.status === 'success') {
push('booking_complete', event.detail);
}
});
window.addEventListener('resevu:voucher-checkout', (event) => {
push('voucher_checkout', event.detail);
});
window.addEventListener('resevu:voucher-status', (event) => {
if (event.detail?.status === 'success') {
push('purchase', event.detail);
}
});
})();
In GTM, create Custom Event triggers for the resulting event names. A typical mapping is:
dataLayer event |
GA4/advertising meaning |
|---|---|
booking_payment_redirect |
Begin or initiate checkout |
booking_complete |
Completed appointment or schedule conversion |
voucher_checkout |
Begin or initiate voucher checkout |
purchase |
Completed gift-voucher purchase |
Use event_id for browser/server deduplication if the tenant later adds a server-side conversion integration.
Consent And Privacy
The hooks are functional, local browser events. The tenant website remains responsible for analytics consent and tag configuration.
- Do not forward names, email addresses, telephone numbers, notes, medical information, or other personal data.
- Configure GTM tags to respect the tenant website's consent settings.
- Do not initialize a tenant's GA4 or advertising pixel inside the shared Resevu widget.
- Keep analytics destinations and credentials in the tenant website or its own tag manager.
- Treat service names, values, and identifiers according to the tenant's privacy policy and applicable law.
Debugging
Run this in the tenant website's browser console before opening the widget:
[
'booking-opened',
'booking-step',
'booking-checkout',
'booking-status',
'voucher-opened',
'voucher-checkout',
'voucher-status'
].forEach((name) => {
window.addEventListener(`resevu:${name}`, (event) => {
console.log(`resevu:${name}`, event.detail);
});
});
Verify the following:
event.detail.tenantmatches the website's tenant slug.- No personal form fields appear in the payload.
- Checkout and final status use the same
event_id. - A successful booking produces one completed-booking conversion.
- A successful voucher payment produces one purchase conversion.
- Refreshing a success URL does not create another conversion.
Payment tests should use the payment provider's test mode whenever available.