Calendly

Calendly เป็นเครื่องมือนัดหมายออนไลน์ที่ใช้กันอย่างแพร่หลาย แต่ฟีเจอร์แจ้งเตือน SMS ในตัวรองรับเพียงบางประเทศและภูมิภาคเท่านั้น โดยมีความครอบคลุมที่จำกัดในเอเชียแปซิฟิก เอเชียตะวันออกเฉียงใต้ ตะวันออกกลาง และตลาดอื่นๆ การรวม Calendly Webhooks เข้ากับ EngageLab SMS ช่วยให้คุณสามารถส่งการแจ้งเตือน SMS ไปยังหมายเลขโทรศัพท์ทั่วโลกเมื่อมีการสร้างนัดหมาย ยกเลิก หรือก่อนการประชุมจะเริ่มต้น — เติมเต็มช่องว่างทางภูมิศาสตร์ของความสามารถดั้งเดิมของ Calendly

ข้อกำหนดเบื้องต้น

ก่อนเริ่มต้น ตรวจสอบให้แน่ใจว่าการกำหนดค่าต่อไปนี้เสร็จสมบูรณ์:

ฝั่ง EngageLab

  • บริการ EngageLab SMS ถูกเปิดใช้งานแล้ว
  • สร้างเทมเพลต SMS และได้รับการอนุมัติในหน้าจัดการเทมเพลตแล้ว; ได้รับ ID เทมเพลตแล้ว
  • สร้างคีย์ API ในหน้า API Keys แล้ว; ได้รับ dev_key และ dev_secret แล้ว

ฝั่ง Calendly

  • คุณมีแผน Calendly Standard ขึ้นไป (ฟังก์ชัน Webhook ต้องใช้แผนชำระเงิน)
  • สร้าง Personal Access Token ในหน้า Integrations & apps → API and webhooks แล้ว

ฝั่งเซิร์ฟเวอร์

  • คุณมีเซิร์ฟเวอร์ที่เข้าถึงได้จากอินเทอร์เน็ตสาธารณะและมีใบรับรอง HTTPS ที่ถูกต้อง
  • สำหรับการพัฒนาและดีบักในเครื่อง คุณสามารถใช้ ngrok เพื่อเปิดเผยพอร์ตในเครื่องชั่วคราว

ขั้นตอนที่ 1: เตรียมเทมเพลต SMS

การส่ง SMS ผ่าน API ต้องใช้เทมเพลตที่ได้รับการอนุมัติล่วงหน้า; ไม่สามารถส่งข้อความที่กำหนดเองโดยตรงได้

เข้าสู่ระบบคอนโซล EngageLab ไปที่ SMS → จัดการเทมเพลต และสร้างเทมเพลตสามรายการต่อไปนี้สำหรับแต่ละสถานการณ์:

วัตถุประสงค์เทมเพลต ตัวอย่างเนื้อหา
ยืนยันนัดหมาย สวัสดี {{name}} การประชุมของคุณได้รับการยืนยันเวลา {{time}} แล้ว รอพบคุณนะ
แจ้งยกเลิก สวัสดี {{name}} นัดหมายการประชุมของคุณถูกยกเลิกแล้ว กรุณาติดต่อเราเพื่อนัดหมายใหม่
เตือนการประชุม สวัสดี {{name}} คุณมีการประชุมที่จะเริ่มใน {{advance}} กำหนดเวลา {{time}} กรุณาเตรียมตัวล่วงหน้า

หลังจากส่งเทมเพลต รอการอนุมัติและจดบันทึก ID เทมเพลตทั้งสามรายการ

เคล็ดลับ: สร้างเทมเพลตแยกสำหรับแต่ละสถานการณ์ — ทำให้เจตนาชัดเจนขึ้นและเพิ่มอัตราการอนุมัติ หากเทมเพลตมีตัวแปรที่กำหนดเอง (เช่น {{name}}) คุณต้องส่งค่าผ่านฟิลด์ params เมื่อเรียก API มิฉะนั้นตัวแปรจะถูกส่งตามที่เป็น

ขั้นตอนที่ 2: สร้าง Webhook Subscription ใน Calendly

Calendly ไม่มีอินเทอร์เฟซแบบภาพสำหรับจัดการ Webhook; ต้องสร้าง subscription ผ่าน API

รับ Organization URI

ขั้นแรก เรียก endpoint ต่อไปนี้เพื่อรับ organization 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 Subscription

ใช้ organization URI เพื่อสร้าง Webhook ที่ subscribe เหตุการณ์การสร้างและยกเลิกนัดหมาย:

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

            
โค้ดนี้โชว์เป็นหน้าต่างลอย

เมื่อสร้างเสร็จ Calendly จะส่งคำขอยืนยันไปยัง url ที่ให้ไว้ และเซิร์ฟเวอร์ของคุณต้องตอบกลับด้วย HTTP 200 เพื่อเสร็จสิ้นการ handshake เหตุการณ์นัดหมายและการยกเลิกในภายหลังจะถูกส่งไปยังที่อยู่นี้

