Logo Site EngageLab Mark Colored TransparentDocumentation
Rechercher

Calendly

Calendly est un outil de prise de rendez-vous en ligne largement utilisé, mais sa fonction de rappel par SMS intégrée ne prend en charge que certains pays et régions, laissant des marchés comme l'Asie-Pacifique, l'Asie du Sud-Est et le Moyen-Orient avec une couverture limitée. En associant les Webhooks Calendly à EngageLab SMS, vous pouvez envoyer des notifications SMS vers n'importe quel numéro dans le monde lors de la création ou de l'annulation d'un rendez-vous, ainsi qu'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 terminées :

Côté EngageLab

  • Le service EngageLab SMS est activé
  • Des modèles SMS ont été créés et validés sur la page de gestion des modèles, et les ID de modèle ont été obtenus
  • Une clé API a été créée sur la page des clés API, et les dev_key et dev_secret ont été obtenus

Côté Calendly

  • Vous disposez d'un forfait Calendly Standard ou supérieur (la fonctionnalité Webhook nécessite un forfait payant)
  • Un Personal Access Token a été créé sur la page Integrations & apps → API and webhooks

Côté serveur

  • Vous disposez d'un serveur accessible publiquement et configuré avec un certificat HTTPS valide
  • Pour le développement et le débogage en local, vous pouvez utiliser ngrok pour exposer temporairement un port local

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

L'appel de l'API pour envoyer des SMS nécessite des modèles pré-validés ; la transmission directe de texte personnalisé n'est pas prise en charge.

Connectez-vous à la console EngageLab, accédez à SMS → Gestion des modèles et créez les trois modèles suivants selon le scénario :

Usage du modèle Exemple de contenu de modèle
Confirmation de rendez-vous Bonjour {{name}}, votre réunion est confirmée pour le {{time}}. Au plaisir de vous rencontrer.
Notification d'annulation Bonjour {{name}}, votre rendez-vous de réunion a été annulé. Contactez-nous si vous souhaitez le reprogrammer.
Rappel de réunion Bonjour {{name}}, vous avez une réunion qui commence dans {{advance}}, le {{time}}. Merci de vous préparer à l'avance.

Après avoir soumis les modèles, attendez leur validation et notez les trois ID de modèle.

Recommandation : créer un modèle indépendant pour chaque scénario rend la sémantique plus claire et augmente le taux de validation. Si un modèle contient des variables personnalisées (comme {{name}}), vous devez transmettre les valeurs via le champ params lors de l'appel à l'API, sinon les variables seront envoyées telles quelles.

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

La gestion des Webhooks de Calendly ne dispose pas d'interface visuelle et doit se faire via l'API.

Obtenir l'URI de l'organisation

Appelez d'abord l'interface suivante pour obtenir l'URI de l'organisation du compte actuel :

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, au format suivant :

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

            
Afficher ce bloc de code dans la fenêtre flottante

Une fois la création réussie, Calendly envoie une requête de vérification à l'url fournie, et le serveur doit renvoyer HTTP 200 pour finaliser la prise de contact. Par la suite, chaque fois qu'une personne prend ou annule un rendez-vous, un événement est envoyé à cette adresse.

Remarque : l'url doit être une adresse HTTPS accessible publiquement. Pendant le développement local, utilisez ngrok pour générer une adresse temporaire : ngrok http 3000, puis renseignez l'adresse https://xxxx.ngrok.io ainsi générée.

Étape 3 : Mettre en place le service de réception des Webhooks

