Logo Site EngageLab Mark Colored TransparentDocument
Search

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_key and dev_secret have 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 the params field 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"
              
              curl https://api.calendly.com/users/me \
  -H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"

            
This code block in the floating window

Find the current_organization field in the response, which looks like:

https://api.calendly.com/organizations/xxxxxxxxxxxxxxxx
              
              https://api.calendly.com/organizations/xxxxxxxxxxxxxxxx

            
This code block in the floating window

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" }'
              
              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"
  }'

            
This code block in the floating window

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 url must be a publicly accessible HTTPS address. For local development, you can use ngrok to generate a temporary address: ngrok http 3000, then use the output https://xxxx.ngrok.io URL.

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
              
              npm install express

            
This code block in the floating window

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'));
              
              // 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'));

            
This code block in the floating window

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
              
              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

            
This code block in the floating window

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); }
              
              // 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);
}

            
This code block in the floating window

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) }, }); }
              
              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) },
  });
}

            
This code block in the floating window

Important Notes

  1. Webhooks must respond with HTTP 200 within 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.
  2. The text_reminder_number field may be empty — add null checks in your code to avoid passing an empty number to the SMS API, which would cause errors.
  3. Phone numbers must include the country code, e.g. +8618701235678 for mainland China numbers. Calendly prompts users to enter international format when collecting phone numbers, but server-side format validation is recommended.
  4. setTimeout is 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.
  5. Templates must be approved before use — if a template is pending review or has been rejected when called, the API will return a 4001 error.
  6. An HTTP 200 response from the API does not guarantee successful SMS delivery — check the code field in the response body. Refer to the error code documentation for non-zero values.
Icon Solid Transparent White Qiyu
Contact Sales