Calendly
Calendly is a widely used online scheduling tool, but its built-in SMS reminder feature only supports a limited number of countries and regions, with limited coverage in Asia-Pacific, Southeast Asia, the Middle East, and other markets. By integrating Calendly Webhooks with EngageLab SMS, you can send SMS notifications to any phone number worldwide when an appointment is created, canceled, or before a meeting begins — filling the geographic gaps in Calendly's native capabilities.
Prerequisites
Before getting started, make sure the following configurations are complete:
EngageLab Side
- EngageLab SMS service has been activated
- An SMS template has been created and approved in the Template Management page; template ID has been obtained
- An API key has been created on the API Keys page;
dev_keyanddev_secrethave been obtained
Calendly Side
- You have a Calendly Standard plan or higher (Webhook functionality requires a paid plan)
- A Personal Access Token has been created on the Integrations & apps → API and webhooks page
Server Side
- You have a server with public internet access and a valid HTTPS certificate configured
- For local development and debugging, you can use ngrok to temporarily expose a local port
Step 1: Prepare SMS Templates
Sending SMS via the API requires pre-approved templates; custom text content cannot be sent directly.
Log in to the EngageLab console, go to SMS → Template Management, and create the following three templates for each scenario:
| Template Purpose | Example Content |
|---|---|
| Appointment Confirmation | Hi {{name}}, your meeting has been confirmed for {{time}}. Looking forward to seeing you. |
| Cancellation Notice | Hi {{name}}, your meeting appointment has been canceled. Please contact us to reschedule. |
| Meeting Reminder | Hi {{name}}, you have a meeting starting in {{advance}}, scheduled for {{time}}. Please prepare in advance. |
After submitting the templates, wait for approval and note down the three template IDs.
Tip: Create a separate template for each scenario — it makes the intent clearer and improves the approval rate. If a template contains custom variables (e.g.
{{name}}), you must pass values via theparamsfield when calling the API; otherwise, the variables will be sent as-is.
Step 2: Create a Webhook Subscription in Calendly
Calendly does not provide a visual interface for Webhook management; subscriptions must be created via the API.
Get the Organization URI
First, call the following endpoint to get the organization URI for your account:
curl https://api.calendly.com/users/me \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
Find the current_organization field in the response, which looks like:
https://api.calendly.com/organizations/xxxxxxxxxxxxxxxx
Create the Webhook Subscription
Use the organization URI to create a Webhook that subscribes to the appointment creation and cancellation events:
curl -X POST https://api.calendly.com/webhook_subscriptions \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server-address/webhooks/calendly",
"events": [
"invitee.created",
"invitee.canceled"
],
"organization": "https://api.calendly.com/organizations/your-org-id",
"scope": "organization"
}'
Once created, Calendly will send a verification request to the provided url, and your server must return HTTP 200 to complete the handshake. Subsequent appointment and cancellation events will be pushed to this address.
Note: The
urlmust be a publicly accessible HTTPS address. For local development, you can use ngrok to generate a temporary address:ngrok http 3000, then use the outputhttps://xxxx.ngrok.ioURL.
Step 3: Build the Webhook Receiver Service
Below is a complete Node.js server example that receives Calendly Webhook events and calls the EngageLab SMS API to send messages.
Install Dependencies
npm install express
Full Code
// server.js
import express from 'express';
const app = express();
app.use(express.json());
// EngageLab authentication: base64(dev_key:dev_secret)
const ENGAGELAB_AUTH = Buffer.from(
`${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}`
).toString('base64');
// Call EngageLab SMS API to send a message
async function sendSMS({ to, templateId, params }) {
const res = await fetch('https://smsapi.engagelab.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${ENGAGELAB_AUTH}`,
},
body: JSON.stringify({
to: [to],
template: {
id: templateId,
params,
},
}),
});
const data = await res.json();
// HTTP 200 does not guarantee delivery; check the code field
if (data.code && data.code !== 0) {
console.error(`SMS sending failed: code=${data.code}, message=${data.message}`);
} else {
console.log(`SMS sent successfully: plan_id=${data.plan_id}, message_id=${data.message_id}`);
}
}
// Convert an ISO time string to local time (Asia/Shanghai)
function formatTime(isoString) {
return new Date(isoString).toLocaleString('en-US', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
// Schedule pre-meeting reminders (24h and 1h before)
function scheduleReminders({ phone, name, startTime }) {
const meetingTime = new Date(startTime).getTime();
const now = Date.now();
const reminders = [
{ advance: '24 hours', triggerAt: meetingTime - 24 * 60 * 60 * 1000 },
{ advance: '1 hour', triggerAt: meetingTime - 60 * 60 * 1000 },
];
for (const { advance, triggerAt } of reminders) {
const delay = triggerAt - now;
if (delay <= 0) continue; // Trigger time has passed, skip
setTimeout(async () => {
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_REMINDER,
params: { name, time: formatTime(startTime), advance },
});
}, delay);
}
}
// Webhook receiver endpoint
app.post('/webhooks/calendly', async (req, res) => {
const { event, payload } = req.body;
const phone = payload.text_reminder_number; // Phone number provided by the invitee
const name = payload.invitee?.name ?? 'User';
const startTime = payload.scheduled_event?.start_time;
// Skip SMS if no phone number is provided
if (!phone) {
console.log('No phone number provided for this appointment, skipping SMS notification');
return res.sendStatus(200);
}
if (event === 'invitee.created') {
// Send appointment confirmation SMS
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CONFIRM,
params: { name, time: formatTime(startTime) },
});
// Schedule pre-meeting reminders
scheduleReminders({ phone, name, startTime });
} else if (event === 'invitee.canceled') {
// Send cancellation notice SMS
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CANCEL,
params: { name },
});
}
// Must return 200 within 2 seconds, otherwise Calendly will consider the push failed and retry
res.sendStatus(200);
});
app.listen(3000, () => console.log('Server running on port 3000'));
Environment Variable Configuration
Create a .env file in the project root directory with the following variables:
ENGAGELAB_DEV_KEY=your_dev_key
ENGAGELAB_DEV_SECRET=your_dev_secret
TEMPLATE_ID_CONFIRM=appointment_confirmation_template_id
TEMPLATE_ID_CANCEL=cancellation_notice_template_id
TEMPLATE_ID_REMINDER=meeting_reminder_template_id
Key Field Reference
The following fields in the Calendly Webhook payload are directly related to SMS sending:
| Field | Description |
|---|---|
event |
Event type: invitee.created (appointment created) or invitee.canceled (appointment canceled) |
payload.text_reminder_number |
Phone number provided by the invitee, including country code; may be empty |
payload.invitee.name |
Invitee's name |
payload.scheduled_event.start_time |
Meeting start time in ISO 8601 format |
Extended Scenarios
Send Follow-up SMS After a Meeting
Add a post-meeting trigger in scheduleReminders to send a satisfaction survey or follow-up appointment invitation:
// Append to the scheduleReminders function
const followUpAt = meetingTime + 30 * 60 * 1000; // 30 minutes after the meeting ends
const followUpDelay = followUpAt - now;
if (followUpDelay > 0) {
setTimeout(async () => {
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_FOLLOWUP,
params: { name },
});
}, followUpDelay);
}
Notify the Host As Well
When an appointment is created, in addition to notifying the invitee, you can also send a reminder to the meeting host:
if (event === 'invitee.created') {
// Notify the invitee
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CONFIRM,
params: { name, time: formatTime(startTime) },
});
// Notify the host
await sendSMS({
to: process.env.HOST_PHONE,
templateId: process.env.TEMPLATE_ID_HOST_NOTIFY,
params: { name, time: formatTime(startTime) },
});
}
Important Notes
- Webhooks must respond with
HTTP 200within 2 seconds, otherwise Calendly will consider the push failed and retry. It is recommended to return 200 first, then execute the SMS sending logic asynchronously to avoid Calendly misjudging due to EngageLab API response latency. - The
text_reminder_numberfield may be empty — add null checks in your code to avoid passing an empty number to the SMS API, which would cause errors. - Phone numbers must include the country code, e.g.
+8618701235678for mainland China numbers. Calendly prompts users to enter international format when collecting phone numbers, but server-side format validation is recommended. setTimeoutis not suitable for production environments — all scheduled tasks will be lost when the server restarts. For production, use a persistent task queue (such as BullMQ + Redis) or cloud-based scheduled tasks, and store reminder records in a database for recovery after restarts.- Templates must be approved before use — if a template is pending review or has been rejected when called, the API will return a
4001error. - An HTTP 200 response from the API does not guarantee successful SMS delivery — check the
codefield in the response body. Refer to the error code documentation for non-zero values.
