Calendly

Calendly は広く使われているオンライン予約ツールですが、標準の SMS リマインダーは対応国・地域が限られており、アジア太平洋・東南アジア・中東などではカバーが限定的です。Calendly Webhooks と EngageLab SMS を連携すると、予約の作成・キャンセル時やミーティング開始前に、世界中の電話番号へ SMS 通知を送れます。Calendly 標準機能では届きにくい地域を補完できます。

前提条件

始める前に、次の設定が済んでいることを確認してください。

EngageLab 側

  • EngageLab SMS が有効になっていること
  • テンプレート管理で SMS テンプレートを作成し承認済みであること。テンプレート ID を取得済みであること
  • API Keys ページで API キーを作成し、dev_keydev_secret を取得済みであること

Calendly 側

  • Calendly Standard 以上のプランであること(Webhook は有料プランが必要です)
  • Integrations & apps → API and webhooks で Personal Access Token を作成済みであること

サーバー側

  • インターネットから到達可能なサーバーと、有効な HTTPS 証明書が設定されていること
  • ローカル開発・デバッグでは ngrok で一時的にローカルポートを公開できます

ステップ 1: SMS テンプレートの準備

API 経由の送信には事前承認済みテンプレートが必要で、自由文をそのまま送ることはできません。

EngageLab コンソールにログインし、SMS → テンプレート管理へ進み、シナリオごとに次の 3 種類のテンプレートを作成します。

テンプレート用途 サンプル文言
予約確定 {{name}}様、ミーティングは{{time}}に確定しました。お会いできるのを楽しみにしています。
キャンセル通知 {{name}}様、ご予約のミーティングはキャンセルされました。再調整の際はご連絡ください。
ミーティングリマインダー {{name}}様、{{advance}}後にミーティングが{{time}}から始まります。事前のご準備をお願いします。

テンプレートを提出したら承認を待ち、3 つのテンプレート ID を控えておきます。

ヒント: シナリオごとにテンプレートを分けると意図が明確になり、承認されやすくなります。カスタム変数(例: {{name}})を含める場合は、API 呼び出し時に params で値を渡す必要があります。渡さないと変数名がそのまま送信されます。

ステップ 2: Calendly で Webhook 購読を作成する

Calendly には Webhook 管理のビジュアル UI がないため、API で購読を作成します。

Organization URI の取得

まず、次のエンドポイントを呼び出してアカウントの 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 購読の作成

organization 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://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 を返すとハンドシェイクが完了します。その後、予約・キャンセルイベントがこの URL にプッシュされます。

注意: url は公開 HTTPS で到達可能である必要があります。ローカル開発では ngrok で一時 URL を取得できます: ngrok http 3000 の出力にある https://xxxx.ngrok.io などを使います。

ステップ 3: Webhook 受信サービスの実装

以下は、Calendly の Webhook イベントを受信し、EngageLab SMS API を呼び出してメッセージを送る Node.js サーバーの完全な例です。

依存関係のインストール

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 のペイロードのうち、SMS 送信に直接関係するフィールドは次のとおりです。

フィールド 説明
event イベント種別: invitee.created(予約作成)または invitee.canceled(予約キャンセル)
payload.text_reminder_number 招待された人が入力した電話番号(国番号を含む)。空の場合あり
payload.invitee.name 招待された人の名前
payload.scheduled_event.start_time ミーティング開始時刻(ISO 8601 形式)

拡張シナリオ

ミーティング後のフォローアップ SMS

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

            
このコードブロックはフローティングウィンドウ内に表示されます

ホストにも通知する

予約作成時に、招待された人だけでなくミーティングのホストにも SMS を送れます。

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. Webhook は 2 秒以内に HTTP 200 を返す必要があります。 超えると Calendly はプッシュ失敗とみなして再送します。先に 200 を返し、SMS 送信は非同期で実行することを推奨します。EngageLab API の遅延で Calendly が誤判定するのを防げます。
  2. text_reminder_number は空の場合があります。 コードで null チェックを行い、空の番号を SMS API に渡さないようにしてください。エラーの原因になります。
  3. 電話番号には国番号を含めてください。 例: 中国本土は +8618701235678。Calendly は電話番号入力時に国際形式を促しますが、サーバー側での形式検証も推奨します。
  4. setTimeout は本番向きではありません。 サーバー再起動でスケジュールはすべて失われます。本番では BullMQ + Redis などの永続キューやクラウドの定期実行を使い、リマインダー記録を DB に残して復旧できるようにしてください。
  5. テンプレートは利用前に承認が必要です。 審査中や却下のテンプレートを呼ぶと、API は 4001 エラーを返します。
  6. API が HTTP 200 を返しても SMS 配信成功を保証しません。 レスポンス本文の code を確認してください。0 以外の値は エラーコードのドキュメント を参照してください。
Icon Solid Transparent White Qiyu
お問い合わせ