Track Calendly Bookings
Calendly bookings are not form submissions strictly speaking, but they are the conversion event for any service-business site using Calendly for discovery calls. Calendly’s widget posts a message to the parent window on every step of the booking flow. This recipe captures the scheduled-event signal, filters by origin, and sends a GA4 booking_scheduled.
Valid as of April 2026, Calendly inline embed v2.
The success signal
Section titled “The success signal”Calendly’s embed posts messages to the parent window on these events:
| Event name | Meaning | Use for conversions? |
|---|---|---|
calendly.profile_page_viewed | Visitor opened the Calendly profile | No |
calendly.event_type_viewed | Visitor selected a meeting type | No |
calendly.date_and_time_selected | Visitor picked a time slot | Funnel step |
calendly.event_scheduled | Booking confirmed | Yes |
Only calendly.event_scheduled is the conversion. The other three are useful for a booking funnel (which many teams build: views → slot-selected → scheduled).
dataLayer push pattern
Section titled “dataLayer push pattern”Filters by Calendly origin before trusting the payload.
(function () { function isCalendlyEvent(event) { return event.origin === 'https://calendly.com' && event.data && typeof event.data.event === 'string' && event.data.event.indexOf('calendly.') === 0; }
window.addEventListener('message', function (event) { if (!isCalendlyEvent(event)) return;
var payload = event.data.payload || {};
if (event.data.event === 'calendly.date_and_time_selected') { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'booking_time_selected', booking_vendor: 'calendly' }); return; }
if (event.data.event === 'calendly.event_scheduled') { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'booking_scheduled', booking_vendor: 'calendly', calendly_event_uri: payload.event && payload.event.uri, calendly_invitee_uri: payload.invitee && payload.invitee.uri }); } });})();The URIs look like:
https://api.calendly.com/scheduled_events/GBGBDCAADAEDCRZ2https://api.calendly.com/scheduled_events/GBGBDCAADAEDCRZ2/invitees/A1B2C3D4E5Both are queryable against the Calendly API for full event metadata including invitee email, meeting URL, and cancellation status.
GTM setup
Section titled “GTM setup”-
Create a Custom Event trigger for scheduled bookings
- Trigger type: Custom Event
- Event name:
booking_scheduled - This fires: Some Custom Events →
DLV - booking_vendorequalscalendly
-
Create a Custom Event trigger for slot selection (optional, for funnel)
- Event name:
booking_time_selected - Same vendor filter
- Event name:
-
Create Data Layer Variables
DLV - booking_vendor→booking_vendorDLV - calendly_event_uri→calendly_event_uriDLV - calendly_invitee_uri→calendly_invitee_uri
-
Create a GA4 Event Tag
- Event name:
booking_scheduled - Parameters:
booking_vendor→{{DLV - booking_vendor}}calendly_event_uri→{{DLV - calendly_event_uri}}calendly_invitee_uri→{{DLV - calendly_invitee_uri}}
- Trigger: the
booking_scheduledtrigger
- Event name:
-
Optionally create a
booking_time_selectedGA4 tag for the funnel.
GA4 - booking_scheduled (Calendly)
- Type
- Google Analytics: GA4 Event
- Trigger
- Custom Event - booking_scheduled (calendly)
- Variables
-
DLV - booking_vendorDLV - calendly_event_uriDLV - calendly_invitee_uri
Inline vs popup widgets
Section titled “Inline vs popup widgets”Both widget types post identical messages. The only difference is that popup widgets are inserted dynamically — the listener on window is bound once and handles both.
<!-- Inline widget --><div class="calendly-inline-widget" data-url="https://calendly.com/acme/discovery" style="min-width:320px;height:700px;"></div>
<!-- Popup --><a href="" onclick="Calendly.initPopupWidget({url: 'https://calendly.com/acme/discovery'}); return false;"> Schedule a call</a>
<script src="https://assets.calendly.com/assets/external/widget.js"></script>No special handling per type — your existing listener handles both.
Enriching with Calendly API data (server-side)
Section titled “Enriching with Calendly API data (server-side)”The dataLayer event only has URIs, not meeting details. To enrich GA4 with meeting duration, invitee email, or meeting type, route the event through a server:
- Use the
calendly_event_urias a client-side marker. - Server-side (via GA4 Measurement Protocol or a webhook endpoint), call the Calendly API
GET /scheduled_events/{uuid}with a Calendly personal access token. - Enrich and send a server-side
booking_scheduled_enrichedevent with sanitised fields.
Test it
Section titled “Test it”- Open GTM Preview mode on a page with a Calendly embed.
- Open the Calendly widget and select a date and time — confirm
booking_time_selectedappears in the Summary pane. - Complete the booking with a test email. Confirm
booking_scheduledappears with populatedcalendly_event_uriandcalendly_invitee_uri. - Verify Tags Fired lists your GA4 tag.
- In GA4 DebugView, confirm
booking_scheduledarrives within 15 seconds. - Copy the
calendly_event_urifrom the Variables tab and verify via the Calendly API that the scheduled event exists (or check the Calendly dashboard’s Scheduled Events list). - Test cancellation: after booking, cancel the meeting via the invitee link. Calendly does not post a cancellation message to the parent — cancellation is detected server-side only (via webhooks).
Common gotchas
Section titled “Common gotchas”Skipping the origin check. Any iframe on the page can post to the parent. An event.origin === 'https://calendly.com' check is mandatory — otherwise ads or third-party widgets could spoof your conversions.
Localhost development. Calendly’s postMessage is only dispatched when the parent URL is allowlisted in Calendly’s embed settings. For local dev, use 127.0.0.1 with a tunnel (ngrok, Cloudflare Tunnel) and add the tunnel URL to the allowlist.
event.data.payload is undefined on older widget versions. The v2 widget (current as of April 2026) always includes payload. If you see undefined URIs, check which widget.js version is loaded.
Single-page-app navigation. If your site is an SPA and the Calendly embed unmounts/remounts on route changes, the window.addEventListener('message', ...) listener persists — do not re-bind on route change or you will double-fire.
Popup closes before event fires. The popup posts calendly.event_scheduled just before closing, and closes the iframe shortly after. Your listener runs synchronously on message receipt, so the dataLayer push completes before the iframe is destroyed. No race condition.
Embedded on a page with strict CSP. If your Content Security Policy blocks frame-src https://calendly.com, the widget never loads and no events fire. Check the browser console for CSP violations before debugging the tracking.