Logo Site EngageLab Mark Colored TransparentDokumen
Cari

Calendly

Calendly adalah tool booking online yang banyak digunakan, tetapi fitur pengingat SMS bawaannya hanya mendukung sebagian negara dan wilayah, dengan cakupan terbatas di pasar Asia Pasifik, Asia Tenggara, Timur Tengah, dan lainnya. Dengan menggabungkan Calendly Webhook dan EngageLab SMS, Anda dapat mengirim notifikasi SMS ke nomor mana pun di seluruh dunia saat booking dibuat, dibatalkan, serta sebelum meeting dimulai, untuk menutup kekosongan geografis dari kemampuan native Calendly.

Prasyarat

Sebelum memulai, harap pastikan konfigurasi berikut sudah selesai:

Sisi EngageLab

  • Sudah mengaktifkan layanan EngageLab SMS
  • Sudah membuat template SMS di halaman pengelolaan template dan lolos peninjauan, serta mendapatkan ID template
  • Sudah membuat secret key API di halaman secret key API, serta mendapatkan dev_key dan dev_secret

Sisi Calendly

  • Sudah memiliki paket Calendly Standard atau lebih tinggi (fitur Webhook memerlukan paket berbayar)
  • Sudah membuat Personal Access Token di halaman Integrations & apps → API and webhooks

Sisi Server

  • Memiliki server yang dapat diakses publik, dan telah dikonfigurasi dengan sertifikat HTTPS yang valid
  • Saat pengembangan dan debugging lokal, dapat menggunakan ngrok untuk mengekspos port lokal sementara

Langkah Pertama: Menyiapkan Template SMS

Memanggil API untuk mengirim SMS harus menggunakan template yang sudah lolos peninjauan, tidak mendukung input teks kustom secara langsung.

Login ke konsol EngageLab, masuk ke SMS → Pengelolaan Template, lalu buat tiga template berikut sesuai skenario:

Kegunaan Template Contoh Konten Template
Konfirmasi booking Halo {{name}}, meeting Anda telah dikonfirmasi, waktu: {{time}}, kami menantikan kehadiran Anda.
Notifikasi pembatalan Halo {{name}}, booking meeting Anda telah dibatalkan, jika ingin booking ulang silakan hubungi kami.
Pengingat meeting Halo {{name}}, Anda memiliki meeting yang akan dimulai dalam {{advance}}, waktu: {{time}}, harap bersiap lebih awal.

Setelah template diajukan, tunggu hingga lolos peninjauan, lalu catat ketiga ID template tersebut.

Saran: Buat template terpisah untuk setiap skenario agar maknanya lebih jelas dan tingkat kelulusan peninjauan lebih tinggi. Jika template mengandung variabel kustom (seperti {{name}}), saat memanggil API perlu memberikan nilai melalui field params, jika tidak variabel akan dikirim apa adanya.

Langkah Kedua: Membuat Langganan Webhook di Calendly

Pengelolaan Webhook Calendly tidak memiliki antarmuka visual, perlu dibuat melalui API.

Mendapatkan URI Organisasi

Pertama panggil endpoint berikut untuk mendapatkan URI organisasi akun saat ini:

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"

            
Tampilkan blok kode ini di jendela mengambang

Pada hasil yang dikembalikan, temukan field current_organization, dengan format sebagai berikut:

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

            
Tampilkan blok kode ini di jendela mengambang

Membuat Langganan Webhook

Gunakan URI organisasi untuk membuat Webhook, berlangganan dua event yaitu pembuatan dan pembatalan booking:

curl -X POST https://api.calendly.com/webhook_subscriptions \ -H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://alamat-server-anda/webhooks/calendly", "events": [ "invitee.created", "invitee.canceled" ], "organization": "https://api.calendly.com/organizations/ID-organisasi-anda", "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://alamat-server-anda/webhooks/calendly",
    "events": [
      "invitee.created",
      "invitee.canceled"
    ],
    "organization": "https://api.calendly.com/organizations/ID-organisasi-anda",
    "scope": "organization"
  }'

            
Tampilkan blok kode ini di jendela mengambang

Setelah berhasil dibuat, Calendly akan mengirim satu request verifikasi ke url yang diisi, dan server harus mengembalikan HTTP 200 untuk menyelesaikan handshake. Selanjutnya setiap kali ada orang yang booking atau membatalkan, event akan dikirim ke alamat tersebut.

Catatan: url harus berupa alamat HTTPS yang dapat diakses publik. Saat pengembangan lokal, dapat menggunakan ngrok untuk menghasilkan alamat sementara: ngrok http 3000, lalu isikan https://xxxx.ngrok.io yang dihasilkan.

Langkah Ketiga: Membangun Layanan Penerima Webhook

Berikut adalah contoh lengkap server Node.js, yang menerima event Calendly Webhook lalu memanggil EngageLab SMS API untuk mengirim SMS.

Menginstal Dependensi

npm install express
              
              npm install express

            