หมายเหตุ: url ต้องเป็นที่อยู่ HTTPS ที่เข้าถึงได้จากสาธารณะ สำหรับการพัฒนาในเครื่อง คุณสามารถใช้ ngrok เพื่อสร้างที่อยู่ชั่วคราว: ngrok http 3000 จากนั้นใช้ URL https://xxxx.ngrok.io ที่ได้

ขั้นตอนที่ 3: สร้างบริการรับ Webhook

ด้านล่างเป็นตัวอย่างเซิร์ฟเวอร์ Node.js แบบสมบูรณ์ที่รับเหตุการณ์ Calendly Webhook และเรียก EngageLab SMS API เพื่อส่งข้อความ

ติดตั้ง Dependencies

npm install express
              
              npm install express

            
โค้ดนี้โชว์เป็นหน้าต่างลอย

โค้ดแบบสมบูรณ์

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

            
โค้ดนี้โชว์เป็นหน้าต่างลอย

การกำหนดค่าตัวแปรสภาพแวดล้อม

สร้างไฟล์ .env ในไดเรกทอรีรากของโปรเจกต์ด้วยตัวแปรต่อไปนี้:

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

            
โค้ดนี้โชว์เป็นหน้าต่างลอย

ข้อมูลอ้างอิงฟิลด์สำคัญ

ฟิลด์ต่อไปนี้ใน Calendly Webhook payload เกี่ยวข้องโดยตรงกับการส่ง SMS:

ฟิลด์ คำอธิบาย
event ประเภทเหตุการณ์: invitee.created (สร้างนัดหมาย) หรือ invitee.canceled (ยกเลิกนัดหมาย)
payload.text_reminder_number หมายเลขโทรศัพท์ที่ผู้ได้รับเชิญให้ไว้ รวมรหัสประเทศ; อาจว่างเปล่า
payload.invitee.name ชื่อผู้ได้รับเชิญ
payload.scheduled_event.start_time เวลาเริ่มการประชุมในรูปแบบ ISO 8601

สถานการณ์ขยาย

ส่ง SMS ติดตามหลังการประชุม

เพิ่ม trigger หลังการประชุมใน scheduleReminders เพื่อส่งแบบสำรวจความพึงพอใจหรือคำเชิญนัดหมายติดตาม:

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

            
โค้ดนี้โชว์เป็นหน้าต่างลอย

แจ้งเตือนผู้จัดประชุมด้วย

เมื่อสร้างนัดหมาย นอกจากแจ้งเตือนผู้ได้รับเชิญแล้ว คุณยังสามารถส่งการเตือนไปยังผู้จัดประชุมได้:

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

            
โค้ดนี้โชว์เป็นหน้าต่างลอย

หมายเหตุสำคัญ

  1. Webhooks ต้องตอบกลับด้วย HTTP 200 ภายใน 2 วินาที มิฉะนั้น Calendly จะถือว่าการส่งล้มเหลวและลองใหม่ แนะนำให้ตอบกลับ 200 ก่อน จากนั้นดำเนินการส่ง SMS แบบอะซิงโครนัสเพื่อหลีกเลี่ยงการตัดสินผิดพลาดของ Calendly เนื่องจากความล่าช้าของ EngageLab API
  2. ฟิลด์ text_reminder_number อาจว่างเปล่า — เพิ่มการตรวจสอบค่า null ในโค้ดของคุณเพื่อหลีกเลี่ยงการส่งหมายเลขว่างไปยัง SMS API ซึ่งจะทำให้เกิดข้อผิดพลาด
  3. หมายเลขโทรศัพท์ต้องมีรหัสประเทศ เช่น +8618701235678 สำหรับหมายเลขจีนแผ่นดินใหญ่ Calendly จะแนะนำผู้ใช้ให้กรอกรูปแบบสากลเมื่อเก็บหมายเลขโทรศัพท์ แต่แนะนำให้ตรวจสอบรูปแบบฝั่งเซิร์ฟเวอร์ด้วย
  4. setTimeout ไม่เหมาะสำหรับสภาพแวดล้อมการผลิต — งานที่กำหนดเวลาทั้งหมดจะสูญหายเมื่อเซิร์ฟเวอร์รีสตาร์ท สำหรับการผลิต ใช้คิวงานแบบถาวร (เช่น BullMQ + Redis) หรืองานที่กำหนดเวลาบนคลาวด์ และเก็บบันทึกการเตือนในฐานข้อมูลเพื่อกู้คืนหลังจากรีสตาร์ท
  5. เทมเพลตต้องได้รับการอนุมัติก่อนใช้งาน — หากเทมเพลตอยู่ระหว่างการตรวจสอบหรือถูกปฏิเสธเมื่อเรียกใช้ API จะคืนค่าข้อผิดพลาด 4001
  6. การตอบกลับ HTTP 200 จาก API ไม่รับประกันการส่ง SMS สำเร็จ — ตรวจสอบฟิลด์ code ในเนื้อหาการตอบกลับ ดูเอกสารรหัสข้อผิดพลาด สำหรับค่าที่ไม่ใช่ศูนย์
Icon Solid Transparent White Qiyu
ติดต่อฝ่ายขาย