Skill Tracer
Skills › Marketing Skills › conversion-tracking › detect-cal-com.json

detect-cal-com.json

141 lines · skills/conversion-tracking/assets/gtm-recipes/detect/detect-cal-com.json at 88c79f6

1{
2 "exportFormatVersion": 2,
3 "exportTime": "2026-10-15 00:00:00",
4 "containerVersion": {
5 "path": "accounts/0/containers/0/versions/0",
6 "accountId": "0",
7 "containerId": "0",
8 "containerVersionId": "0",
9 "container": {
10 "path": "accounts/0/containers/0",
11 "accountId": "0",
12 "containerId": "0",
13 "name": "Conversion Detection - Cal.com",
14 "publicId": "GTM-XXXXXXX",
15 "usageContext": [
16 "WEB"
17 ],
18 "fingerprint": "0",
19 "tagManagerUrl": "https://tagmanager.google.com/#/container/accounts/0/containers/0/workspaces/0"
20 },
21 "tag": [
22 {
23 "accountId": "0",
24 "containerId": "0",
25 "tagId": "1",
26 "name": "Conversion Detection - Cal.com Booking Listener",
27 "type": "html",
28 "parameter": [
29 {
30 "type": "TEMPLATE",
31 "key": "html",
32 "value": "<script>\n(function () {\n 'use strict';\n if (window.__ctDetectCalcom) return;\n window.__ctDetectCalcom = true;\n window.dataLayer = window.dataLayer || [];\n\n // Cal.com's embed code (inline, popup and floating button) renders an iframe\n // from https://app.cal.com; a booking page pasted in as a plain iframe loads\n // from https://cal.com. Each posts from its own origin, so a frame is only\n // trusted for the origin its address starts with. The CSS selectors do that\n // prefix match so this listener never reads an iframe's address, because\n // Cal.com prefill parameters can carry a name or email.\n var CAL_FRAMES = [\n { selector: 'iframe[src^=\"https://app.cal.com/\"]', origin: 'https://app.cal.com' },\n { selector: 'iframe[src^=\"https://cal.com/\"]', origin: 'https://cal.com' }\n ];\n var UID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;\n var STATUS_PATTERN = /^[A-Z_]{1,32}$/;\n var trackedFrames = [];\n var seenUids = [];\n\n function trackFrame(frame, origin) {\n if (!frame || !frame.contentWindow) return;\n for (var i = 0; i < trackedFrames.length; i++) {\n if (trackedFrames[i].frame === frame) {\n trackedFrames[i].origin = origin;\n return;\n }\n }\n trackedFrames.push({ frame: frame, origin: origin });\n }\n\n function scan(root) {\n if (!root || typeof root.querySelectorAll !== 'function') return;\n for (var k = 0; k < CAL_FRAMES.length; k++) {\n var frames;\n try { frames = root.querySelectorAll(CAL_FRAMES[k].selector); } catch (e) { continue; }\n for (var i = 0; i < frames.length; i++) trackFrame(frames[i], CAL_FRAMES[k].origin);\n }\n }\n\n // The message must come from a Cal.com frame that is on this page right now,\n // and from the origin that frame was loaded from. Cal.com re-creates popup\n // frames, so a frame seen earlier is re-checked live.\n function sourceIsTracked(message) {\n for (var i = 0; i < trackedFrames.length; i++) {\n var item = trackedFrames[i];\n try {\n if (item.frame.isConnected === true && item.frame.contentWindow === message.source && item.origin === message.origin) return true;\n } catch (e) { /* replaced or inaccessible frames fail closed */ }\n }\n return false;\n }\n\n function isCalOrigin(origin) {\n for (var k = 0; k < CAL_FRAMES.length; k++) if (CAL_FRAMES[k].origin === origin) return true;\n return false;\n }\n\n function isPlainObject(value) {\n return !!value && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]';\n }\n\n function uidSeen(uid) {\n for (var i = 0; i < seenUids.length; i++) if (seenUids[i] === uid) return true;\n return false;\n }\n\n function onMessage(message) {\n if (!message || !message.source || message.source === window) return;\n if (!isCalOrigin(message.origin)) return;\n // A frame added since the last scan (a popup that just opened) is picked up here.\n if (!sourceIsTracked(message)) {\n scan(document);\n if (!sourceIsTracked(message)) return;\n }\n var data = message.data;\n if (typeof data === 'string') {\n try { data = JSON.parse(data); } catch (e) { return; }\n }\n // Only a completed new booking. Reschedules (rescheduleBookingSuccessfulV2)\n // and the deprecated V1 events, which carry the attendee's details, never match.\n if (!isPlainObject(data) || data.originator !== 'CAL' || data.type !== 'bookingSuccessfulV2') return;\n var booking = data.data;\n if (!isPlainObject(booking) || typeof booking.uid !== 'string' || !UID_PATTERN.test(booking.uid)) return;\n if (uidSeen(booking.uid)) return;\n // Only these three values are copied. The booking title names the attendee,\n // so it and every other field are left behind.\n var push = { event: 'calcom_booking_successful', booking_uid: booking.uid };\n if (typeof booking.eventTypeId === 'number' && isFinite(booking.eventTypeId)) push.event_type_id = String(booking.eventTypeId);\n if (typeof booking.status === 'string' && STATUS_PATTERN.test(booking.status)) push.booking_status = booking.status;\n // The ID is remembered only once the push went through, so a dataLayer\n // that throws (a broken wrapper) never escapes into the page, and a\n // repeat of the same message can still be counted.\n try {\n window.dataLayer = window.dataLayer || [];\n window.dataLayer.push(push);\n } catch (e) {\n return;\n }\n seenUids.push(booking.uid);\n }\n\n function start() {\n if (typeof document === 'undefined') return;\n scan(document);\n if (typeof MutationObserver === 'undefined' || !document.documentElement) return;\n try {\n var observer = new MutationObserver(function (mutations) {\n for (var i = 0; i < mutations.length; i++) {\n var added = mutations[i].addedNodes || [];\n for (var j = 0; j < added.length; j++) {\n var node = added[j];\n if (node && node.tagName === 'IFRAME') scan(document);\n else scan(node);\n }\n if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'src') scan(document);\n }\n });\n observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] });\n } catch (e) {}\n }\n\n if (typeof document !== 'undefined') {\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, false);\n else start();\n }\n window.addEventListener('message', onMessage, false);\n})();\n</script>"
33 },
34 {
35 "type": "BOOLEAN",
36 "key": "supportDocumentWrite",
37 "value": "false"
38 }
39 ],
40 "firingTriggerId": [
41 "2147479553"
42 ],
43 "tagFiringOption": "ONCE_PER_EVENT",
44 "monitoringMetadata": {
45 "type": "MAP"
46 },
47 "consentSettings": {
48 "consentStatus": "NOT_SET"
49 }
50 }
51 ],
52 "trigger": [
53 {
54 "accountId": "0",
55 "containerId": "0",
56 "triggerId": "2",
57 "name": "CE - Cal.com Booking Successful",
58 "type": "CUSTOM_EVENT",
59 "customEventFilter": [
60 {
61 "type": "EQUALS",
62 "parameter": [
63 {
64 "type": "TEMPLATE",
65 "key": "arg0",
66 "value": "{{_event}}"
67 },
68 {
69 "type": "TEMPLATE",
70 "key": "arg1",
71 "value": "calcom_booking_successful"
72 }
73 ]
74 }
75 ],
76 "filter": []
77 }
78 ],
79 "variable": [
80 {
81 "accountId": "0",
82 "containerId": "0",
83 "variableId": "1",
84 "name": "DLV - Cal.com Booking UID",
85 "type": "v",
86 "parameter": [
87 {
88 "type": "TEMPLATE",
89 "key": "name",
90 "value": "booking_uid"
91 },
92 {
93 "type": "INTEGER",
94 "key": "dataLayerVersion",
95 "value": "2"
96 }
97 ]
98 },
99 {
100 "accountId": "0",
101 "containerId": "0",
102 "variableId": "2",
103 "name": "DLV - Cal.com Event Type ID",
104 "type": "v",
105 "parameter": [
106 {
107 "type": "TEMPLATE",
108 "key": "name",
109 "value": "event_type_id"
110 },
111 {
112 "type": "INTEGER",
113 "key": "dataLayerVersion",
114 "value": "2"
115 }
116 ]
117 },
118 {
119 "accountId": "0",
120 "containerId": "0",
121 "variableId": "3",
122 "name": "DLV - Cal.com Booking Status",
123 "type": "v",
124 "parameter": [
125 {
126 "type": "TEMPLATE",
127 "key": "name",
128 "value": "booking_status"
129 },
130 {
131 "type": "INTEGER",
132 "key": "dataLayerVersion",
133 "value": "2"
134 }
135 ]
136 }
137 ],
138 "fingerprint": "0",
139 "tagManagerUrl": "https://tagmanager.google.com/#/container/accounts/0/containers/0/versions/0"
140 }