Calendly

Calendly est un outil de planification en ligne très répandu, mais sa fonctionnalité native de rappel par SMS ne couvre qu’un nombre limité de pays et régions, avec une couverture limitée en Asie-Pacifique, en Asie du Sud-Est, au Moyen-Orient et sur d’autres marchés. En intégrant les webhooks Calendly avec EngageLab SMS, vous pouvez envoyer des notifications SMS vers n’importe quel numéro dans le monde lorsqu’un rendez-vous est créé ou annulé, ou avant le début d’une réunion — comblant ainsi les lacunes géographiques des capacités natives de Calendly.

Prérequis

Avant de commencer, assurez-vous que les configurations suivantes sont en place :

Côté EngageLab

  • Le service SMS EngageLab est activé
  • Un modèle SMS a été créé et approuvé sur la page Gestion des modèles ; l’ID du modèle a été obtenu
  • Une clé API a été créée sur la page Clés API ; dev_key et dev_secret ont été obtenus

Côté Calendly

  • Vous disposez d’un abonnement Calendly Standard ou supérieur (les webhooks nécessitent un abonnement payant)
  • Un jeton d’accès personnel a été créé sur la page Integrations & apps → API and webhooks

Côté serveur

  • Vous disposez d’un serveur accessible depuis Internet avec un certificat HTTPS valide
  • Pour le développement et le débogage locaux, vous pouvez utiliser ngrok pour exposer temporairement un port local

Étape 1 : Préparer les modèles SMS

L’envoi de SMS via l’API nécessite des modèles préapprouvés ; le texte personnalisé ne peut pas être envoyé directement.

Connectez-vous à la console EngageLab, allez dans SMS → Gestion des modèles, et créez les trois modèles suivants pour chaque scénario :

Objectif du modèle Exemple de contenu
Confirmation de rendez-vous Hi {{name}}, your meeting has been confirmed for {{time}}. Looking forward to seeing you.
Avis d’annulation Hi {{name}}, your meeting appointment has been canceled. Please contact us to reschedule.
Rappel de réunion Hi {{name}}, you have a meeting starting in {{advance}}, scheduled for {{time}}. Please prepare in advance.

Après soumission des modèles, attendez l’approbation et notez les trois ID de modèle.

Astuce : Créez un modèle distinct pour chaque scénario — l’intention est plus claire et le taux d’approbation s’en trouve amélioré. Si un modèle contient des variables personnalisées (ex. {{name}}), vous devez transmettre les valeurs via le champ params lors de l’appel API ; sinon, les variables seront envoyées telles quelles.

Étape 2 : Créer un abonnement webhook dans Calendly

Calendly ne fournit pas d’interface visuelle pour la gestion des webhooks ; les abonnements doivent être créés via l’API.

Obtenir l’URI de l’organisation

Appelez d’abord le point de terminaison suivant pour obtenir l’URI de l’organisation de votre compte :

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"

            
Afficher ce bloc de code dans la fenêtre flottante

Repérez le champ current_organization dans la réponse, qui ressemble à :

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

            
Afficher ce bloc de code dans la fenêtre flottante

Créer l’abonnement webhook

Utilisez l’URI de l’organisation pour créer un webhook qui s’abonne aux événements de création et d’annulation de rendez-vous :

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

            
Afficher ce bloc de code dans la fenêtre flottante

Une fois créé, Calendly enverra une requête de vérification vers l’url fournie, et votre serveur doit renvoyer HTTP 200 pour finaliser la poignée de main. Les événements de rendez-vous et d’annulation suivants seront poussés vers cette adresse.

Remarque : L’url doit être une adresse HTTPS accessible publiquement. Pour le développement local, vous pouvez utiliser ngrok pour générer une adresse temporaire : ngrok http 3000, puis utiliser l’URL https://xxxx.ngrok.io affichée.

Étape 3 : Mettre en place le service récepteur de webhook

Voici un exemple complet de serveur Node.js qui reçoit les événements webhook Calendly et appelle l’API SMS EngageLab pour envoyer les messages.

Installer les dépendances

npm install express
              
              npm install express

            
Afficher ce bloc de code dans la fenêtre flottante

Code complet

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

            
Afficher ce bloc de code dans la fenêtre flottante

Configuration des variables d’environnement

Créez un fichier .env à la racine du projet avec les variables suivantes :

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

            
Afficher ce bloc de code dans la fenêtre flottante

Référence des champs clés

Les champs suivants dans la charge utile du webhook Calendly sont directement liés à l’envoi de SMS :

Champ Description
event Type d’événement : invitee.created (rendez-vous créé) ou invitee.canceled (rendez-vous annulé)
payload.text_reminder_number Numéro fourni par l’invité, avec indicatif pays ; peut être vide
payload.invitee.name Nom de l’invité
payload.scheduled_event.start_time Heure de début de la réunion au format ISO 8601

Scénarios étendus

Envoyer un SMS de suivi après une réunion

Ajoutez un déclencheur post-réunion dans scheduleReminders pour envoyer une enquête de satisfaction ou une invitation à un nouveau rendez-vous :

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

            
Afficher ce bloc de code dans la fenêtre flottante

Avertir également l’hôte

Lorsqu’un rendez-vous est créé, en plus de prévenir l’invité, vous pouvez envoyer un rappel à l’hôte de la réunion :

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

            
Afficher ce bloc de code dans la fenêtre flottante

Points importants

  1. Les webhooks doivent répondre par HTTP 200 dans les 2 secondes, sinon Calendly considère que la livraison a échoué et réessaie. Il est recommandé de renvoyer 200 d’abord, puis d’exécuter l’envoi de SMS de manière asynchrone pour éviter qu’une latence de l’API EngageLab ne fasse échouer à tort la livraison côté Calendly.
  2. Le champ text_reminder_number peut être vide — ajoutez des vérifications de nullité dans votre code pour ne pas transmettre un numéro vide à l’API SMS, ce qui provoquerait des erreurs.
  3. Les numéros doivent inclure l’indicatif pays, ex. +8618701235678 pour la Chine continentale. Calendly invite les utilisateurs au format international lors de la saisie du téléphone, mais une validation côté serveur est recommandée.
  4. setTimeout ne convient pas aux environnements de production — toutes les tâches planifiées sont perdues au redémarrage du serveur. En production, utilisez une file de tâches persistante (par ex. BullMQ + Redis) ou des tâches planifiées cloud, et stockez les rappels en base pour pouvoir les reprendre après redémarrage.
  5. Les modèles doivent être approuvés avant utilisation — si un modèle est en attente d’examen ou rejeté lors de l’appel, l’API renvoie une erreur 4001.
  6. Une réponse HTTP 200 de l’API ne garantit pas la livraison du SMS — vérifiez le champ code dans le corps de la réponse. Consultez la documentation des codes d’erreur pour les valeurs non nulles.
Icon Solid Transparent White Qiyu
Contactez-nous