If you want to fix your Calendly tracking and capture full user details like Name, Email, Phone Number, then this guide is exactly for you.
Most people embed Calendly but donโt get full booking data inside their tracking tools โ Meta Pixel, CAPI, Google Tags, and GTM fail to receive key information.
A missing DataLayer = broken tracking.
In this short guide, I will show you:
โ What a DataLayer is
โ Why you must enable it for Calendly
โ The benefits youโll get
โ The custom JavaScript code (below)
โ A video tutorial so you can follow step-by-step
๐ฅ Watch the Full Video Video To Enable DataLayer
<script>
(function() {
window.addEventListener("message", function (e) {
if (!e.data || e.data.event !== "calendly.event_scheduled") return;
var inviteeUrl = e.data.payload && e.data.payload.invitee ? e.data.payload.invitee.uri : null;
if (!inviteeUrl) return;
var token = "{{calendly access token}}";
var headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
};
var PHONE_SEPARATORS = "()._-+/ ";
// Helper functions matching server-side logic
function toPhoneNumber(val, strict) {
if (typeof val !== "string") return undefined;
var digits = "0123456789";
var hasLeadingPlus = val.trim().indexOf("+") === 0;
var cleaned = "";
var onlyPhoneChars = true;
for (var i = 0; i < val.length; i++) {
var ch = val.charAt(i);
if (digits.indexOf(ch) !== -1) {
cleaned += ch;
} else if (PHONE_SEPARATORS.indexOf(ch) === -1) {
onlyPhoneChars = false;
}
}
if (strict && !onlyPhoneChars) return undefined;
if (cleaned.length < 7 || cleaned.length > 15) return undefined;
return hasLeadingPlus ? "+" + cleaned : cleaned;
}
function buildAnswerMap(qAndA) {
var map = {};
if (!qAndA || !Array.isArray(qAndA)) return map;
qAndA.forEach(function (item) {
if (typeof item.question !== "string") return;
var q = item.question.trim();
var a = typeof item.answer === "string" ? item.answer.trim() : item.answer;
if (q && a !== undefined && a !== null && map[q] === undefined) {
map[q] = a;
}
});
return map;
}
function getEventUrl(uri) {
var parts = uri.split("/");
if (parts.length < 5 || parts[3] !== "scheduled_events" || !parts[4]) return null;
return "https://api.calendly.com/scheduled_events/" + parts[4];
}
function fetchCalendly(url) {
return fetch(url, { method: "GET", headers: headers })
.then(function (res) {
return res.ok ? res.json() : null;
})
.then(function (json) {
return json && json.resource ? json.resource : null;
})
.catch(function () {
return null;
});
}
var eventUrl = getEventUrl(inviteeUrl);
// Fetch both Invitee and Event in parallel
Promise.all([
fetchCalendly(inviteeUrl),
eventUrl ? fetchCalendly(eventUrl) : Promise.resolve(null)
]).then(function (results) {
var invitee = results[0];
var eventData = results[1];
if (!invitee) return;
var routingPromise = invitee.routing_form_submission
? fetchCalendly(invitee.routing_form_submission)
: Promise.resolve(null);
routingPromise.then(function (submission) {
var payload = {
event: "calendly_booked"
};
// 1. Meeting details
if (eventData) {
if (eventData.name) payload.name = eventData.name;
if (eventData.meeting_notes_plain) payload.meeting_notes_plain = eventData.meeting_notes_plain;
if (eventData.event_guests) payload.event_guests = eventData.event_guests;
if (eventData.invitees_counter) payload.invitees_counter = eventData.invitees_counter;
if (eventData.start_time) payload.start_time = eventData.start_time;
if (eventData.end_time) payload.end_time = eventData.end_time;
if (eventData.created_at) payload.event_created_at = eventData.created_at;
if (eventData.updated_at) payload.event_updated_at = eventData.updated_at;
if (eventData.location) payload.location = eventData.location;
}
// 2. Invitee details
if (invitee.timezone) payload.timezone = invitee.timezone;
if (invitee.scheduling_method) payload.scheduling_method = invitee.scheduling_method;
if (invitee.created_at) payload.created_at = invitee.created_at;
if (invitee.updated_at) payload.updated_at = invitee.updated_at;
// User Data Structure
var userData = {};
var address = {};
if (invitee.email) {
var cleanEmail = invitee.email.trim().toLowerCase();
if (cleanEmail && cleanEmail.indexOf("@") > 0) {
userData.email_address = cleanEmail;
}
}
// Names
if (invitee.name) {
var parts = invitee.name.trim().split(/\s+/);
if (parts.length > 0) {
address.first_name = parts[0];
if (parts.length > 1) {
address.last_name = parts.slice(1).join(" ");
}
}
}
if (invitee.first_name) address.first_name = invitee.first_name;
if (invitee.last_name) address.last_name = invitee.last_name;
if (Object.keys(address).length > 0) userData.address = address;
// Form Q&A and Phone Parsing
if (invitee.questions_and_answers && invitee.questions_and_answers.length > 0) {
payload.event_questions = [];
payload.event_answers = [];
invitee.questions_and_answers.forEach(function (qa) {
payload.event_questions.push(qa.question);
payload.event_answers.push(qa.answer);
if (!userData.phone_number) {
var extractedPhone = toPhoneNumber(qa.answer, true);
if (extractedPhone) userData.phone_number = extractedPhone;
}
});
var eventAnsMap = buildAnswerMap(invitee.questions_and_answers);
if (Object.keys(eventAnsMap).length > 0) {
payload.event_answers_by_question = eventAnsMap;
}
}
// SMS Reminder Phone Priority
if (invitee.text_reminder_number) {
var smsPhone = toPhoneNumber(invitee.text_reminder_number, false);
if (smsPhone) userData.phone_number = smsPhone;
}
if (Object.keys(userData).length > 0) payload.user_data = userData;
// 3. Tracking (UTMs)
if (invitee.tracking) {
var tracking = {};
var keys = ["utm_campaign", "utm_source", "utm_medium", "utm_content", "utm_term", "salesforce_uuid"];
keys.forEach(function (k) {
if (invitee.tracking[k]) tracking[k] = invitee.tracking[k];
});
if (Object.keys(tracking).length > 0) payload.tracking = tracking;
}
// 4. Payment Data
if (invitee.payment) {
var pay = invitee.payment;
var details = {};
Object.keys(pay).forEach(function (k) {
if (k !== "terms" && pay[k] !== null) details[k] = pay[k];
});
if (Object.keys(details).length > 0) payload.payment = details;
if (pay.external_id) payload.transaction_id = pay.external_id;
if (typeof pay.amount === "number") payload.value = pay.amount;
if (pay.currency) payload.currency = pay.currency;
if (typeof pay.successful === "boolean") payload.payment_successful = pay.successful;
}
// 5. Routing Form Data
if (submission && submission.questions_and_answers) {
payload.routing_form_questions = [];
payload.routing_form_answers = [];
submission.questions_and_answers.forEach(function (qa) {
payload.routing_form_questions.push(qa.question);
payload.routing_form_answers.push(qa.answer);
});
var routeAnsMap = buildAnswerMap(submission.questions_and_answers);
if (Object.keys(routeAnsMap).length > 0) {
payload.routing_form_answers_by_question = routeAnsMap;
}
}
// Push unified payload to dataLayer
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(payload);
});
});
});
})();
</script>
๐ Calendly Form Event Tracking Video
This Video covers everything you need to track Calendly bookings using Google Tag Manager (GTM) for both browser-side Pixel tracking and server-side CAPI tracking. Ideal for improving your event match quality, event coverage, deduplication, and ad optimization!
โ Key topics covered:
00:00 Intro & Why Accurate Tracking Matters
01:15 Connect Google Tag Manager to WordPress
03:10 Enable Calendly DataLayer Push
05:35 Generate Calendly Personal Access Token
06:55 Replace Token in GTM Variables
08:40 Set Up Meta Pixel PageView Event in GTM
10:30 Set Up Calendly Booking Custom Event in GTM
12:20 Pass User Info: Email, Phone, Name, External ID
14:15 Test Pixel Events with Meta Event Manager
15:30 Why Server-Side CAPI is Important
16:45 Set Up Server Container in Stape.io
19:10 Connect Stape.io Subdomain or Use Default URL
21:00 Set Up Custom Loader for AdBlocker Bypass
23:00 Create GA4 Measurement ID for Testing
25:00 Pass External IDs for Deduplication
26:45 Set Up Conversion API Tags in Server GTM
29:20 Testing Server-Side Events in Event Manager
32:10 Final Testing, Troubleshooting & Publishing
34:30 Important: Remove Test Event Codes Before Ads
35:00 Service Offer, Free Advice & How to Contact Me