Logo Site EngageLab Mark Colored Transparent文档
搜索

Calendly

Calendly 是广泛使用的在线预约工具,但其内置短信提醒功能仅支持部分国家和地区,亚太、东南亚、中东等市场覆盖有限。通过将 Calendly Webhook 与 EngageLab SMS 结合,可在预约创建、取消以及会议开始前,向全球任意号码发送短信通知,补齐 Calendly 原生能力的地域空白。

前置条件

开始前,请确认以下配置已完成:

EngageLab 侧

  • 已开通 EngageLab SMS 服务
  • 已在模板管理页面创建短信模板并通过审核,获取模板 ID
  • 已在 API 密钥页面创建 API 密钥,获取 dev_keydev_secret

Calendly 侧

  • 已拥有 Calendly Standard 及以上套餐(Webhook 功能需付费套餐支持)
  • 已在 Integrations & apps → API and webhooks 页面创建 Personal Access Token

服务器侧

  • 拥有一台可公网访问的服务器,并配置了有效的 HTTPS 证书
  • 本地开发调试时,可使用 ngrok 临时暴露本地端口

第一步:准备短信模板

调用 API 发送短信必须使用预审核通过的模板,不支持直接传入自定义文本。

登录 EngageLab 控制台,进入 SMS → 模板管理,根据场景分别创建以下三个模板:

模板用途 模板内容示例
预约确认 您好 {{name}},您的会议已确认,时间:{{time}},期待与您相见。
取消通知 您好 {{name}},您的会议预约已取消,如需重新预约请联系我们。
会议提醒 您好 {{name}},您有一个会议将在 {{advance}} 后开始,时间:{{time}},请提前准备。

模板提交后等待审核通过,分别记录三个模板 ID。

建议:为每个场景创建独立模板,语义更清晰,审核通过率也更高。若模板中包含自定义变量(如 {{name}}),调用 API 时需通过 params 字段传值,否则变量将原样下发。

第二步:在 Calendly 创建 Webhook 订阅

Calendly 的 Webhook 管理没有可视化界面,需通过 API 创建。

获取组织 URI

首先调用以下接口获取当前账号的组织 URI:

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"

            
此代码块在浮窗中显示

在返回结果中找到 current_organization 字段,格式如下:

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

            
此代码块在浮窗中显示

创建 Webhook 订阅

使用组织 URI 创建 Webhook,订阅预约创建和取消两个事件:

curl -X POST https://api.calendly.com/webhook_subscriptions \ -H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://你的服务器地址/webhooks/calendly", "events": [ "invitee.created", "invitee.canceled" ], "organization": "https://api.calendly.com/organizations/你的组织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://你的服务器地址/webhooks/calendly",
    "events": [
      "invitee.created",
      "invitee.canceled"
    ],
    "organization": "https://api.calendly.com/organizations/你的组织ID",
    "scope": "organization"
  }'

            
此代码块在浮窗中显示

创建成功后,Calendly 会向填入的 url 推送一条验证请求,服务器需返回 HTTP 200 完成握手。后续每次有人预约或取消,均会向该地址推送事件。

注意url 必须是公网可访问的 HTTPS 地址。本地开发时,可使用 ngrok 生成临时地址:ngrok http 3000,将输出的 https://xxxx.ngrok.io 填入即可。

第三步:搭建 Webhook 接收服务

以下为完整的 Node.js 服务端示例,接收 Calendly Webhook 事件后调用 EngageLab SMS API 发送短信。

安装依赖

npm install express
              
              npm install express

            
此代码块在浮窗中显示

完整代码

