Logo Site EngageLab Mark Colored TransparentDokumentation
Suchen

Calendly

Calendly ist ein weit verbreitetes Online-Terminplanungstool, aber seine integrierte SMS-Erinnerungsfunktion unterstützt nur bestimmte Länder und Regionen, sodass Märkte wie der asiatisch-pazifische Raum, Südostasien und der Nahe Osten nur eingeschränkt abgedeckt sind. Durch die Kombination von Calendly-Webhooks mit EngageLab SMS können Sie SMS-Benachrichtigungen an jede beliebige Nummer weltweit senden, wenn Termine erstellt oder storniert werden und bevor Meetings beginnen, und so die geografischen Lücken der nativen Funktionen von Calendly schließen.

Voraussetzungen

Bevor Sie beginnen, stellen Sie bitte sicher, dass die folgenden Konfigurationen abgeschlossen sind:

Auf der EngageLab-Seite

  • Der EngageLab SMS Dienst ist aktiviert
  • SMS-Vorlagen wurden auf der Seite Vorlagenverwaltung erstellt und genehmigt und Vorlagen-IDs erhalten
  • API Keys wurden auf der Seite API Key erstellt und dev_key und dev_secret erhalten

Auf der Calendly-Seite

  • Sie haben einen Calendly Standard-Plan oder höher (die Webhook-Funktion erfordert einen kostenpflichtigen Plan)
  • Auf der Seite Integrations & apps → API and webhooks wurde ein Personal Access Token erstellt

Auf der Serverseite

  • Sie haben einen öffentlich zugänglichen Server, der mit einem gültigen HTTPS-Zertifikat konfiguriert ist
  • Für lokale Entwicklung und Debugging können Sie ngrok verwenden, um lokale Ports temporär freizugeben

Schritt 1: SMS-Vorlagen vorbereiten

Der Aufruf der API zum Senden von SMS erfordert vorab genehmigte Vorlagen; das direkte Übergeben von benutzerdefiniertem Text wird nicht unterstützt.

Melden Sie sich bei der EngageLab Konsole an, gehen Sie zu SMS → Vorlagenverwaltung und erstellen Sie je nach Szenario die folgenden drei Vorlagen:

Vorlagenzweck Beispiel für Vorlageninhalt
Terminbestätigung Hello {{name}}, your meeting is confirmed for {{time}}. Looking forward to seeing you.
Stornierungsbenachrichtigung Hello {{name}}, your meeting appointment has been canceled. Please contact us if you need to reschedule.
Meeting-Erinnerung Hello {{name}}, you have a meeting starting in {{advance}} at {{time}}. Please prepare in advance.

Warten Sie nach dem Einreichen der Vorlagen auf die Genehmigung und notieren Sie sich die drei Vorlagen-IDs.

Empfehlung: Das Erstellen unabhängiger Vorlagen für jedes Szenario sorgt für klarere Semantik und höhere Genehmigungsraten. Wenn die Vorlage benutzerdefinierte Variablen enthält (z. B. {{name}}), müssen Sie beim Aufruf der API Werte über das Feld params übergeben, andernfalls werden die Variablen unverändert zugestellt.

Schritt 2: Webhook-Abonnement in Calendly erstellen

Die Webhook-Verwaltung von Calendly verfügt über keine visuelle Oberfläche und muss über die API erstellt werden.

Organisations-URI abrufen

Rufen Sie zunächst die folgende Schnittstelle auf, um die Organisations-URI des aktuellen Kontos abzurufen:

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"

            
Diesen Codeblock im schwebenden Fenster anzeigen

Suchen Sie das Feld current_organization in der Antwort, formatiert wie folgt:

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Webhook-Abonnement erstellen

Verwenden Sie die Organisations-URI, um einen Webhook zu erstellen, der Ereignisse zur Terminerstellung und -stornierung abonniert:

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Nach erfolgreicher Erstellung übermittelt Calendly eine Verifizierungsanfrage an die angegebene url, und der Server muss HTTP 200 zurückgeben, um den Handshake abzuschließen. Anschließend wird bei jeder Terminbuchung oder -stornierung ein Ereignis an diese Adresse übermittelt.

