Logo Site EngageLab Mark Colored TransparentDokumen
Cari

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_key dan dev_secret telah 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 bidang params saat 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"
              
              curl https://api.calendly.com/users/me \
  -H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"

            
Tampilkan blok kode ini di jendela mengambang

Temukan bidang current_organization dalam respons, bentuknya seperti:

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

            
Tampilkan blok kode ini di jendela mengambang

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

            
Tampilkan blok kode ini di jendela mengambang

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: url harus berupa alamat HTTPS yang dapat diakses publik. Untuk pengembangan lokal, Anda dapat memakai ngrok untuk menghasilkan alamat sementara: ngrok http 3000, lalu gunakan URL https://xxxx.ngrok.io dari 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
              
              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()); // 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'));

            
Tampilkan blok kode ini di jendela mengambang

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
              
              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

            
Tampilkan blok kode ini di jendela mengambang

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

            
Tampilkan blok kode ini di jendela mengambang

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

            
Tampilkan blok kode ini di jendela mengambang

Catatan Penting

  1. Webhook harus merespons dengan HTTP 200 dalam 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.
  2. Bidang text_reminder_number boleh kosong — tambahkan pengecekan null di kode agar tidak meneruskan nomor kosong ke API SMS, yang akan menyebabkan kesalahan.
  3. Nomor telepon harus menyertakan kode negara, mis. +8618701235678 untuk nomor Tiongkok daratan. Calendly meminta pengguna memasukkan format internasional saat mengumpulkan nomor telepon, tetapi validasi format di sisi server disarankan.
  4. setTimeout tidak 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.
  5. Template harus disetujui sebelum dipakai — jika template masih ditinjau atau ditolak saat dipanggil, API akan mengembalikan kesalahan 4001.
  6. Respons HTTP 200 dari API tidak menjamin pengiriman SMS berhasil — periksa bidang code dalam body respons. Lihat dokumentasi kode kesalahan untuk nilai bukan nol.
Icon Solid Transparent White Qiyu
Hubungi Sales