// server.js import express from 'express'; const app = express(); app.use(express.json()); // EngageLab 鉴权:base64(dev_key:dev_secret) const ENGAGELAB_AUTH = Buffer.from( `${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}` ).toString('base64'); // 调用 EngageLab SMS API 发送短信 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 不代表发送成功,需检查 code 字段 if (data.code && data.code !== 0) { console.error(`SMS 发送失败: code=${data.code}, message=${data.message}`); } else { console.log(`SMS 发送成功: plan_id=${data.plan_id}, message_id=${data.message_id}`); } } // 将 ISO 时间字符串转换为本地时间(Asia/Shanghai) function formatTime(isoString) { return new Date(isoString).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } // 注册会议前提醒定时任务(24h 和 1h 各一条) function scheduleReminders({ phone, name, startTime }) { const meetingTime = new Date(startTime).getTime(); const now = Date.now(); const reminders = [ { advance: '24 小时', triggerAt: meetingTime - 24 * 60 * 60 * 1000 }, { advance: '1 小时', triggerAt: meetingTime - 60 * 60 * 1000 }, ]; for (const { advance, triggerAt } of reminders) { const delay = triggerAt - now; if (delay <= 0) continue; // 触发时间已过,跳过 setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_REMINDER, params: { name, time: formatTime(startTime), advance }, }); }, delay); } } // Webhook 接收端点 app.post('/webhooks/calendly', async (req, res) => { const { event, payload } = req.body; const phone = payload.text_reminder_number; // 用户预约时填写的手机号 const name = payload.invitee?.name ?? '用户'; const startTime = payload.scheduled_event?.start_time; // 手机号未填时跳过短信发送 if (!phone) { console.log('该预约未填写手机号,跳过短信通知'); return res.sendStatus(200); } if (event === 'invitee.created') { // 发送预约确认短信 await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) }, }); // 注册会议前提醒任务 scheduleReminders({ phone, name, startTime }); } else if (event === 'invitee.canceled') { // 发送取消通知短信 await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CANCEL, params: { name }, }); } // 必须在 2 秒内返回 200,否则 Calendly 会认为推送失败并重试 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 鉴权:base64(dev_key:dev_secret)
const ENGAGELAB_AUTH = Buffer.from(
  `${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}`
).toString('base64');

// 调用 EngageLab SMS API 发送短信
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 不代表发送成功,需检查 code 字段
  if (data.code && data.code !== 0) {
    console.error(`SMS 发送失败: code=${data.code}, message=${data.message}`);
  } else {
    console.log(`SMS 发送成功: plan_id=${data.plan_id}, message_id=${data.message_id}`);
  }
}

// 将 ISO 时间字符串转换为本地时间(Asia/Shanghai)
function formatTime(isoString) {
  return new Date(isoString).toLocaleString('zh-CN', {
    timeZone: 'Asia/Shanghai',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  });
}

// 注册会议前提醒定时任务(24h 和 1h 各一条)
function scheduleReminders({ phone, name, startTime }) {
  const meetingTime = new Date(startTime).getTime();
  const now = Date.now();

  const reminders = [
    { advance: '24 小时', triggerAt: meetingTime - 24 * 60 * 60 * 1000 },
    { advance: '1 小时',  triggerAt: meetingTime -      60 * 60 * 1000 },
  ];

  for (const { advance, triggerAt } of reminders) {
    const delay = triggerAt - now;
    if (delay <= 0) continue; // 触发时间已过,跳过

    setTimeout(async () => {
      await sendSMS({
        to: phone,
        templateId: process.env.TEMPLATE_ID_REMINDER,
        params: { name, time: formatTime(startTime), advance },
      });
    }, delay);
  }
}

// Webhook 接收端点
app.post('/webhooks/calendly', async (req, res) => {
  const { event, payload } = req.body;

  const phone     = payload.text_reminder_number;     // 用户预约时填写的手机号
  const name      = payload.invitee?.name ?? '用户';
  const startTime = payload.scheduled_event?.start_time;

  // 手机号未填时跳过短信发送
  if (!phone) {
    console.log('该预约未填写手机号,跳过短信通知');
    return res.sendStatus(200);
  }

  if (event === 'invitee.created') {
    // 发送预约确认短信
    await sendSMS({
      to: phone,
      templateId: process.env.TEMPLATE_ID_CONFIRM,
      params: { name, time: formatTime(startTime) },
    });

    // 注册会议前提醒任务
    scheduleReminders({ phone, name, startTime });

  } else if (event === 'invitee.canceled') {
    // 发送取消通知短信
    await sendSMS({
      to: phone,
      templateId: process.env.TEMPLATE_ID_CANCEL,
      params: { name },
    });
  }

  // 必须在 2 秒内返回 200,否则 Calendly 会认为推送失败并重试
  res.sendStatus(200);
});

