Skip to content

Form Events & dataLayer

The widget emits DOM CustomEvents and pushes to window.dataLayer (for Google Tag Manager) so you can track form views, field interactions, bookings, and submissions in your analytics stack — without touching the form internals.

EventDetailDOMdataLayerTelemetry
rl:form:viewformId, studioId?
rl:form:prefillformId, studioId?, prefilledFieldIds
rl:form:stepformId, studioId?, step
rl:form:field:changeformId, fieldId, fieldName?
rl:form:field:blurformId, fieldId, fieldName?
rl:form:submit:startformId, studioId?
rl:form:submit:successformId, submissionId, formData, bookingSlot?
rl:form:submit:errorformId, errorCode, error?
rl:form:booking:typeformId, fieldId, fieldName?, appointmentTypeId
rl:form:booking:monthformId, fieldId, fieldName?, direction
rl:form:booking:dateformId, fieldId, fieldName?, dateKey
rl:form:booking:slotformId, fieldId, fieldName?, slotKey
rl:form:captcha:loadformId, provider
rl:form:captcha:successformId, provider
rl:form:captcha:errorformId, provider
rl:form:captcha:resetformId, provider

All events share this superset of detail fields:

interface EventDetail {
formId: string;
studioId?: string;
step?: number;
fieldId?: string;
fieldName?: string; // set only when the field has a non-empty `name` — fall back to fieldId when absent
submissionId?: string;
error?: string;
errorCode?: 'validation' | 'captcha' | 'network' | 'server' | 'unknown';
durationMs?: number;
// Booking calendar context
appointmentTypeId?: string;
direction?: 'prev' | 'next';
dateKey?: string;
slotKey?: string;
// Captcha context
provider?: string;
// DOM/dataLayer only — never sent to telemetry
formData?: Record<string, string | string[]>;
bookingSlot?: { appointmentTypeId, calendarId, resourceId?, startDateTime, endDateTime };
prefilledFieldIds?: string[];
}
document.addEventListener('rl:form:submit:success', (e) => {
console.log('Form submitted:', e.detail.submissionId);
console.log('Data:', e.detail.formData);
});

Listeners on document see events from every form on the page. Filter by e.detail.formId if you have multiple.

The widget pushes to window.dataLayer for every event in the list above (except the two excluded). Each push has the shape:

{ event: '<event-name>', ...detail }

So a successful submission lands as:

{
event: 'rl:form:submit:success',
formId: '...',
submissionId: '...',
formData: { email: '...', firstName: '...', ... },
bookingSlot: { ... }
}
  1. Create a Custom Event trigger for rl:form:submit:success (or whichever event you want).
  2. Build a tag (Meta Pixel, Google Ads, etc.) that fires on the trigger.
  3. Map {{eventModel.formId}}, {{eventModel.submissionId}}, etc. into your tag’s parameters via Data Layer Variables.

For field-level triggers (e.g., “track when user fills email”), key on eventModel.fieldName rather than fieldId — see the tip above.

Ad pixel integration (Meta CAPI, Google Ads)

Section titled “Ad pixel integration (Meta CAPI, Google Ads)”

For conversion tracking with deduplication:

document.addEventListener('rl:form:submit:success', (e) => {
fbq('track', 'Lead', {
eventID: e.detail.submissionId, // dedup key for Conversion API
content_name: e.detail.formId,
});
});

Phone numbers in formData are E.164-normalized — see Field Types → tel.

The widget batches a subset of events into a POST /public/forms/:id/telemetry beacon for funnel analytics (see Public API → telemetry). The beacon is best-effort — drops on 429 or network failure — and contains no PII: formData, bookingSlot, appointmentTypeId, slotKey are stripped client-side before queueing.