Hinweis: Die url muss eine öffentlich zugängliche HTTPS-Adresse sein. Verwenden Sie während der lokalen Entwicklung ngrok, um eine temporäre Adresse zu generieren: ngrok http 3000, und tragen Sie die ausgegebene https://xxxx.ngrok.io ein.

Schritt 3: Webhook-Empfangsdienst einrichten

Nachfolgend finden Sie ein vollständiges serverseitiges Node.js-Beispiel, das Calendly-Webhook-Ereignisse empfängt und die EngageLab SMS API aufruft, um eine SMS zu senden.

Abhängigkeiten installieren

npm install express
              
              npm install express

            
Diesen Codeblock im schwebenden Fenster anzeigen

Vollständiger Code

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Konfiguration der Umgebungsvariablen

Erstellen Sie eine .env-Datei im Stammverzeichnis des Projekts und tragen Sie die folgenden Variablen ein:

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Beschreibung der wichtigsten Felder

Im vom Calendly-Webhook übermittelten Payload stehen die folgenden Felder in direktem Zusammenhang mit dem SMS-Versand:

Feld Beschreibung
event Ereignistyp, invitee.created (Terminerstellung) oder invitee.canceled (Terminstornierung)
payload.text_reminder_number Vom Eingeladenen ausgefüllte Telefonnummer, einschließlich Ländervorwahl; kann leer sein
payload.invitee.name Name des Eingeladenen
payload.scheduled_event.start_time Meeting-Startzeit, ISO 8601-Format

Erweiterte Szenarien

Follow-up-SMS nach dem Meeting senden

Fügen Sie scheduleReminders eine nach dem Meeting ausgelöste Aufgabe hinzu, um eine Zufriedenheitsumfrage oder eine Einladung zum nächsten Termin zu senden:

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Gleichzeitige Benachrichtigung des Gastgebers

Wenn ein Termin erstellt wird, kann neben der Benachrichtigung des Eingeladenen auch eine Erinnerung an den Meeting-Gastgeber gesendet werden:

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Hinweise

  1. Der Webhook muss innerhalb von 2 Sekunden mit HTTP 200 antworten, andernfalls betrachtet Calendly die Übermittlung als fehlgeschlagen und wiederholt sie. Es wird empfohlen, zuerst 200 zurückzugeben und die SMS-Sendelogik anschließend asynchron auszuführen, um zu verhindern, dass Calendly aufgrund von Antwortverzögerungen der EngageLab API eine Fehleinschätzung trifft.
  2. Das Feld text_reminder_number kann leer sein, daher müssen Sie in Ihrem Code Null-Prüfungen durchführen, um zu vermeiden, dass eine leere Nummer an die SMS-API übergeben wird und Fehler verursacht.
  3. Das Telefonnummernformat muss die Ländervorwahl enthalten, z. B. +6591234567 für eine Nummer aus Singapur. Calendly leitet Nutzer an, beim Erfassen von Telefonnummern internationale Formate einzugeben, es wird jedoch empfohlen, serverseitig eine Formatprüfung durchzuführen.
  4. setTimeout ist für Produktionsumgebungen nicht geeignet, da alle geplanten Aufgaben verloren gehen, wenn der Server neu gestartet wird. In einer Produktionsumgebung wird empfohlen, persistente Aufgaben-Warteschlangen (wie BullMQ + Redis) oder Cron-Jobs von Cloud-Diensten zu verwenden, um Erinnerungsdatensätze in einer Datenbank zu speichern, damit sie nach einem Neustart wiederhergestellt werden können.
  5. Vorlagen müssen genehmigt sein, bevor sie verwendet werden können. Wenn die Vorlage beim Aufruf in Prüfung oder abgelehnt ist, gibt die API einen 4001-Fehler zurück.
  6. Eine HTTP-200-Antwort der API bedeutet nicht, dass die SMS erfolgreich gesendet wurde. Bitte überprüfen Sie das Feld code im Antwortkörper. Wenn es ungleich null ist, finden Sie in der Fehlercode-Erläuterung Hinweise zur Fehlerbehebung.
Icon Solid Transparent White Qiyu
Vertrieb kontaktieren