app.listen(3000, () => console.log('Server running on port 3000'));

            
此代码块在浮窗中显示

环境变量配置

在项目根目录创建 .env 文件,填入以下变量:

ENGAGELAB_DEV_KEY=你的dev_key ENGAGELAB_DEV_SECRET=你的dev_secret TEMPLATE_ID_CONFIRM=预约确认模板ID TEMPLATE_ID_CANCEL=取消通知模板ID TEMPLATE_ID_REMINDER=会议提醒模板ID
              
              ENGAGELAB_DEV_KEY=你的dev_key
ENGAGELAB_DEV_SECRET=你的dev_secret

TEMPLATE_ID_CONFIRM=预约确认模板ID
TEMPLATE_ID_CANCEL=取消通知模板ID
TEMPLATE_ID_REMINDER=会议提醒模板ID

            
此代码块在浮窗中显示

关键字段说明

Calendly Webhook 推送的 payload 中,以下字段与短信发送直接相关:

字段 说明
event 事件类型,invitee.created(预约创建)或 invitee.canceled(预约取消)
payload.text_reminder_number 预约者填写的手机号,含国家代码,可能为空
payload.invitee.name 预约者姓名
payload.scheduled_event.start_time 会议开始时间,ISO 8601 格式

扩展场景

会议结束后发送跟进短信

scheduleReminders 中增加一条会议结束后触发的任务,用于发送满意度调查或下次预约邀请:

// 在 scheduleReminders 函数中追加 const followUpAt = meetingTime + 30 * 60 * 1000; // 会议结束后 30 分钟 const followUpDelay = followUpAt - now; if (followUpDelay > 0) { setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_FOLLOWUP, params: { name }, }); }, followUpDelay); }
              
              // 在 scheduleReminders 函数中追加
const followUpAt = meetingTime + 30 * 60 * 1000; // 会议结束后 30 分钟
const followUpDelay = followUpAt - now;

if (followUpDelay > 0) {
  setTimeout(async () => {
    await sendSMS({
      to: phone,
      templateId: process.env.TEMPLATE_ID_FOLLOWUP,
      params: { name },
    });
  }, followUpDelay);
}

            
此代码块在浮窗中显示

同时通知主持人

预约创建时,除通知预约者外,还可向会议主持人发送一条提醒:

if (event === 'invitee.created') { // 通知预约者 await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) }, }); // 通知主持人 await sendSMS({ to: process.env.HOST_PHONE, templateId: process.env.TEMPLATE_ID_HOST_NOTIFY, params: { name, time: formatTime(startTime) }, }); }
              
              if (event === 'invitee.created') {
  // 通知预约者
  await sendSMS({
    to: phone,
    templateId: process.env.TEMPLATE_ID_CONFIRM,
    params: { name, time: formatTime(startTime) },
  });

  // 通知主持人
  await sendSMS({
    to: process.env.HOST_PHONE,
    templateId: process.env.TEMPLATE_ID_HOST_NOTIFY,
    params: { name, time: formatTime(startTime) },
  });
}

            
此代码块在浮窗中显示

注意事项

  1. Webhook 需在 2 秒内响应 HTTP 200,否则 Calendly 会认为推送失败并进行重试。建议先返回 200,再异步执行短信发送逻辑,避免因 EngageLab API 响应延迟导致 Calendly 误判。
  2. text_reminder_number 字段可能为空,需在代码中做判空处理,避免将空号码传入 SMS API 导致报错。
  3. 手机号格式需含国家代码,如中国大陆号码为 +8618701235678。Calendly 在收集手机号时会引导用户填写国际格式,但建议在服务端做一次格式校验。
  4. setTimeout 不适用于生产环境,服务器重启后所有定时任务将丢失。生产环境建议使用持久化任务队列(如 BullMQ + Redis)或云服务的定时任务,将提醒记录存入数据库,重启后可恢复。
  5. 模板必须审核通过后方可使用,调用时若模板处于待审核或审核拒绝状态,API 将返回 4001 错误。
  6. API 返回 HTTP 200 并不代表短信发送成功,请检查响应体中的 code 字段,非零时参考错误码说明排查原因。
Icon Solid Transparent White Qiyu
联系销售