
Author Name
Emtiaz Hossain
Cal.com bookings don't fire a normal form submit event, the booking flow lives inside an embedded widget, and confirmation happens through Cal.com's own event system. There are two ways to hook into it depending on how the widget is embedded: Cal.com's own JS API for its native embed, or a postMessage listener for an iframe embed.
If the site uses Cal.com's own embed script (the Cal.ns[...] pattern from their official embed snippet), Cal.com exposes a direct event API to hook into.
<script>
Cal.ns["30min"]("on", {
action: "bookingSuccessful",
callback: () => {
// Replace with your tracking method
// Example: dataLayer.push({'event': 'calcom_booking_success'});
}
});
</script>Cal.ns["30min"] refers to the specific named embed instance, "30min" here is just an example matching whatever namespace was set up in the original embed snippet (this has to match exactly, it's set when the widget is first initialized on the page). The "on" method with action: "bookingSuccessful" registers a callback that fires precisely when Cal.com confirms a booking went through, not on widget open, not on form interaction, only on actual confirmed success.
Some implementations embed Cal.com as a plain iframe rather than through the native embed script. In that case, use a postMessage listener instead, following the same cross-origin pattern JotForm's iframe embed uses.
<script>
window.addEventListener("message", function(event) {
if (
event.origin === "https://app.cal.com" &&
event.data &&
event.data.originator === "CAL" &&
event.data.type === "bookingSuccessful"
) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "booking_success_v2",
calBookingData: event.data
});
}
});
</script>The three-part check on event.origin, event.data.originator, and event.data.type matters here specifically because window.addEventListener('message', ...) receives every postMessage sent to the window, from any source, not just Cal.com. Without all three conditions, the listener risks firing on unrelated cross-origin messages the browser happens to receive, a generic analytics script, a chat widget, an ad network, anything else on the page also using postMessage.
Check the embed code: Cal.com's official embed snippet (loaded via their embed.js and initialized with Cal("init", ...)) means Method 1. A plain <iframe src="https://cal.com/..."> tag with no Cal.com JS snippet means Method 2. Using the native API pattern on a raw iframe embed does nothing, since there's no Cal.ns object to attach to.
calcom_booking_success or booking_success_v2, name it consistently with the rest of the tracking setup).Discussed this on LinkedIn
Found this helpful?
Drop a comment on the LinkedIn post – I read every reply and love connecting with tracking folks.
Book a free 30-minute call. I will review your current tracking setup, show you where conversions are leaking, and tell you exactly what I would fix.
A full GA4/GTM/Google Ads audit on a UK Shopify store spending £18,000+/month found duplicate conversion tags inflating reported purchases 2x, a dropped GCLID at checkout, and misconfigured variables silently degrading Enhanced Conversions. Here's the audit, the fixes, and the migration off a legacy GTM setup before Shopify's deprecation deadline.
How a UK marketing agency's LinkedIn Insight Tag upgrade turned into a full conversion audit, catching duplicate tags and stale triggers across eight lead-gen conversions.
Duplicate form conversions in GA4 are usually a reload firing the same tag twice, not a real second submission. Here's the Custom JavaScript variable that catches it, plus other common causes to rule out.