Field notes Field note 01
Calendly Conversion Tracking in GA4 and Google Ads via GTM
Calendly conversion tracking, step by step: listen for calendly.event_scheduled in GTM, send one clean GA4 key event, import it to Google Ads, and test it.
· 7 min read
Calendly conversion tracking is the most common gap we see on service business sites. The booking happens inside an embedded widget, the visitor never reaches a thank-you page, and so GA4 and Google Ads record nothing. You pay for the click, a real person books a call, and your reports say zero.
The fix is small. Calendly's embed tells your page when a booking is made, and Google Tag Manager (GTM) can listen for that message. This guide shows the full setup: the listener, the trigger, the GA4 event, the Google Ads conversion, de-duplication and testing.
Why Calendly bookings go untracked
Standard tracking relies on page views. A visitor lands on /contact, picks a slot in the Calendly widget, and confirms. The URL never changes, because the widget runs in an iframe that belongs to Calendly. Your page has no idea a booking happened.
Calendly does send a signal, though. Its embed uses window.postMessage() to notify the parent page. According to Calendly's advanced embed documentation, the events are:
calendly.profile_page_viewedcalendly.event_type_viewedcalendly.date_and_time_selectedcalendly.event_scheduled
The one that matters for conversions is calendly.event_scheduled. It fires after the invitee confirms the booking, not when they merely pick a time. Using date_and_time_selected instead would count people who abandon the last step.
One limit to know up front: Calendly states that plain iframes do not support event tracking via postMessage. You need Calendly's own embed code (the inline widget, popup widget or popup text), not a bare <iframe> you pasted in. If your site uses a bare iframe, swap it for the official embed first.
Calendly conversion tracking: what you will build
- A custom HTML tag in GTM that listens for the message and pushes a clean event to the dataLayer.
- A Custom Event trigger that fires on that dataLayer event.
- A GA4 event tag, marked as a key event in GA4.
- A Google Ads conversion, either imported from GA4 or fired directly.
Keeping the listener separate from the GA4 tag matters. The listener only reports what happened on the page. The GA4 and Ads tags decide what to do with it, and they respect consent.
Step 1: Add the postMessage listener in GTM
In GTM, create a new tag: Custom HTML. Paste this:
<script>
(function () {
window.dataLayer = window.dataLayer || [];
var locked = false;
window.addEventListener("message", function (e) {
// Only trust messages from Calendly
if (e.origin !== "https://calendly.com") return;
if (!e.data || typeof e.data.event !== "string") return;
if (e.data.event !== "calendly.event_scheduled") return;
// De-duplicate: ignore repeats within 5 seconds
if (locked) return;
locked = true;
setTimeout(function () { locked = false; }, 5000);
window.dataLayer.push({
event: "calendly_booking",
booking_source: "calendly_embed"
});
});
})();
</script>
Set the trigger to Initialization - All Pages or All Pages, and limit it to the pages that carry the widget if you prefer. Name the tag something obvious, such as HTML - Calendly listener.
Three details in that code are deliberate:
- Origin check. Any script on the page can post a message. Checking
e.originmeans only Calendly's domain can trigger your conversion. - No personal data. Calendly's message payload includes URIs for the event and invitee. We do not forward them. Names and emails should never go into the dataLayer or GA4, which prohibits sending personally identifiable information.
- Fixed event name.
calendly_bookingis your own name. It does not change if Calendly changes its payload.
If you want to see exactly what Calendly sends, temporarily add console.log(e.data) before the filters and make a test booking. Remove it afterwards.
Step 2: Create the trigger
Create a trigger of type Custom Event (see GTM custom event triggers):
- Event name:
calendly_booking - This trigger fires on: All Custom Events
Name it CE - calendly_booking. The match is exact by default, so spelling must be identical to the dataLayer push.
Step 3: Send the booking to GA4
Create a Google Analytics: GA4 Event tag:
- Configuration tag or measurement ID: your existing GA4 setup
- Event name:
generate_lead(a recommended GA4 event, which keeps reports consistent) orcalendly_bookingif you want to separate bookings from form leads - Event parameter:
booking_sourcewith the value{{DLV - booking_source}}(a Data Layer Variable you create for the keybooking_source) - Trigger:
CE - calendly_booking
Then mark the event as a key event in GA4. Open Admin, Data display, Events, find the event once it has arrived, and switch on Mark as key event. Google's page on key events in GA4 explains where the setting lives. The event only appears in that list after it has been received at least once, so run your first test booking before looking for it.
Step 4: Count it in Google Ads
You have two routes. Pick one and do not run both for the same action, or you will double count.
Route A: import the GA4 key event. In Google Ads, go to Goals, create a conversion action, choose Import, and select Google Analytics 4 properties. The key event appears after GA4 has seen it. This is the least work, and Ads and GA4 then agree by construction. The trade-off is a delay: imported conversions can take a while to show up.
Route B: fire an Ads conversion tag. In GTM, create a Google Ads Conversion Tracking tag with your Conversion ID and Label, triggered by CE - calendly_booking. Make sure a Conversion Linker tag fires on all pages. This is faster and works well if you want Ads to own its own count.
Either way, give the conversion a clear name such as "Booked call (Calendly)", and set it to count one conversion per click, since one person booking twice from one ad click is still one lead.
Step 5: Respect consent
Your listener tag only pushes an event to the dataLayer, which is not a measurement. The GA4 and Ads tags are the ones that must follow consent. In each tag's settings, check Consent settings and require analytics_storage for GA4 and ad_storage plus ad_user_data for Ads. If you use Consent Mode v2, those requirements are enforced for you, and Google's tags adapt to the visitor's choice. Our guide to Consent Mode v2 covers the setup.
De-duplication: three ways to double count
Double counted bookings are the usual failure after the first version works. Check each:
- Two listeners. The Calendly embed sometimes exists in GTM and also in a plugin or theme snippet. Search your page source for
calendly.event_scheduled. You want one listener. - Listener tag firing twice. If the Custom HTML tag fires on both All Pages and a Window Loaded trigger, it registers two listeners and every booking pushes twice. The five second lock in the code above catches this, but fix the cause too.
- Ads import plus Ads tag. As covered above, use one route.
If you have a single page app where the listener could be added on every route change, set the tag to fire once per page load, not once per history change.
Step 6: Test it end to end
Use a real test booking. You can cancel it straight after.
- Open Tag Assistant, enter your site URL and connect.
- In the connected tab, scroll to the Calendly widget and complete a booking with a throwaway email.
- In the Tag Assistant summary, look for a
calendly_bookingevent in the left column. Click it. The listener tag should show under Tags Fired, and the GA4 tag should fire on that same event. - Open GA4 Admin, DebugView. Your test device appears when GTM preview is active. Confirm the event arrived once, with the
booking_sourceparameter. - If you used Route B, check the Google Ads tag also fired in Tag Assistant, and that the Conversion Linker ran earlier in the session.
If you see the message but no calendly_booking event, the usual cause is that e.origin does not match. Log it and compare. If the dataLayer event exists but GA4 stays silent, check the tag's consent settings and the trigger name.
Remember that reports lag. Real-time and DebugView show the event within seconds, but standard reports and Google Ads conversion columns take longer, and Ads may take hours to show an imported conversion.
When this approach does not apply
- Bare iframe embeds. No messages are sent, as noted above. Use the official embed code.
- Calendly links that open in a new tab. If a button just links to your Calendly page, the booking happens on calendly.com, outside your site. Your GTM container is not there. Calendly offers a redirect to your own page after booking on paid plans, which lets you track a thank-you page view. Check your plan's settings.
- Embeds inside another tool. If a landing page builder or a form tool wraps the widget, the message may not reach the top window where GTM runs. Test it in Tag Assistant first.
Quick checklist
- Official Calendly embed in place, no bare iframe
- One Custom HTML listener, origin checked, locked against repeats
- Custom Event trigger on
calendly_booking - GA4 event tag with consent requirements, marked as key event
- Ads conversion by import or tag, not both
- Test booking seen once in Tag Assistant and DebugView
Done properly, every booked call becomes a number you can trust in GA4 and in Google Ads, which is what lets you tell which campaigns actually fill the calendar.