Tampilkan blok kode ini di jendela mengambang

Kode Lengkap

// server.js import express from 'express'; const app = express(); app.use(express.json()); // Autentikasi EngageLab: base64(dev_key:dev_secret) const ENGAGELAB_AUTH = Buffer.from( `${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}` ).toString('base64'); // Memanggil EngageLab SMS API untuk mengirim 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 tidak berarti pengiriman berhasil, perlu memeriksa field code if (data.code && data.code !== 0) { console.error(`SMS gagal dikirim: code=${data.code}, message=${data.message}`); } else { console.log(`SMS berhasil dikirim: plan_id=${data.plan_id}, message_id=${data.message_id}`); } } // Mengonversi string waktu ISO menjadi waktu lokal (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', }); } // Mendaftarkan tugas terjadwal pengingat sebelum meeting (24 jam dan 1 jam masing-masing satu) function scheduleReminders({ phone, name, startTime }) { const meetingTime = new Date(startTime).getTime(); const now = Date.now(); const reminders = [ { advance: '24 jam', triggerAt: meetingTime - 24 * 60 * 60 * 1000 }, { advance: '1 jam', triggerAt: meetingTime - 60 * 60 * 1000 }, ]; for (const { advance, triggerAt } of reminders) { const delay = triggerAt - now; if (delay <= 0) continue; // Waktu trigger sudah lewat, lewati setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_REMINDER, params: { name, time: formatTime(startTime), advance }, }); }, delay); } } // Endpoint penerima Webhook app.post('/webhooks/calendly', async (req, res) => { const { event, payload } = req.body; const phone = payload.text_reminder_number; // Nomor ponsel yang diisi pengguna saat booking const name = payload.invitee?.name ?? 'Pengguna'; const startTime = payload.scheduled_event?.start_time; // Lewati pengiriman SMS jika nomor ponsel tidak diisi if (!phone) { console.log('Booking ini tidak mengisi nomor ponsel, lewati notifikasi SMS'); return res.sendStatus(200); } if (event === 'invitee.created') { // Mengirim SMS konfirmasi booking await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) }, }); // Mendaftarkan tugas pengingat sebelum meeting scheduleReminders({ phone, name, startTime }); } else if (event === 'invitee.canceled') { // Mengirim SMS notifikasi pembatalan await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CANCEL, params: { name }, }); } // Harus mengembalikan 200 dalam 2 detik, jika tidak Calendly akan menganggap pengiriman gagal dan mencoba ulang 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());

// Autentikasi EngageLab: base64(dev_key:dev_secret)
const ENGAGELAB_AUTH = Buffer.from(
  `${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}`
).toString('base64');

// Memanggil EngageLab SMS API untuk mengirim 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 tidak berarti pengiriman berhasil, perlu memeriksa field code
  if (data.code && data.code !== 0) {
    console.error(`SMS gagal dikirim: code=${data.code}, message=${data.message}`);
  } else {
    console.log(`SMS berhasil dikirim: plan_id=${data.plan_id}, message_id=${data.message_id}`);
  }
}

// Mengonversi string waktu ISO menjadi waktu lokal (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',
  });
}

