How to Track Acuity Scheduling Appointments with Google Tag Manager
A step-by-step guide to connecting embedded Acuity bookings to Google Tag Manager with postMessage, an AcuityEvent data layer event, and reusable conversion triggers.
Written by Abdel · Updated September 3, 2026
If you’re using Acuity Scheduling to book appointments on your website, you may want to track completed bookings as conversions in Google Tag Manager. This lets you measure how many visitors actually schedule appointments, then send the relevant conversions to GA4, Google Ads, Meta, LinkedIn, or another marketing platform.
The challenge is that Acuity can run inside an iframe—or several nested iframes. GTM on your main website does not automatically know when someone completes an appointment.
In this guide, I’ll show you how to connect the two using JavaScript and the browser’s postMessage() method. We’ll push an AcuityEvent event into the GTM data layer after a booking and use it to trigger your conversion tags.
Prefer to watch the video?
Follow along with my complete tutorial: add both scripts, complete a test appointment, find AcuityEvent in GTM Preview Mode, and connect a conversion tag. The written version below includes additional security checks and corrects the calendar field label.
What we’re going to build
We’ll use two snippets. The first runs in Acuity’s Custom Conversion Tracking section and sends a booking message to the top-level browser window. The second runs in GTM on your main website and translates that message into a data layer event.
Once AcuityEvent reaches GTM, you can use it like any other Custom Event. One booking signal can serve multiple destinations without a separate Acuity integration for each platform.
Add the tracking script to Acuity Scheduling
Log in to Acuity and open Integrations → Analytics + Custom Integrations → Custom conversion tracking. Paste this into the HTML tracking code field and save your settings.
<script>
try {
var postobject = JSON.stringify({
event: "AcuityEvent",
Email: "%email%",
ID: "%id%",
Type: "%type%",
AppointmentType: "%appointmentType%",
Calendar: "%calendar%"
});
var targetWindow = window;
while (targetWindow.parent && targetWindow.parent !== targetWindow) {
targetWindow = targetWindow.parent;
}
// Replace this with the exact origin of your main website.
targetWindow.postMessage(postobject, "https://www.example.com");
} catch (e) {
window.console.warn('Acuity tracking message could not be sent.');
}
</script>Replace https://www.example.com with the exact protocol and hostname of the website containing the scheduler—no path or trailing slash. The www and non-www versions are different origins. Do not replace it with * when sending customer information.
Acuity also runs conversion tracking for packages, gift certificates, and subscriptions. The listener below filters for appointments so those purchases do not become appointment leads. See Acuity’s conversion tracking documentation for supported variables and setup details.
Understand the Acuity script
The script builds an object from Acuity’s dynamic values, then converts it to a JSON string using JSON.stringify().
{
event: "AcuityEvent",
Email: "%email%",
ID: "%id%",
Type: "%type%",
AppointmentType: "%appointmentType%",
Calendar: "%calendar%"
}| Variable | What it represents |
|---|---|
%id% | Appointment ID |
%type% | Appointment or order |
%appointmentType% | Appointment type |
%calendar% | Calendar name—not the customer’s full name |
%email% | Client email address |
Find the top-level window
var targetWindow = window;
while (targetWindow.parent && targetWindow.parent !== targetWindow) {
targetWindow = targetWindow.parent;
}Rather than assuming a fixed iframe depth, this loop follows parent windows until it reaches the top. At that point, targetWindow.parent === targetWindow. It then sends the serialized event to the configured website origin.
targetWindow.postMessage(postobject, "https://www.example.com");These placeholders are inserted into JavaScript strings. Test appointment and calendar names containing quotes, backslashes, and line breaks; JSON.stringify() does not repair a string literal that was broken by substitution before the script ran.
Add the listener to Google Tag Manager
Open GTM and go to Tags → New → Tag Configuration → Custom HTML. Paste this listener:
<script>
(function () {
// Prevent duplicate listeners if this tag fires more than once.
if (window.__acuityListenerInstalled) return;
window.__acuityListenerInstalled = true;
var seenAppointments = Object.create(null);
window.addEventListener('message', function (ev) {
// Acuity's documented conversion-tracking frame origin.
// Confirm this origin in your own booking setup before publishing.
if (ev.origin !== 'https://sandbox.acuityinnovation.com') return;
try {
var data = typeof ev.data === 'string'
? JSON.parse(ev.data)
: ev.data;
if (!data || data.event !== 'AcuityEvent') return;
if (typeof data.Type !== 'string' ||
data.Type.toLowerCase() !== 'appointment') return;
if (typeof data.ID !== 'string' || !data.ID ||
data.ID === '%id%') return;
if (typeof data.AppointmentType !== 'string') return;
if (seenAppointments[data.ID]) return;
seenAppointments[data.ID] = true;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'AcuityEvent',
ID: data.ID,
appointmentType: data.AppointmentType,
calendar: data.Calendar
});
} catch (e) {
// Ignore unrelated or malformed messages.
}
});
})();
</script>If the scheduler appears throughout your website, use All Pages. Make sure the listener loads before the booking is completed, then save the tag.
How the listener works
window.addEventListener('message', ...) receives messages. The listener checks the sender’s origin, parses string messages, and accepts only an AcuityEvent with an appointment ID and appointment type. It then pushes the event into window.dataLayer.
The guard at the top prevents multiple listener registrations. The ID check suppresses repeated messages for the same appointment during this page load; it does not provide deduplication across reloads or devices.
A matching event name alone does not prove who sent a message. Confirm the conversion frame’s exact origin in your real setup; do not accept arbitrary origins or blindly allow null. For additional validation, check ev.source against a known frame when your embedding structure allows it. See MDN’s postMessage security guidance.
Test the Acuity appointment in GTM Preview Mode
Click Preview in GTM, enter your website URL, and connect Tag Assistant. On your website, select an appointment type, choose a date and time, enter test details, and complete the booking.
Return to Tag Assistant and look for AcuityEvent in the timeline. Select it and open the Data Layer tab. With the listener above, you should see something like:
{
event: "AcuityEvent",
ID: "123456789",
appointmentType: "Free Consultation",
calendar: "Consultations"
}The values here are examples, not real bookings. Seeing this event confirms that the message reached GTM; it does not yet confirm delivery to an analytics or advertising platform. Use an approved test appointment and remember to cancel it afterward if needed.
Create an Acuity Custom Event trigger
Go to Triggers → New → Trigger Configuration → Custom Event. Enter AcuityEvent exactly, including capitalization, choose All Custom Events, and save.
You now have a reusable GTM trigger representing the appointment message accepted by your listener.
Use AcuityEvent to trigger a conversion
Attach your new trigger to the appropriate conversion tag. Examples include a GA4 generate_lead event, a Google Ads conversion, a Meta Lead event, a LinkedIn conversion, or a TikTok event.
Configure each destination separately, including its base tag, consent settings, and required parameters. Do not forward the whole message object automatically.
Test the full path before publishing
Open GTM Preview again and complete another test booking. Select AcuityEvent. Your conversion tag should appear under Tags Fired:
AcuityEvent
Tags Fired:
GA4 - generate_lead
Or, for a configured Meta implementation:
Meta - LeadA base tag may have fired earlier on page load; it does not need to appear on this same event. Also verify receipt in the destination platform’s debugging tools. “Tags Fired” means GTM ran the tag, not that the platform necessarily accepted the conversion.
Track different Acuity appointment types
A free consultation, product demo, discovery call, and existing-client meeting may have different business value. You do not have to count all of them as the same conversion.
Create a Data Layer Variable using Variables → New → Data Layer Variable. Enter appointmentType and name it DLV - Acuity Appointment Type.
Change the relevant Custom Event trigger to Some Custom Events and add a matching condition:
Event = AcuityEvent
AND
DLV - Acuity Appointment Type equals Free ConsultationRepeat with the exact type names shown in your Data Layer tab. For example, send free consultations to your lead conversion and track existing-client meetings separately.
Use the Acuity appointment ID
Create another Data Layer Variable with the name ID and label it DLV - Acuity Appointment ID. The capitalization must match the listener.
ID: data.IDA stable appointment ID can help connect a browser event to backend data or support a destination’s deduplication mechanism. Mapping it into an arbitrary event parameter does not automatically deduplicate anything—follow that platform’s rules.
Be careful with email addresses and PII
The sender demonstrates Email: "%email%", but basic conversion tracking normally needs only the event name, ID, and appointment type. Remove the Email line from the Acuity script if you do not need it. The default listener does not push email or a customer name.
Do not send raw email addresses as ordinary GA4 event parameters or forward personal data indiscriminately to advertising platforms. If you have a permitted enhanced-conversion or CRM use case, use its dedicated user-data fields and meet the destination’s consent, formatting, and hashing requirements.
Calendar names and appointment types can also reveal sensitive information, particularly for health services. Review them before sending them to analytics. Data in a browser data layer is accessible to other scripts on the page.
For a minimal data layer event, use:
window.dataLayer.push({
event: data.event,
ID: data.ID,
appointmentType: data.AppointmentType
});Why find the top-level window and use postMessage()?
An embedded scheduler may sit directly inside your website, or inside a widget containing additional frames. Hardcoding window.parent.parent assumes exactly two parent levels. Walking through the parent hierarchy avoids that assumption.
Main website (GTM listener)
└─ Scheduler / widget frame
└─ Acuity conversion tracking frame
Conversion message → top-level website → dataLayerBrowsers restrict direct access between different origins. A normal GTM form trigger cannot simply reach inside a third-party scheduler. postMessage() gives the two contexts an explicit communication channel without reading the iframe’s internal page.
This method assumes the top-level page is your website and contains the listener. A booking opened directly on Acuity, in a new tab, or inside another top-level host needs a different integration or a deliberately chosen receiving window.
Why not just track the schedule button?
A visitor can click “Book a Call,” inspect the available dates, and leave without booking. A click measures intent, not an appointment. Here, the conversion message originates from Acuity’s completed-booking flow, providing a more meaningful signal.
Troubleshooting Acuity tracking
Confirm the Acuity script is installed
Check Custom Conversion Tracking and make sure the code and settings were saved. Verify that the example receiving origin has been replaced with your actual website origin.
Confirm the GTM listener fired
In Preview Mode, check the page event before the appointment was completed. If the Custom HTML listener did not load, it cannot receive the message.
Complete the entire booking
Opening the scheduler or selecting a date is not enough. Finish an appointment and confirm the booking in Acuity. Test an order separately to make sure it is not counted as an appointment.
Check the message origin and payload
Compare the conversion frame’s origin with the allowed origin in the listener. Check for unresolved placeholders, malformed strings, or missing IDs. Do not remove origin validation just to make the event appear.
Separate message problems from tag problems
If AcuityEvent never appears, troubleshoot the sender, window hierarchy, origin, and listener. If it appears but your conversion tag does not fire, inspect the GTM trigger and consent configuration. If the tag fires but the destination shows nothing, debug the destination request and configuration.
Check duplicate and sensitive events
Test reloads and repeated confirmation views. The example only suppresses repeat IDs in the current page session. Check existing native integrations too, so a single appointment does not send duplicate conversions. Review all outgoing fields before publishing.
Final tracking architecture
One completed booking. One reusable event.
Acuity sends the appointment message, your website validates it, and GTM decides which destinations receive a conversion. That separation makes debugging clearer and avoids rebuilding the same bridge for every marketing platform.
Conclusion
Tracking Acuity appointments takes a different approach from tracking a normal form, but the pieces are straightforward once the sender and listener agree.
- Add the Acuity conversion script and set your website’s exact origin.
- Install the GTM message listener on the page containing your scheduler.
- Complete a test appointment and verify
AcuityEvent. - Create a Custom Event trigger using that exact event name.
- Connect the GA4, Google Ads, Meta, or other conversion tags you need.
- Use appointment type and ID variables where relevant.
- Check consent, duplicates, failure paths, and destination receipt.
- Publish your GTM container once the implementation is validated.
For a visual walkthrough of the original implementation, watch my complete Acuity + GTM tutorial.
Watch the step-by-step tutorial