Calendly
Calendly adalah alat penjadwalan daring yang banyak dipakai, tetapi fitur pengingat SMS bawaannya hanya mendukung sejumlah negara dan wilayah terbatas, dengan cakupan terbatas di Asia-Pasifik, Asia Tenggara, Timur Tengah, dan pasar lainnya. Dengan mengintegrasikan Webhook Calendly dan SMS EngageLab, Anda dapat mengirim notifikasi SMS ke nomor telepon di mana pun di dunia ketika janji dibuat, dibatalkan, atau sebelum rapat dimulai — menutup celah geografis pada kemampuan asli Calendly.
Prasyarat
Sebelum memulai, pastikan konfigurasi berikut telah selesai:
Sisi EngageLab
- Layanan SMS EngageLab telah diaktifkan
- Template SMS telah dibuat dan disetujui di halaman Manajemen Template; ID template telah diperoleh
- Kunci API telah dibuat di halaman Kunci API;
dev_keydandev_secrettelah diperoleh
Sisi Calendly
- Anda memiliki paket Calendly Standard atau lebih tinggi (fitur Webhook memerlukan paket berbayar)
- Personal Access Token telah dibuat di halaman Integrations & apps → API and webhooks
Sisi Server
- Anda memiliki server dengan akses internet publik dan sertifikat HTTPS yang valid
- Untuk pengembangan dan debugging lokal, Anda dapat memakai ngrok untuk mengekspos sementara port lokal
Langkah 1: Siapkan Template SMS
Pengiriman SMS melalui API memerlukan template yang telah disetujui; konten teks kustom tidak dapat dikirim langsung.
Masuk ke konsol EngageLab, buka SMS → Template Management, dan buat tiga template berikut untuk setiap skenario:
| Tujuan Template | Contoh Isi |
|---|---|
| Konfirmasi Janji | Hai {{name}}, pertemuan Anda telah dikonfirmasi untuk {{time}}. Sampai bertemu nanti. |
| Pemberitahuan Pembatalan | Hai {{name}}, janji pertemuan Anda telah dibatalkan. Silakan hubungi kami untuk menjadwalkan ulang. |
| Pengingat Rapat | Hai {{name}}, Anda memiliki rapat yang akan dimulai dalam {{advance}}, dijadwalkan pada {{time}}. Harap menyiapkan diri sebelumnya. |
Setelah template dikirim, tunggu persetujuan dan catat ketiga ID template tersebut.
Tips: Buat template terpisah untuk setiap skenario — maksudnya lebih jelas dan tingkat persetujuan lebih baik. Jika template berisi variabel kustom (mis.
{{name}}), Anda harus meneruskan nilai melalui bidangparamssaat memanggil API; jika tidak, variabel akan terkirim apa adanya.
Langkah 2: Buat Langganan Webhook di Calendly
Calendly tidak menyediakan antarmuka visual untuk manajemen Webhook; langganan harus dibuat melalui API.
Dapatkan URI Organisasi
Pertama, panggil endpoint berikut untuk mendapatkan URI organisasi akun Anda:
curl https://api.calendly.com/users/me \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
Temukan bidang current_organization dalam respons, bentuknya seperti:
https://api.calendly.com/organizations/xxxxxxxxxxxxxxxx
Buat Langganan Webhook
Gunakan URI organisasi untuk membuat Webhook yang berlangganan peristiwa pembuatan dan pembatalan janji:
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"
}'
Setelah dibuat, Calendly akan mengirim permintaan verifikasi ke url yang Anda berikan, dan server Anda harus mengembalikan HTTP 200 untuk menyelesaikan handshake. Peristiwa janji dan pembatalan selanjutnya akan didorong ke alamat ini.
Catatan:
urlharus berupa alamat HTTPS yang dapat diakses publik. Untuk pengembangan lokal, Anda dapat memakai ngrok untuk menghasilkan alamat sementara:ngrok http 3000, lalu gunakan URLhttps://xxxx.ngrok.iodari keluaran tersebut.
Langkah 3: Bangun Layanan Penerima Webhook
Di bawah ini contoh server Node.js lengkap yang menerima peristiwa Webhook Calendly dan memanggil API SMS EngageLab untuk mengirim pesan.
Instal Dependensi
npm install express
Kode Lengkap
// 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'));
Konfigurasi Variabel Lingkungan
Buat berkas .env di direktori akar proyek dengan variabel berikut:
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
Referensi Bidang Penting
Bidang berikut dalam payload Webhook Calendly terkait langsung dengan pengiriman SMS:
| Bidang | Deskripsi |
|---|---|
event |
Jenis peristiwa: invitee.created (janji dibuat) atau invitee.canceled (janji dibatalkan) |
payload.text_reminder_number |
Nomor telepon yang diberikan undangan, termasuk kode negara; boleh kosong |
payload.invitee.name |
Nama undangan |
payload.scheduled_event.start_time |
Waktu mulai rapat dalam format ISO 8601 |
Skenario Lanjutan
Kirim SMS Tindak Lanjut Setelah Rapat
Tambahkan pemicu pasca-rapat di scheduleReminders untuk mengirim survei kepuasan atau undangan janji tindak lanjut:
// 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);
}
Beri Tahu Host Juga
Ketika janji dibuat, selain memberi tahu undangan, Anda juga dapat mengirim pengingat ke host rapat:
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) },
});
}
Catatan Penting
- Webhook harus merespons dengan
HTTP 200dalam 2 detik, jika tidak Calendly menganggap push gagal dan akan mencoba ulang. Disarankan mengembalikan 200 terlebih dahulu, lalu menjalankan logika pengiriman SMS secara asinkron agar Calendly tidak salah menilai karena latensi respons API EngageLab. - Bidang
text_reminder_numberboleh kosong — tambahkan pengecekan null di kode agar tidak meneruskan nomor kosong ke API SMS, yang akan menyebabkan kesalahan. - Nomor telepon harus menyertakan kode negara, mis.
+8618701235678untuk nomor Tiongkok daratan. Calendly meminta pengguna memasukkan format internasional saat mengumpulkan nomor telepon, tetapi validasi format di sisi server disarankan. setTimeouttidak cocok untuk lingkungan produksi — semua tugas terjadwal akan hilang saat server dimulai ulang. Untuk produksi, gunakan antrean tugas persisten (seperti BullMQ + Redis) atau tugas terjadwal berbasis cloud, dan simpan catatan pengingat di basis data agar dapat dipulihkan setelah restart.- Template harus disetujui sebelum dipakai — jika template masih ditinjau atau ditolak saat dipanggil, API akan mengembalikan kesalahan
4001. - Respons HTTP 200 dari API tidak menjamin pengiriman SMS berhasil — periksa bidang
codedalam body respons. Lihat dokumentasi kode kesalahan untuk nilai bukan nol.