// Mendaftarkan tugas terjadwal pengingat sebelum meeting (24 jam dan 1 jam masing-masing satu)
function scheduleReminders({ phone, name, startTime }) {
  const meetingTime = new Date(startTime).getTime();
  const now = Date.now();

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

  for (const { advance, triggerAt } of reminders) {
    const delay = triggerAt - now;
    if (delay <= 0) continue; // Waktu trigger sudah lewat, lewati

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

// Endpoint penerima Webhook
app.post('/webhooks/calendly', async (req, res) => {
  const { event, payload } = req.body;

  const phone     = payload.text_reminder_number;     // Nomor ponsel yang diisi pengguna saat booking
  const name      = payload.invitee?.name ?? 'Pengguna';
  const startTime = payload.scheduled_event?.start_time;

  // Lewati pengiriman SMS jika nomor ponsel tidak diisi
  if (!phone) {
    console.log('Booking ini tidak mengisi nomor ponsel, lewati notifikasi SMS');
    return res.sendStatus(200);
  }

  if (event === 'invitee.created') {
    // Mengirim SMS konfirmasi booking
    await sendSMS({
      to: phone,
      templateId: process.env.TEMPLATE_ID_CONFIRM,
      params: { name, time: formatTime(startTime) },
    });

    // Mendaftarkan tugas pengingat sebelum meeting
    scheduleReminders({ phone, name, startTime });

  } else if (event === 'invitee.canceled') {
    // Mengirim SMS notifikasi pembatalan
    await sendSMS({
      to: phone,
      templateId: process.env.TEMPLATE_ID_CANCEL,
      params: { name },
    });
  }

  // Harus mengembalikan 200 dalam 2 detik, jika tidak Calendly akan menganggap pengiriman gagal dan mencoba ulang
  res.sendStatus(200);
});

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

            
Tampilkan blok kode ini di jendela mengambang

Konfigurasi Environment Variable

Buat file .env di direktori root proyek, lalu isikan variabel berikut:

ENGAGELAB_DEV_KEY=dev_key_anda ENGAGELAB_DEV_SECRET=dev_secret_anda TEMPLATE_ID_CONFIRM=ID_template_konfirmasi_booking TEMPLATE_ID_CANCEL=ID_template_notifikasi_pembatalan TEMPLATE_ID_REMINDER=ID_template_pengingat_meeting
              
              ENGAGELAB_DEV_KEY=dev_key_anda
ENGAGELAB_DEV_SECRET=dev_secret_anda

TEMPLATE_ID_CONFIRM=ID_template_konfirmasi_booking
TEMPLATE_ID_CANCEL=ID_template_notifikasi_pembatalan
TEMPLATE_ID_REMINDER=ID_template_pengingat_meeting

            
Tampilkan blok kode ini di jendela mengambang

Penjelasan Field Penting

Pada payload yang dikirim Calendly Webhook, field-field berikut berkaitan langsung dengan pengiriman SMS:

Field Penjelasan
event Tipe event, invitee.created (booking dibuat) atau invitee.canceled (booking dibatalkan)
payload.text_reminder_number Nomor ponsel yang diisi pemesan, termasuk kode negara, bisa kosong
payload.invitee.name Nama pemesan
payload.scheduled_event.start_time Waktu mulai meeting, format ISO 8601

Skenario Lanjutan

Mengirim SMS Tindak Lanjut Setelah Meeting Selesai

Tambahkan satu tugas yang terpicu setelah meeting selesai pada scheduleReminders, untuk mengirim survei kepuasan atau undangan booking berikutnya:

// Tambahkan pada fungsi scheduleReminders const followUpAt = meetingTime + 30 * 60 * 1000; // 30 menit setelah meeting selesai const followUpDelay = followUpAt - now; if (followUpDelay > 0) { setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_FOLLOWUP, params: { name }, }); }, followUpDelay); }
              
              // Tambahkan pada fungsi scheduleReminders
const followUpAt = meetingTime + 30 * 60 * 1000; // 30 menit setelah meeting selesai
const followUpDelay = followUpAt - now;

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

            
Tampilkan blok kode ini di jendela mengambang

Memberi Tahu Host Sekaligus

Saat booking dibuat, selain memberi tahu pemesan, Anda juga dapat mengirim pengingat ke host meeting:

if (event === 'invitee.created') { // Memberi tahu pemesan await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) }, }); // Memberi tahu host await sendSMS({ to: process.env.HOST_PHONE, templateId: process.env.TEMPLATE_ID_HOST_NOTIFY, params: { name, time: formatTime(startTime) }, }); }
              
              if (event === 'invitee.created') {
  // Memberi tahu pemesan
  await sendSMS({
    to: phone,
    templateId: process.env.TEMPLATE_ID_CONFIRM,
    params: { name, time: formatTime(startTime) },
  });

  // Memberi tahu host
  await sendSMS({
    to: process.env.HOST_PHONE,
    templateId: process.env.TEMPLATE_ID_HOST_NOTIFY,
    params: { name, time: formatTime(startTime) },
  });
}

            
Tampilkan blok kode ini di jendela mengambang

Catatan Penting

  1. Webhook harus merespons HTTP 200 dalam 2 detik, jika tidak Calendly akan menganggap pengiriman gagal dan mencoba ulang. Disarankan untuk mengembalikan 200 terlebih dahulu, baru menjalankan logika pengiriman SMS secara asinkron, guna menghindari kesalahan penilaian Calendly akibat keterlambatan respons EngageLab API.
  2. Field text_reminder_number bisa kosong, perlu penanganan null dalam kode, agar tidak meneruskan nomor kosong ke SMS API yang menyebabkan error.
  3. Format nomor ponsel harus menyertakan kode negara, misalnya nomor Singapura adalah +6591234567. Calendly akan mengarahkan pengguna untuk mengisi format internasional saat mengumpulkan nomor ponsel, tetapi disarankan untuk melakukan validasi format sekali lagi di sisi server.
  4. setTimeout tidak cocok untuk lingkungan produksi, semua tugas terjadwal akan hilang setelah server restart. Untuk lingkungan produksi disarankan menggunakan antrian tugas persisten (seperti BullMQ + Redis) atau tugas terjadwal layanan cloud, simpan catatan pengingat ke database agar dapat dipulihkan setelah restart.
  5. Template harus lolos peninjauan sebelum dapat digunakan, jika saat dipanggil template berstatus menunggu peninjauan atau ditolak, API akan mengembalikan error 4001.
  6. API mengembalikan HTTP 200 tidak berarti SMS berhasil dikirim, harap periksa field code pada response body, jika tidak nol lihat penjelasan kode error untuk menyelidiki penyebabnya.
Icon Solid Transparent White Qiyu
Hubungi Sales