GTM template: track Calendly bookings as GA4 events
A free GTM container that turns every Calendly booking into a GA4 event — DataLayer listener, custom HTML tag, and the one condition that keeps your conversion count honest.
Originally written in German — read it on visnakovs.de.
Calendly runs as an embed or a pop-up and sends no standard events to GA4 — so the bookings show up nowhere in your reports. This GTM template closes the gap and makes every Calendly booking measurable as a GA4 event.
What’s in the template
- 1 custom HTML tag — Calendly event listener that watches for
messageevents from the Calendly embed - 1 GA4 event tag — fires on Calendly events (booking started, booking completed)
- 1 custom event trigger — the
calendlyevent - 1 DataLayer variable —
calendly_event, carrying the event type
How it works
Calendly talks to the parent page over the postMessage API. The custom HTML tag listens for those messages and pushes them into the DataLayer:
window.addEventListener('message', function(e) {
if (e.data.event && e.data.event.indexOf('calendly') > -1) {
dataLayer.push({
event: 'calendly',
calendly_event: e.data.event
});
}
});
The GA4 event tag fires on the calendly custom event and passes the event type through as a parameter.
The Calendly events you can track
calendly.profile_page_viewed— the Calendly widget loadedcalendly.event_type_viewed— the user looked at an appointment typecalendly.date_and_time_selected— date and time pickedcalendly.event_scheduled— the appointment was booked — that’s your conversion
Setup in two minutes
1. Import the template
Import container → upload the JSON → merge.
2. Enter your GA4 measurement ID
Drop your G-XXXXXXX ID into the GA4 event tag.
3. Test it
- Turn on GTM preview mode
- Open the Calendly widget
- Book a test appointment
- Check: does the tag fire on
calendly.event_scheduled?
Count only real bookings as conversions
Add a condition to the GA4 event tag so it only fires on calendly.event_scheduled. Then the actual booking counts as a conversion — not every widget view. Skip this and your conversion column quietly inflates with people who opened the calendar and left.
Download the template
The configurator that generates this container lives on my German site: the GTM configurator (in German) bundles all seven of my setups in one tool. The interface is German, the JSON it produces is language-neutral.
Copy the JSON below, save it as a .json file and import it into your Google Tag Manager container under Admin → Import Container.
No email, no form — just take it.
Show the GTM container JSON
{
"exportFormatVersion": 2,
"exportTime": "2023-05-25 13:50:28",
"containerVersion": {
"path": "accounts/0000000000/containers/000000000/versions/0",
"accountId": "0000000000",
"containerId": "000000000",
"containerVersionId": "0",
"container": {
"path": "accounts/0000000000/containers/000000000",
"accountId": "0000000000",
"containerId": "000000000",
"name": "Calendly GA4 recipe",
"publicId": "GTM-XXXXXX",
"usageContext": [
"WEB"
],
"fingerprint": "0000000000000",
"tagManagerUrl": "https://tagmanager.google.com/#/container/accounts/0000000000/containers/000000000/workspaces?apiLink=container",
"features": {
"supportUserPermissions": true,
"supportEnvironments": true,
"supportWorkspaces": true,
"supportGtagConfigs": false,
"supportBuiltInVariables": true,
"supportClients": false,
"supportFolders": true,
"supportTags": true,
"supportTemplates": true,
"supportTriggers": true,
"supportVariables": true,
"supportVersions": true,
"supportZones": true
},
"tagIds": [
"GTM-XXXXXX"
]
},
"tag": [
{
"accountId": "0000000000",
"containerId": "000000000",
"tagId": "3",
"name": "cHTML - Calendly listener",
"type": "html",
"parameter": [
{
"type": "TEMPLATE",
"key": "html",
"value": "<script>\nwindow.dataLayer = window.dataLayer ||[];\nwindow.addEventListener('message',\n function(e) {\n if (e.data.event && e.data.event.indexOf('calendly') === 0) {\n window.dataLayer.push({\n 'event' : 'calendly',\n 'calendly_event' : e.data.event.split('.')[1]\n });\n }\n }\n);\n</script>"
},
{
"type": "BOOLEAN",
"key": "supportDocumentWrite",
"value": "false"
}
],
"fingerprint": "0000000000000",
"firingTriggerId": [
"2147479553"
],
"tagFiringOption": "ONCE_PER_EVENT",
"monitoringMetadata": {
"type": "MAP"
},
"consentSettings": {
"consentStatus": "NOT_SET"
}
},
{
"accountId": "0000000000",
"containerId": "000000000",
"tagId": "6",
"name": "GA4 event - calendar events - calendly",
"type": "gaawe",
"parameter": [
{
"type": "BOOLEAN",
"key": "sendEcommerceData",
"value": "false"
},
{
"type": "TEMPLATE",
"key": "eventName",
"value": "calendar_{{dlv - calendly_event}}"
},
{
"type": "TEMPLATE",
"key": "measurementId",
"value": "none"
},
{
"type": "TEMPLATE",
"key": "measurementIdOverride",
"value": "{{INSERT YOUR MEASUREMENT ID HERE}}"
}
],
"fingerprint": "0000000000000",
"firingTriggerId": [
"4"
],
"tagFiringOption": "ONCE_PER_EVENT",
"monitoringMetadata": {
"type": "MAP"
},
"consentSettings": {
"consentStatus": "NOT_SET"
}
}
],
"trigger": [
{
"accountId": "0000000000",
"containerId": "000000000",
"triggerId": "4",
"name": "custom - calendly",
"type": "CUSTOM_EVENT",
"customEventFilter": [
{
"type": "EQUALS",
"parameter": [
{
"type": "TEMPLATE",
"key": "arg0",
"value": "{{_event}}"
},
{
"type": "TEMPLATE",
"key": "arg1",
"value": "calendly"
}
]
}
],
"fingerprint": "0000000000000"
}
],
"variable": [
{
"accountId": "0000000000",
"containerId": "000000000",
"variableId": "5",
"name": "dlv - calendly_event",
"type": "v",
"parameter": [
{
"type": "INTEGER",
"key": "dataLayerVersion",
"value": "2"
},
{
"type": "BOOLEAN",
"key": "setDefaultValue",
"value": "false"
},
{
"type": "TEMPLATE",
"key": "name",
"value": "calendly_event"
}
],
"fingerprint": "0000000000000",
"formatValue": {}
}
],
"fingerprint": "0000000000000",
"tagManagerUrl": "https://tagmanager.google.com/#/versions/accounts/0000000000/containers/000000000/versions/0?apiLink=version"
}
}If your booking tracking is guessing rather than measuring — or you want a second pair of eyes on the setup before you trust the numbers — write to me.