Voici un exemple complet de serveur Node.js qui, après réception des événements Webhook de Calendly, appelle l'API EngageLab SMS pour envoyer un SMS.

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()); // Authentification EngageLab : base64(dev_key:dev_secret) const ENGAGELAB_AUTH = Buffer.from( `${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}` ).toString('base64'); // Appel de l'API EngageLab SMS pour envoyer un 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 ne signifie pas que l'envoi a réussi ; vérifiez le champ code if (data.code && data.code !== 0) { console.error(`Échec de l'envoi du SMS : code=${data.code}, message=${data.message}`); } else { console.log(`SMS envoyé avec succès : plan_id=${data.plan_id}, message_id=${data.message_id}`); } } // Convertit une chaîne de temps ISO en heure locale (Asia/Shanghai) function formatTime(isoString) { return new Date(isoString).toLocaleString('fr-FR', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } // Planifie les tâches de rappel avant la réunion (une à 24 h et une à 1 h) function scheduleReminders({ phone, name, startTime }) { const meetingTime = new Date(startTime).getTime(); const now = Date.now(); const reminders = [ { advance: '24 heures', triggerAt: meetingTime - 24 * 60 * 60 * 1000 }, { advance: '1 heure', triggerAt: meetingTime - 60 * 60 * 1000 }, ]; for (const { advance, triggerAt } of reminders) { const delay = triggerAt - now; if (delay <= 0) continue; // L'heure de déclenchement est passée, on ignore setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_REMINDER, params: { name, time: formatTime(startTime), advance }, }); }, delay); } } // Point de terminaison de réception des Webhooks app.post('/webhooks/calendly', async (req, res) => { const { event, payload } = req.body; const phone = payload.text_reminder_number; // Numéro de téléphone renseigné par l'utilisateur lors de la réservation const name = payload.invitee?.name ?? 'Utilisateur'; const startTime = payload.scheduled_event?.start_time; // Ignorer l'envoi du SMS si aucun numéro de téléphone n'est renseigné if (!phone) { console.log('Aucun numéro de téléphone renseigné pour ce rendez-vous, notification SMS ignorée'); return res.sendStatus(200); } if (event === 'invitee.created') { // Envoyer le SMS de confirmation de rendez-vous await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) }, }); // Planifier les tâches de rappel avant la réunion scheduleReminders({ phone, name, startTime }); } else if (event === 'invitee.canceled') { // Envoyer le SMS de notification d'annulation await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CANCEL, params: { name }, }); } // Doit renvoyer 200 dans les 2 secondes, sinon Calendly considère l'envoi comme échoué et réessaie 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());

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

// Appel de l'API EngageLab SMS pour envoyer un 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 ne signifie pas que l'envoi a réussi ; vérifiez le champ code
  if (data.code && data.code !== 0) {
    console.error(`Échec de l'envoi du SMS : code=${data.code}, message=${data.message}`);
  } else {
    console.log(`SMS envoyé avec succès : plan_id=${data.plan_id}, message_id=${data.message_id}`);
  }
}

// Convertit une chaîne de temps ISO en heure locale (Asia/Shanghai)
function formatTime(isoString) {
  return new Date(isoString).toLocaleString('fr-FR', {
    timeZone: 'Asia/Shanghai',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  });
}

// Planifie les tâches de rappel avant la réunion (une à 24 h et une à 1 h)
function scheduleReminders({ phone, name, startTime }) {
  const meetingTime = new Date(startTime).getTime();
  const now = Date.now();

  const reminders = [
    { advance: '24 heures', triggerAt: meetingTime - 24 * 60 * 60 * 1000 },
    { advance: '1 heure',  triggerAt: meetingTime -      60 * 60 * 1000 },
  ];

  for (const { advance, triggerAt } of reminders) {
    const delay = triggerAt - now;
    if (delay <= 0) continue; // L'heure de déclenchement est passée, on ignore

    setTimeout(async () => {
      await sendSMS({
        to: phone,
        templateId: process.env.TEMPLATE_ID_REMINDER,
        params: { name, time: formatTime(startTime), advance },
      });
    }, delay);
  }
}

