Calendly
Calendly is a widely used online scheduling tool, but its built-in SMS reminder feature only supports certain countries and regions, leaving markets like Asia-Pacific, Southeast Asia, and the Middle East with limited coverage. By combining Calendly Webhooks with EngageLab SMS, you can send SMS notifications to any number globally when appointments are created or canceled, and before meetings start, filling the geographical gaps of Calendly's native capabilities.
Prerequisites
Before starting, please ensure the following configurations are complete:
On the EngageLab Side
- The EngageLab SMS service is enabled
- SMS templates have been created and approved on the Template Management page, and template IDs obtained
- API keys have been created on the API Key page, and
dev_keyanddev_secretobtained
On the Calendly Side
- You have a Calendly Standard plan or above (Webhook feature requires a paid plan)
- A Personal Access Token has been created on the Integrations & apps → API and webhooks page
On the Server Side
- You have a publicly accessible server configured with a valid HTTPS certificate
- For local development and debugging, you can use ngrok to temporarily expose local ports
Step 1: Prepare SMS Templates
Calling the API to send SMS requires pre-approved templates; direct custom text passing is not supported.
Log in to the EngageLab Console, go to SMS → Template Management, and create the following three templates based on the scenario:
| Template Purpose | Template Content Example |
|---|---|
| Appointment Confirmation | Hello {{name}}, your meeting is confirmed for {{time}}. Looking forward to seeing you. |
| Cancellation Notification | Hello {{name}}, your meeting appointment has been canceled. Please contact us if you need to reschedule. |
| Meeting Reminder | Hello {{name}}, you have a meeting starting in {{advance}} at {{time}}. Please prepare in advance. |
After submitting the templates, wait for approval and record the three template IDs.
Recommendation: Creating independent templates for each scenario ensures clearer semantics and higher approval rates. If the template contains custom variables (e.g.,
{{name}}), you must pass values via theparamsfield when calling the API, otherwise, the variables will be delivered as-is.
Step 2: Create Webhook Subscription in Calendly
Calendly's Webhook management does not have a visual interface and must be created via API.
Get Organization URI
First, call the following interface to get the organization URI of the current account:
curl https://api.calendly.com/users/me \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
Find the current_organization field in the response, formatted as follows:
https://api.calendly.com/organizations/xxxxxxxxxxxxxxxx
Create Webhook Subscription
Use the organization URI to create a Webhook subscribing to 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-organization-ID",
"scope": "organization"
}'
Once successfully created, Calendly will push a verification request to the provided url, and the server must return HTTP 200 to complete the handshake. Subsequently, every time someone schedules or cancels, an event will be pushed to this address.
Note: The
urlmust be a publicly accessible HTTPS address. During local development, use ngrok to generate a temporary address:ngrok http 3000, and fill in the outputhttps://xxxx.ngrok.io.
Step 3: Set Up Webhook Receiving Service
Below is a complete Node.js server-side example that receives Calendly Webhook events and calls the EngageLab SMS API to send an SMS.
Install Dependencies
npm install express
Complete 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 SMS
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 mean successful sending; check the code field
if (data.code && data.code !== 0) {
console.error(`SMS send failed: code=${data.code}, message=${data.message}`);
} else {
console.log(`SMS sent successfully: plan_id=${data.plan_id}, message_id=${data.message_id}`);
}
}
// Convert 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',
});
}
// Register pre-meeting reminder tasks (one at 24h and one at 1h)
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 Receiving Endpoint
app.post('/webhooks/calendly', async (req, res) => {
const { event, payload } = req.body;
const phone = payload.text_reminder_number; // Phone number provided by user during booking
const name = payload.invitee?.name ?? 'User';
const startTime = payload.scheduled_event?.start_time;
// Skip sending SMS if phone number is not 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) },
});
// Register pre-meeting reminder tasks
scheduleReminders({ phone, name, startTime });
} else if (event === 'invitee.canceled') {
// Send cancellation notification SMS
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CANCEL,
params: { name },
});
}
// Must return 200 within 2 seconds, otherwise Calendly considers the push failed and retries
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 and fill in 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_notification_template_id
TEMPLATE_ID_REMINDER=meeting_reminder_template_id
Key Field Descriptions
In the payload pushed by the Calendly Webhook, the following fields are directly related to SMS sending:
| Field | Description |
|---|---|
event |
Event type, invitee.created (appointment creation) or invitee.canceled (appointment cancellation) |
payload.text_reminder_number |
Phone number filled out by the invitee, including country code; may be empty |
payload.invitee.name |
Invitee's name |
payload.scheduled_event.start_time |
Meeting start time, ISO 8601 format |
Extended Scenarios
Send Follow-up SMS After Meeting
Add a task triggered after the meeting to scheduleReminders to send a satisfaction survey or next 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 Host Simultaneously
When an appointment is created, besides notifying the invitee, a reminder can also be sent to the meeting host:
if (event === 'invitee.created') {
// Notify invitee
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CONFIRM,
params: { name, time: formatTime(startTime) },
});
// Notify host
await sendSMS({
to: process.env.HOST_PHONE,
templateId: process.env.TEMPLATE_ID_HOST_NOTIFY,
params: { name, time: formatTime(startTime) },
});
}
Notes
- Webhook must respond with
HTTP 200within 2 seconds, otherwise, Calendly will consider the push failed and retry. It is recommended to return 200 first, and then execute the SMS sending logic asynchronously to prevent Calendly from misjudging due to EngageLab API response delays. - The
text_reminder_numberfield may be empty, so you need to handle null checks in your code to avoid passing an empty number to the SMS API and causing errors. - The phone number format must include the country code, such as
+6591234567for a Singapore number. Calendly guides users to fill in international formats when collecting phone numbers, but it's recommended to do a format check on the server side. setTimeoutis not suitable for production environments, as all scheduled tasks will be lost if the server restarts. In a production environment, it is recommended to use persistent task queues (like BullMQ + Redis) or cloud service cron jobs to store reminder records in a database so they can be restored upon restart.- Templates must be approved before they can be used. If the template is pending review or rejected when called, the API will return a
4001error. - An HTTP 200 response from the API does not mean the SMS was sent successfully. Please check the
codefield in the response body. If it is non-zero, refer to the Error Code Instructions for troubleshooting.










