Calendly
Calendlyは広く利用されているオンライン予約ツールですが、組み込みのSMSリマインダー機能は一部の国と地域にしか対応しておらず、アジア太平洋、東南アジア、中東などの市場ではカバレッジが限られています。Calendly WebhookとEngageLab SMSを組み合わせることで、予約の作成・キャンセル時やミーティング開始前に、世界中の任意の番号へSMS通知を送信でき、Calendly本来の機能の地理的なギャップを埋めることができます。
前提条件
開始する前に、次の設定が完了していることを確認してください。
EngageLab側
- EngageLab SMSサービスが有効化されている
- テンプレート管理ページでSMSテンプレートを作成・承認済みで、テンプレートIDを取得している
- API KeyページでAPIキーを作成済みで、
dev_keyとdev_secretを取得している
Calendly側
- Calendly Standardプラン以上を利用している(Webhook機能は有料プランが必要)
- Integrations & apps → API and webhooks ページでPersonal Access Tokenを作成済み
サーバー側
- 有効なHTTPS証明書が設定された、一般公開可能なサーバーを保有している
- ローカル開発・デバッグの場合は、ngrok を使用してローカルポートを一時的に公開できます
ステップ1:SMSテンプレートの準備
APIを呼び出してSMSを送信するには、事前に承認されたテンプレートが必要であり、カスタムテキストを直接渡すことはできません。
EngageLabコンソールにログインし、SMS → テンプレート管理 に移動して、シーンに応じて次の3つのテンプレートを作成します。
| テンプレートの用途 | テンプレート内容の例 |
|---|---|
| 予約確認 | Hello {{name}}, your meeting is confirmed for {{time}}. Looking forward to seeing you. |
| キャンセル通知 | Hello {{name}}, your meeting appointment has been canceled. Please contact us if you need to reschedule. |
| ミーティングリマインダー | Hello {{name}}, you have a meeting starting in {{advance}} at {{time}}. Please prepare in advance. |
テンプレートを送信した後、承認を待ち、3つのテンプレートIDを記録します。
推奨:シーンごとに独立したテンプレートを作成すると、意味がより明確になり、承認率も高まります。テンプレートにカスタム変数(例:
{{name}})が含まれる場合は、API呼び出し時にparamsフィールドで値を渡す必要があります。そうしないと、変数はそのまま配信されてしまいます。
ステップ2:CalendlyでのWebhookサブスクリプションの作成
CalendlyのWebhook管理にはビジュアルインターフェースがなく、API経由で作成する必要があります。
Organization URIの取得
まず、次のインターフェースを呼び出して、現在のアカウントのorganization URIを取得します。
curl https://api.calendly.com/users/me \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
レスポンス内の current_organization フィールドを見つけます。形式は次のとおりです。
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-organization-ID",
"scope": "organization"
}'
作成に成功すると、Calendlyは指定した url へ検証リクエストをプッシュし、サーバーはハンドシェイクを完了するために HTTP 200 を返す必要があります。その後、誰かが予約またはキャンセルするたびに、このアドレスへイベントがプッシュされます。
注意:
urlは一般公開可能なHTTPSアドレスである必要があります。ローカル開発時は、ngrokを使用して一時的なアドレスを生成します:ngrok http 3000を実行し、出力されたhttps://xxxx.ngrok.ioを入力します。
ステップ3:Webhook受信サービスのセットアップ
以下は、Calendly Webhookイベントを受信し、EngageLab SMS APIを呼び出してSMSを送信する、完全なNode.jsのサーバー側サンプルです。
依存関係のインストール
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 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 does not mean successful sending; check the code field
if (data.code && data.code !== 0) {
console.error(`SMS send failed: code=${data.code}, message=${data.message}`);
} else {
console.log(`SMS sent successfully: plan_id=${data.plan_id}, message_id=${data.message_id}`);
}
}
// Convert 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',
});
}
// Register pre-meeting reminder tasks (one at 24h and one at 1h)
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 Receiving Endpoint
app.post('/webhooks/calendly', async (req, res) => {
const { event, payload } = req.body;
const phone = payload.text_reminder_number; // Phone number provided by user during booking
const name = payload.invitee?.name ?? 'User';
const startTime = payload.scheduled_event?.start_time;
// Skip sending SMS if phone number is not 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) },
});
// Register pre-meeting reminder tasks
scheduleReminders({ phone, name, startTime });
} else if (event === 'invitee.canceled') {
// Send cancellation notification SMS
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CANCEL,
params: { name },
});
}
// Must return 200 within 2 seconds, otherwise Calendly considers the push failed and retries
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_notification_template_id
TEMPLATE_ID_REMINDER=meeting_reminder_template_id
主要フィールドの説明
Calendly Webhookがプッシュするpayloadのうち、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);
}
ホストへの同時通知
予約が作成された際、招待者への通知に加えて、ミーティングのホストにもリマインダーを送信できます。
if (event === 'invitee.created') {
// Notify invitee
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CONFIRM,
params: { name, time: formatTime(startTime) },
});
// Notify host
await sendSMS({
to: process.env.HOST_PHONE,
templateId: process.env.TEMPLATE_ID_HOST_NOTIFY,
params: { name, time: formatTime(startTime) },
});
}
注意事項
- Webhookは2秒以内に
HTTP 200を返す必要があります。そうしないと、Calendlyはプッシュが失敗したとみなして再試行します。EngageLab APIのレスポンス遅延によりCalendlyが誤判定するのを防ぐため、まず200を返してから、SMS送信ロジックを非同期で実行することをおすすめします。 text_reminder_numberフィールドは空の場合があります。そのため、空の番号をSMS APIに渡してエラーを引き起こさないよう、コード内でnullチェックを行う必要があります。- 電話番号の形式には国番号を含める必要があります。シンガポールの番号であれば
+6591234567のようになります。Calendlyは電話番号の収集時にユーザーへ国際形式での入力を促しますが、サーバー側でも形式チェックを行うことをおすすめします。 setTimeoutは本番環境には適していません。サーバーが再起動すると、すべてのスケジュールされたタスクが失われるためです。本番環境では、永続的なタスクキュー(BullMQ + Redisなど)やクラウドサービスのcronジョブを使用し、リマインダー記録をデータベースに保存して、再起動時に復元できるようにすることをおすすめします。- テンプレートは承認後にのみ使用できます。呼び出し時にテンプレートが審査待ちまたは却下されている場合、APIは
4001エラーを返します。 - APIからのHTTP 200レスポンスは、SMSが正常に送信されたことを意味しません。レスポンスボディの
codeフィールドを確認してください。0以外の場合は、エラーコードの説明を参照してトラブルシューティングを行ってください。










