Calendly

Calendly ist ein weit verbreitetes Online-Terminbuchungstool, dessen integrierte SMS-Erinnerung jedoch nur in einer begrenzten Zahl von Ländern und Regionen verfügbar ist — mit eingeschränkter Abdeckung in Asien-Pazifik, Südostasien, dem Nahen Osten und anderen Märkten. Durch die Anbindung von Calendly-Webhooks an EngageLab SMS können Sie bei Terminerstellung, -absage oder vor Beginn eines Meetings SMS-Benachrichtigungen an Rufnummern weltweit senden und so die geografischen Lücken der nativen Calendly-Funktionen schließen.

Voraussetzungen

Bevor Sie starten, sollten folgende Konfigurationen abgeschlossen sein:

EngageLab-Seite

  • Der EngageLab-SMS-Dienst ist aktiviert
  • Eine SMS-Vorlage wurde auf der Seite „Vorlagenverwaltung“ erstellt und freigegeben; die Vorlagen-ID liegt vor
  • Auf der Seite „API-Schlüssel“ wurde ein API-Schlüssel erstellt; dev_key und dev_secret liegen vor

Calendly-Seite

  • Sie verfügen über einen Calendly-Standard-Tarif oder höher (Webhooks erfordern einen kostenpflichtigen Plan)
  • Auf der Seite Integrationen & Apps → API und Webhooks wurde ein persönliches Zugriffstoken erstellt

Server-Seite

  • Sie betreiben einen Server mit öffentlichem Internetzugang und gültigem HTTPS-Zertifikat
  • Für lokale Entwicklung und Fehlersuche können Sie mit ngrok vorübergehend einen lokalen Port freigeben

Schritt 1: SMS-Vorlagen vorbereiten

Der Versand per API setzt freigegebene Vorlagen voraus; freier Fließtext kann nicht direkt versendet werden.

Melden Sie sich in der EngageLab-Konsole an, öffnen Sie SMS → Vorlagenverwaltung und legen Sie für jedes Szenario die folgenden drei Vorlagen an:

Zweck der Vorlage Beispielinhalt
Terminbestätigung Hi {{name}}, your meeting has been confirmed for {{time}}. Looking forward to seeing you.
Absagehinweis Hi {{name}}, your meeting appointment has been canceled. Please contact us to reschedule.
Terminerinnerung Hi {{name}}, you have a meeting starting in {{advance}}, scheduled for {{time}}. Please prepare in advance.

Nach dem Einreichen warten Sie auf die Freigabe und notieren sich die drei Vorlagen-IDs.

Hinweis: Legen Sie pro Szenario eine eigene Vorlage an — das macht die Absicht klarer und verbessert die Freigabequote. Enthält eine Vorlage eigene Variablen (z. B. {{name}}), müssen die Werte beim API-Aufruf im Feld params übergeben werden; andernfalls werden die Platzhalter unverändert mitgesendet.

Schritt 2: Webhook-Abonnement in Calendly anlegen

Calendly bietet keine grafische Oberfläche zur Webhook-Verwaltung; Abonnements müssen über die API erstellt werden.

Organisations-URI abrufen

Rufen Sie zunächst folgenden Endpunkt auf, um die Organisations-URI Ihres Kontos zu erhalten:

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 in der Antwort das Feld current_organization, etwa in dieser Form:

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 für Terminerstellung und -absage anzulegen:

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Nach der Erstellung sendet Calendly eine Verifizierungsanfrage an die angegebene url; Ihr Server muss mit HTTP 200 antworten, um den Handshake abzuschließen. Anschließend werden Termin- und Absageereignisse an diese Adresse übermittelt.

Hinweis: Die url muss öffentlich per HTTPS erreichbar sein. Für die lokale Entwicklung können Sie mit ngrok eine temporäre Adresse erzeugen: ngrok http 3000, dann die ausgegebene https://xxxx.ngrok.io-URL verwenden.

Schritt 3: Webhook-Empfangsdienst implementieren

