Calendly
Calendly เป็นเครื่องมือนัดหมายออนไลน์ที่ใช้กันอย่างแพร่หลาย แต่ฟังก์ชันเตือนผ่าน SMS ในตัวรองรับเพียงบางประเทศและภูมิภาคเท่านั้น และครอบคลุมตลาดเอเชียแปซิฟิก เอเชียตะวันออกเฉียงใต้ และตะวันออกกลางได้จำกัด การนำ Calendly Webhook มาผสานรวมกับ EngageLab SMS จะช่วยให้คุณส่งการแจ้งเตือน SMS ไปยังหมายเลขใด ๆ ทั่วโลกเมื่อมีการสร้างการนัดหมาย ยกเลิก และก่อนการประชุมจะเริ่ม เพื่อเติมเต็มช่องว่างด้านภูมิภาคของความสามารถดั้งเดิมของ Calendly
ข้อกำหนดเบื้องต้น
ก่อนเริ่ม โปรดตรวจสอบว่าได้กำหนดค่าต่อไปนี้เสร็จแล้ว:
ฝั่ง EngageLab
- เปิดใช้บริการ EngageLab SMS แล้ว
- สร้างเทมเพลต SMS ในหน้าการจัดการเทมเพลตและผ่านการตรวจสอบแล้ว พร้อมได้รับ ID เทมเพลต
- สร้าง API Key ในหน้า API Key แล้ว พร้อมได้รับ
dev_keyและdev_secret
ฝั่ง Calendly
- มีแพ็กเกจ Calendly Standard ขึ้นไป (ฟังก์ชัน Webhook ต้องใช้แพ็กเกจแบบชำระเงิน)
- สร้าง Personal Access Token ในหน้า Integrations & apps → API and webhooks แล้ว
ฝั่งเซิร์ฟเวอร์
- มีเซิร์ฟเวอร์ที่เข้าถึงได้จากอินเทอร์เน็ตสาธารณะ และกำหนดค่าใบรับรอง HTTPS ที่ใช้งานได้แล้ว
- ขณะพัฒนาและดีบักในเครื่อง สามารถใช้ ngrok เพื่อเปิดเผยพอร์ตในเครื่องชั่วคราวได้
ขั้นตอนที่ 1: เตรียมเทมเพลต SMS
การเรียกใช้ API เพื่อส่ง SMS ต้องใช้เทมเพลตที่ผ่านการตรวจสอบล่วงหน้า ไม่รองรับการส่งข้อความที่กำหนดเองโดยตรง
เข้าสู่ระบบคอนโซล EngageLab ไปที่ SMS → การจัดการเทมเพลต และสร้างเทมเพลตสามรายการต่อไปนี้ตามสถานการณ์:
| วัตถุประสงค์ของเทมเพลต | ตัวอย่างเนื้อหาเทมเพลต |
|---|---|
| ยืนยันการนัดหมาย | สวัสดี {{name}} การประชุมของคุณได้รับการยืนยันแล้ว เวลา: {{time}} เราหวังว่าจะได้พบคุณ |
| แจ้งเตือนการยกเลิก | สวัสดี {{name}} การนัดหมายประชุมของคุณถูกยกเลิกแล้ว หากต้องการนัดหมายใหม่ โปรดติดต่อเรา |
| เตือนการประชุม | สวัสดี {{name}} คุณมีการประชุมที่จะเริ่มในอีก {{advance}} เวลา: {{time}} โปรดเตรียมตัวล่วงหน้า |
หลังจากส่งเทมเพลตแล้ว ให้รอการตรวจสอบให้ผ่าน และบันทึก ID เทมเพลตทั้งสามไว้
คำแนะนำ: ควรสร้างเทมเพลตแยกสำหรับแต่ละสถานการณ์ เพื่อให้ความหมายชัดเจนขึ้นและมีอัตราการผ่านการตรวจสอบสูงขึ้น หากเทมเพลตมีตัวแปรที่กำหนดเอง (เช่น
{{name}}) ตอนเรียกใช้ API ต้องส่งค่าผ่านฟิลด์paramsมิฉะนั้นตัวแปรจะถูกส่งออกตามเดิม
ขั้นตอนที่ 2: สร้างการสมัครรับ Webhook ใน Calendly
การจัดการ Webhook ของ Calendly ไม่มีอินเทอร์เฟซแบบกราฟิก ต้องสร้างผ่าน API
รับ URI องค์กร
ก่อนอื่น เรียกใช้อินเทอร์เฟซต่อไปนี้เพื่อรับ URI องค์กรของบัญชีปัจจุบัน:
curl https://api.calendly.com/users/me \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
ในผลลัพธ์ที่ส่งกลับ ให้หาฟิลด์ current_organization ซึ่งมีรูปแบบดังนี้:
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"
}'
หลังจากสร้างสำเร็จ Calendly จะส่งคำขอตรวจสอบหนึ่งรายการไปยัง url ที่กรอกไว้ เซิร์ฟเวอร์ต้องส่งกลับ HTTP 200 เพื่อทำการ handshake ให้เสร็จสมบูรณ์ หลังจากนั้นทุกครั้งที่มีคนนัดหมายหรือยกเลิก ระบบจะส่งเหตุการณ์ไปยังที่อยู่ดังกล่าว
หมายเหตุ:
urlต้องเป็น URL แบบ HTTPS ที่เข้าถึงได้จากอินเทอร์เน็ตสาธารณะ ขณะพัฒนาในเครื่อง สามารถใช้ ngrok เพื่อสร้างที่อยู่ชั่วคราว:ngrok http 3000แล้วนำhttps://xxxx.ngrok.ioที่ได้มากรอก
ขั้นตอนที่ 3: สร้างบริการรับ Webhook
ต่อไปนี้คือตัวอย่างฝั่งเซิร์ฟเวอร์แบบ Node.js ที่สมบูรณ์ ซึ่งจะเรียกใช้ EngageLab SMS API เพื่อส่ง SMS หลังจากรับเหตุการณ์ Calendly Webhook
ติดตั้ง dependency
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 เพื่อส่ง 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 ไม่ได้หมายความว่าส่งสำเร็จ ต้องตรวจสอบฟิลด์ 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',
});
}
// ลงทะเบียนงานตามกำหนดเวลาสำหรับเตือนก่อนการประชุม (24 ชม. และ 1 ชม. อย่างละหนึ่งรายการ)
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;
// ข้ามการส่ง SMS เมื่อไม่ได้กรอกหมายเลขโทรศัพท์
if (!phone) {
console.log('การนัดหมายนี้ไม่ได้กรอกหมายเลขโทรศัพท์ ข้ามการแจ้งเตือน SMS');
return res.sendStatus(200);
}
if (event === 'invitee.created') {
// ส่ง SMS ยืนยันการนัดหมาย
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CONFIRM,
params: { name, time: formatTime(startTime) },
});
// ลงทะเบียนงานเตือนก่อนการประชุม
scheduleReminders({ phone, name, startTime });
} else if (event === 'invitee.canceled') {
// ส่ง SMS แจ้งเตือนการยกเลิก
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CANCEL,
params: { name },
});
}
// ต้องส่งกลับ 200 ภายใน 2 วินาที มิฉะนั้น 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 เทมเพลตเตือนการประชุม
คำอธิบายฟิลด์สำคัญ
ใน payload ที่ Calendly Webhook ส่งมา ฟิลด์ต่อไปนี้เกี่ยวข้องโดยตรงกับการส่ง SMS:
| ฟิลด์ | คำอธิบาย |
|---|---|
event |
ประเภทเหตุการณ์ invitee.created (สร้างการนัดหมาย) หรือ invitee.canceled (ยกเลิกการนัดหมาย) |
payload.text_reminder_number |
หมายเลขโทรศัพท์ที่ผู้นัดหมายกรอก รวมรหัสประเทศ อาจเป็นค่าว่าง |
payload.invitee.name |
ชื่อของผู้นัดหมาย |
payload.scheduled_event.start_time |
เวลาเริ่มการประชุม รูปแบบ ISO 8601 |
สถานการณ์เพิ่มเติม
ส่ง SMS ติดตามผลหลังการประชุมจบ
เพิ่มงานที่ทริกเกอร์หลังจากการประชุมจบใน 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);
}
แจ้งเตือนผู้ดำเนินการประชุมพร้อมกัน
ตอนสร้างการนัดหมาย นอกจากแจ้งเตือนผู้นัดหมายแล้ว ยังสามารถส่งการเตือนหนึ่งรายการไปยังผู้ดำเนินการประชุมได้ด้วย:
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) },
});
}
ข้อควรทราบ
- Webhook ต้องตอบกลับ
HTTP 200ภายใน 2 วินาที มิฉะนั้น Calendly จะถือว่าการส่งล้มเหลวและลองใหม่ แนะนำให้ส่งกลับ 200 ก่อน แล้วจึงดำเนินตรรกะการส่ง SMS แบบอะซิงโครนัส เพื่อหลีกเลี่ยงไม่ให้ความล่าช้าในการตอบกลับของ EngageLab API ทำให้ Calendly ตัดสินผิดพลาด - ฟิลด์
text_reminder_numberอาจเป็นค่าว่าง ต้องตรวจสอบค่าว่างในโค้ด เพื่อหลีกเลี่ยงการส่งหมายเลขว่างเข้าไปยัง SMS API จนเกิดข้อผิดพลาด - รูปแบบหมายเลขโทรศัพท์ต้องมีรหัสประเทศ เช่น หมายเลขสิงคโปร์คือ
+6591234567Calendly จะแนะนำให้ผู้ใช้กรอกในรูปแบบสากลขณะเก็บหมายเลขโทรศัพท์ แต่แนะนำให้ตรวจสอบรูปแบบอีกครั้งที่ฝั่งเซิร์ฟเวอร์ setTimeoutไม่เหมาะกับสภาพแวดล้อมการใช้งานจริง เมื่อเซิร์ฟเวอร์รีสตาร์ท งานตามกำหนดเวลาทั้งหมดจะสูญหาย สำหรับสภาพแวดล้อมการใช้งานจริง แนะนำให้ใช้คิวงานแบบถาวร (เช่น BullMQ + Redis) หรืองานตามกำหนดเวลาของบริการคลาวด์ โดยจัดเก็บบันทึกการเตือนไว้ในฐานข้อมูล ซึ่งสามารถกู้คืนได้หลังรีสตาร์ท- เทมเพลตต้องผ่านการตรวจสอบก่อนจึงจะใช้งานได้ หากเทมเพลตอยู่ในสถานะรอการตรวจสอบหรือถูกปฏิเสธการตรวจสอบขณะเรียกใช้ API จะส่งกลับข้อผิดพลาด
4001 - API ส่งกลับ HTTP 200 ไม่ได้หมายความว่าส่ง SMS สำเร็จ โปรดตรวจสอบฟิลด์
codeในเนื้อหาการตอบกลับ หากไม่ใช่ศูนย์ ให้ดูคำอธิบายรหัสข้อผิดพลาดเพื่อตรวจหาสาเหตุ