// Point de terminaison de réception des Webhooks
app.post('/webhooks/calendly', async (req, res) => {
  const { event, payload } = req.body;

  const phone     = payload.text_reminder_number;     // Numéro de téléphone renseigné par l'utilisateur lors de la réservation
  const name      = payload.invitee?.name ?? 'Utilisateur';
  const startTime = payload.scheduled_event?.start_time;

  // Ignorer l'envoi du SMS si aucun numéro de téléphone n'est renseigné
  if (!phone) {
    console.log('Aucun numéro de téléphone renseigné pour ce rendez-vous, notification SMS ignorée');
    return res.sendStatus(200);
  }

  if (event === 'invitee.created') {
    // Envoyer le SMS de confirmation de rendez-vous
    await sendSMS({
      to: phone,
      templateId: process.env.TEMPLATE_ID_CONFIRM,
      params: { name, time: formatTime(startTime) },
    });

    // Planifier les tâches de rappel avant la réunion
    scheduleReminders({ phone, name, startTime });

  } else if (event === 'invitee.canceled') {
    // Envoyer le SMS de notification d'annulation
    await sendSMS({
      to: phone,
      templateId: process.env.TEMPLATE_ID_CANCEL,
      params: { name },
    });
  }

  // Doit renvoyer 200 dans les 2 secondes, sinon Calendly considère l'envoi comme échoué et réessaie
  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 et renseignez les variables suivantes :

ENGAGELAB_DEV_KEY=votre_dev_key ENGAGELAB_DEV_SECRET=votre_dev_secret TEMPLATE_ID_CONFIRM=id_du_modele_de_confirmation_de_rendez_vous TEMPLATE_ID_CANCEL=id_du_modele_de_notification_d_annulation TEMPLATE_ID_REMINDER=id_du_modele_de_rappel_de_reunion
              
              ENGAGELAB_DEV_KEY=votre_dev_key
ENGAGELAB_DEV_SECRET=votre_dev_secret

TEMPLATE_ID_CONFIRM=id_du_modele_de_confirmation_de_rendez_vous
TEMPLATE_ID_CANCEL=id_du_modele_de_notification_d_annulation
TEMPLATE_ID_REMINDER=id_du_modele_de_rappel_de_reunion

            
Afficher ce bloc de code dans la fenêtre flottante

Description des champs clés

Dans le payload envoyé par le Webhook Calendly, les champs suivants sont directement liés à l'envoi du SMS :

Champ Description
event Type d'événement, invitee.created (création de rendez-vous) ou invitee.canceled (annulation de rendez-vous)
payload.text_reminder_number Numéro de téléphone renseigné par l'invité, incluant l'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 la réunion

Ajoutez à scheduleReminders une tâche déclenchée après la réunion, afin d'envoyer une enquête de satisfaction ou une invitation à un prochain rendez-vous :

// À ajouter dans la fonction scheduleReminders const followUpAt = meetingTime + 30 * 60 * 1000; // 30 minutes après la fin de la réunion const followUpDelay = followUpAt - now; if (followUpDelay > 0) { setTimeout(async () => { await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_FOLLOWUP, params: { name }, }); }, followUpDelay); }
              
              // À ajouter dans la fonction scheduleReminders
const followUpAt = meetingTime + 30 * 60 * 1000; // 30 minutes après la fin de la réunion
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

Notifier simultanément l'organisateur

Lors de la création d'un rendez-vous, en plus de notifier l'invité, vous pouvez également envoyer un rappel à l'organisateur de la réunion :

if (event === 'invitee.created') { // Notifier l'invité await sendSMS({ to: phone, templateId: process.env.TEMPLATE_ID_CONFIRM, params: { name, time: formatTime(startTime) }, }); // Notifier l'organisateur await sendSMS({ to: process.env.HOST_PHONE, templateId: process.env.TEMPLATE_ID_HOST_NOTIFY, params: { name, time: formatTime(startTime) }, }); }
              
              if (event === 'invitee.created') {
  // Notifier l'invité
  await sendSMS({
    to: phone,
    templateId: process.env.TEMPLATE_ID_CONFIRM,
    params: { name, time: formatTime(startTime) },
  });

  // Notifier l'organisateur
  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 d'attention

  1. Le Webhook doit répondre par HTTP 200 dans les 2 secondes, sinon Calendly considère l'envoi comme échoué et réessaie. Il est recommandé de renvoyer d'abord 200, puis d'exécuter la logique d'envoi du SMS de façon asynchrone, afin d'éviter que Calendly ne se trompe à cause d'un délai de réponse de l'API EngageLab.
  2. Le champ text_reminder_number peut être vide ; vous devez gérer ce cas nul dans votre code pour éviter de transmettre un numéro vide à l'API SMS, ce qui provoquerait une erreur.
  3. Le format du numéro de téléphone doit inclure l'indicatif pays, par exemple +6591234567 pour un numéro de Singapour. Calendly guide les utilisateurs vers le format international lors de la collecte des numéros, mais il est recommandé d'effectuer une validation du format côté serveur.
  4. setTimeout n'est pas adapté à un environnement de production, car toutes les tâches planifiées sont perdues au redémarrage du serveur. En production, il est recommandé d'utiliser une file de tâches persistante (comme BullMQ + Redis) ou les tâches planifiées d'un service cloud, en stockant les rappels dans une base de données afin de pouvoir les restaurer après un redémarrage.
  5. Un modèle ne peut être utilisé qu'après validation ; si le modèle est en attente de validation ou refusé au moment de l'appel, l'API renverra une erreur 4001.
  6. Une réponse HTTP 200 de l'API ne signifie pas que le SMS a été envoyé avec succès ; vérifiez le champ code dans le corps de la réponse, et s'il est différent de zéro, consultez les explications des codes d'erreur pour en identifier la cause.
Icon Solid Transparent White Qiyu
Contactez-nous