Nachfolgend ein vollständiges Node.js-Beispiel, das Calendly-Webhook-Ereignisse entgegennimmt und die EngageLab-SMS-API zum Versand aufruft.

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()); const ENGAGELAB_AUTH = Buffer.from( `${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}` ).toString('base64'); 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(); 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}`); } } 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', }); } 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; setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_REMINDER, params: { name, time: formatTime(startTime), advance } }); }, delay); } } app.post('/webhooks/calendly', async (req, res) => { const { event, payload } = req.body; const phone = payload.text_reminder_number; const name = payload.invitee?.name ?? 'User'; const startTime = payload.scheduled_event?.start_time; if (!phone) { console.log('No phone number provided, skipping'); return res.sendStatus(200); } if (event === 'invitee.created') { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) } }); scheduleReminders({ phone, name, startTime }); } else if (event === 'invitee.canceled') { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CANCEL, params: { name } }); } 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());

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

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

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

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;
    setTimeout(async () => {
      await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_REMINDER, params: { name, time: formatTime(startTime), advance } });
    }, delay);
  }
}

app.post('/webhooks/calendly', async (req, res) => {
  const { event, payload } = req.body;
  const phone     = payload.text_reminder_number;
  const name      = payload.invitee?.name ?? 'User';
  const startTime = payload.scheduled_event?.start_time;
  if (!phone) { console.log('No phone number provided, skipping'); return res.sendStatus(200); }
  if (event === 'invitee.created') {
    await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) } });
    scheduleReminders({ phone, name, startTime });
  } else if (event === 'invitee.canceled') {
    await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CANCEL, params: { name } });
  }
  res.sendStatus(200);
});

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Umgebungsvariablen

Legen Sie im Projektstamm eine .env-Datei mit folgenden Variablen an:

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

            
Diesen Codeblock im schwebenden Fenster anzeigen

Referenz wichtiger Felder

Feld Beschreibung
event Ereignistyp: invitee.created (Termin erstellt) oder invitee.canceled (Termin abgesagt)
payload.text_reminder_number Vom Eingeladenen angegebene Rufnummer inkl. Landesvorwahl; kann leer sein
payload.invitee.name Name des Eingeladenen
payload.scheduled_event.start_time Beginn des Meetings im ISO-8601-Format

Erweiterte Szenarien

Follow-up-SMS nach dem Meeting

Ergänzen Sie in scheduleReminders einen Trigger nach dem Meeting, um eine Zufriedenheitsumfrage oder eine Einladung zum Folgetermin zu senden:

const followUpAt = meetingTime + 30 * 60 * 1000; const followUpDelay = followUpAt - now; if (followUpDelay > 0) { setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_FOLLOWUP, params: { name } }); }, followUpDelay); }
              
              const followUpAt = meetingTime + 30 * 60 * 1000;
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

Auch den Gastgeber benachrichtigen

if (event === 'invitee.created') { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) } }); await sendSMS({ to: process.env.HOST_PHONE, templateId: process.env.TEMPLATE_ID_HOST_NOTIFY, params: { name, time: formatTime(startTime) } }); }
              
              if (event === 'invitee.created') {
  await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) } });
  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

Wichtige Hinweise

  1. Webhooks müssen innerhalb von 2 Sekunden mit HTTP 200 antworten, sonst gilt der Push für Calendly als fehlgeschlagen und wird wiederholt.
  2. Das Feld text_reminder_number kann leer sein — bauen Sie entsprechende Prüfungen in Ihren Code ein.
  3. Rufnummern müssen die Landesvorwahl enthalten, z. B. +8618701235678.
  4. setTimeout ist für Produktionsumgebungen ungeeignet — verwenden Sie eine persistente Auftragswarteschlange (z. B. BullMQ + Redis).
  5. Vorlagen müssen vor der Nutzung freigegeben sein — andernfalls liefert die API einen 4001-Fehler.
  6. Eine HTTP-200-Antwort der API garantiert keinen erfolgreichen SMS-Zustellweg — prüfen Sie das Feld code. Bei Werten ungleich null siehe die Dokumentation zu Fehlercodes.
Icon Solid Transparent White Qiyu
Vertrieb kontaktieren