Calendly

Calendly es una herramienta de programación en línea muy utilizada, pero su recordatorio SMS integrado solo admite un número limitado de países y regiones, con cobertura limitada en Asia-Pacífico, el Sudeste Asiático, Oriente Medio y otros mercados. Integrando los webhooks de Calendly con EngageLab SMS, puede enviar notificaciones por SMS a cualquier número del mundo cuando se crea o cancela una cita o antes de que comience una reunión — cubriendo las lagunas geográficas de las capacidades nativas de Calendly.

Requisitos previos

Antes de empezar, asegúrese de tener completada la siguiente configuración:

Lado EngageLab

  • El servicio SMS EngageLab está activado
  • Se ha creado y aprobado una plantilla SMS en la página Gestión de plantillas; se dispone del ID de plantilla
  • Se ha creado una clave API en la página Claves API; se dispone de dev_key y dev_secret

Lado Calendly

  • Dispone de un plan Calendly Standard o superior (la función Webhook requiere un plan de pago)
  • Se ha creado un token de acceso personal en la página Integrations & apps → API and webhooks

Lado servidor

  • Dispone de un servidor con acceso a Internet público y un certificado HTTPS válido configurado
  • Para desarrollo y depuración local puede usar ngrok para exponer temporalmente un puerto local

Paso 1: Preparar plantillas SMS

El envío de SMS mediante la API requiere plantillas preaprobadas; no se puede enviar texto personalizado directamente.

Inicie sesión en la consola EngageLab, vaya a SMS → Gestión de plantillas y cree las tres plantillas siguientes para cada escenario:

Finalidad de la plantilla Contenido de ejemplo
Confirmación de cita Hi {{name}}, your meeting has been confirmed for {{time}}. Looking forward to seeing you.
Aviso de cancelación Hi {{name}}, your meeting appointment has been canceled. Please contact us to reschedule.
Recordatorio de reunión Hi {{name}}, you have a meeting starting in {{advance}}, scheduled for {{time}}. Please prepare in advance.

Tras enviar las plantillas, espere la aprobación y anote los tres IDs de plantilla.

Consejo: Cree una plantilla distinta para cada escenario — la intención queda más clara y mejora la tasa de aprobación. Si una plantilla incluye variables personalizadas (p. ej. {{name}}), debe pasar los valores mediante el campo params al llamar a la API; de lo contrario, las variables se enviarán tal cual.

Paso 2: Crear una suscripción webhook en Calendly

Calendly no ofrece una interfaz visual para la gestión de webhooks; las suscripciones deben crearse mediante la API.

Obtener el URI de la organización

Primero llame al siguiente endpoint para obtener el URI de la organización de su cuenta:

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"

            
Este bloque de código se muestra en una ventana flotante

Busque en la respuesta el campo current_organization, con un aspecto similar a:

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

            
Este bloque de código se muestra en una ventana flotante

Crear la suscripción webhook

Use el URI de la organización para crear un webhook que se suscriba a los eventos de creación y cancelación de citas:

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

            
Este bloque de código se muestra en una ventana flotante

Una vez creada, Calendly enviará una solicitud de verificación a la url indicada y su servidor debe devolver HTTP 200 para completar el protocolo. Los eventos posteriores de cita y cancelación se enviarán a esa dirección.

Nota: La url debe ser una dirección HTTPS accesible públicamente. Para desarrollo local puede usar ngrok para generar una dirección temporal: ngrok http 3000, y luego usar la URL https://xxxx.ngrok.io que devuelva.

Paso 3: Implementar el servicio receptor del webhook

A continuación se muestra un ejemplo completo de servidor Node.js que recibe eventos webhook de Calendly y llama a la API SMS de EngageLab para enviar mensajes.

Instalar dependencias

npm install express
              
              npm install express

            
Este bloque de código se muestra en una ventana flotante

Código completo

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

            
Este bloque de código se muestra en una ventana flotante

Configuración de variables de entorno

Cree un archivo .env en la raíz del proyecto con las siguientes variables:

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

            
Este bloque de código se muestra en una ventana flotante

Referencia de campos clave

Los siguientes campos en el cuerpo del webhook de Calendly están directamente relacionados con el envío de SMS:

Campo Descripción
event Tipo de evento: invitee.created (cita creada) o invitee.canceled (cita cancelada)
payload.text_reminder_number Número de teléfono facilitado por el invitado, con prefijo de país; puede estar vacío
payload.invitee.name Nombre del invitado
payload.scheduled_event.start_time Hora de inicio de la reunión en formato ISO 8601

Escenarios ampliados

Enviar SMS de seguimiento tras la reunión

Añada un disparador posterior a la reunión en scheduleReminders para enviar una encuesta de satisfacción o una invitación a una nueva cita:

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

            
Este bloque de código se muestra en una ventana flotante

Notificar también al anfitrión

Cuando se crea una cita, además de notificar al invitado puede enviar un recordatorio al anfitrión de la reunión:

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

            
Este bloque de código se muestra en una ventana flotante

Notas importantes

  1. Los webhooks deben responder con HTTP 200 en un plazo de 2 segundos; de lo contrario, Calendly considerará fallido el envío y reintentará. Se recomienda devolver 200 primero y ejecutar el envío de SMS de forma asíncrona para evitar que Calendly interprete mal el retraso de la API de EngageLab.
  2. El campo text_reminder_number puede estar vacío — añada comprobaciones de nulos en su código para no pasar un número vacío a la API SMS, lo que provocaría errores.
  3. Los números de teléfono deben incluir el prefijo de país, p. ej. +8618701235678 para números de China continental. Calendly pide a los usuarios el formato internacional al recopilar teléfonos, pero se recomienda validar el formato en el servidor.
  4. setTimeout no es adecuado para entornos de producción — todas las tareas programadas se pierden al reiniciar el servidor. En producción use una cola de tareas persistente (como BullMQ + Redis) o tareas programadas en la nube, y guarde los registros de recordatorios en una base de datos para recuperarlos tras reinicios.
  5. Las plantillas deben estar aprobadas antes de usarse — si una plantilla está pendiente de revisión o ha sido rechazada al llamarla, la API devolverá un error 4001.
  6. Una respuesta HTTP 200 de la API no garantiza la entrega correcta del SMS — compruebe el campo code en el cuerpo de la respuesta. Consulte la documentación de códigos de error para valores distintos de cero.
Icon Solid Transparent White Qiyu
Contacto