Calendly
Calendly es una herramienta de reservas online ampliamente utilizada, pero su función integrada de recordatorios por SMS solo admite algunos países y regiones, con una cobertura limitada en mercados como Asia-Pacífico, el Sudeste Asiático y Oriente Medio. Al combinar el Webhook de Calendly con EngageLab SMS, puedes enviar notificaciones por SMS a cualquier número del mundo cuando se crea o cancela una cita y antes de que comience la reunión, cubriendo así los vacíos geográficos de las capacidades nativas de Calendly.
Requisitos previos
Antes de empezar, confirma que se ha completado la siguiente configuración:
En EngageLab
- Has activado el servicio EngageLab SMS
- Has creado una plantilla de SMS en la página de gestión de plantillas, ha sido aprobada y has obtenido el ID de la plantilla
- Has creado una API Key en la página de API Key y has obtenido el
dev_keyy eldev_secret
En Calendly
- Tienes un plan Calendly Standard o superior (la función de Webhook requiere un plan de pago)
- Has creado un Personal Access Token en la página Integrations & apps → API and webhooks
En el servidor
- Dispones de un servidor accesible desde Internet, con un certificado HTTPS válido configurado
- Durante el desarrollo y las pruebas locales, puedes usar ngrok para exponer temporalmente un puerto local
Paso 1: prepara las plantillas de SMS
Para enviar SMS mediante la API es obligatorio usar plantillas previamente aprobadas; no se admite pasar texto personalizado directamente.
Inicia sesión en la consola de EngageLab, ve a SMS → Gestión de plantillas y crea las siguientes tres plantillas según el escenario:
| Uso de la plantilla | Ejemplo de contenido de la plantilla |
|---|---|
| Confirmación de cita | Hola {{name}}, tu reunión ha sido confirmada para el {{time}}. Esperamos verte. |
| Notificación de cancelación | Hola {{name}}, tu cita para la reunión ha sido cancelada. Si deseas reprogramarla, ponte en contacto con nosotros. |
| Recordatorio de reunión | Hola {{name}}, tienes una reunión que comenzará dentro de {{advance}}, el {{time}}. Por favor, prepárate con antelación. |
Tras enviar las plantillas, espera a que sean aprobadas y anota los tres ID de plantilla.
Recomendación: crea una plantilla independiente para cada escenario; así la semántica es más clara y la tasa de aprobación es mayor. Si la plantilla contiene variables personalizadas (como
{{name}}), al llamar a la API debes pasar el valor mediante el campoparams; de lo contrario, la variable se enviará tal cual.
Paso 2: crea una suscripción de Webhook en Calendly
La gestión de Webhooks de Calendly no tiene una interfaz visual; debe crearse mediante la API.
Obtén la URI de la organización
Primero, llama a la siguiente interfaz para obtener la URI de la organización de la cuenta actual:
curl https://api.calendly.com/users/me \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
En el resultado, busca el campo current_organization, con el siguiente formato:
https://api.calendly.com/organizations/xxxxxxxxxxxxxxxx
Crea la suscripción de Webhook
Usa la URI de la organización para crear el Webhook y suscribirte a los dos 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://tu-direccion-de-servidor/webhooks/calendly",
"events": [
"invitee.created",
"invitee.canceled"
],
"organization": "https://api.calendly.com/organizations/tu-ID-de-organizacion",
"scope": "organization"
}'
Tras crearla con éxito, Calendly enviará una solicitud de verificación a la url indicada, y el servidor debe responder con HTTP 200 para completar el handshake. A partir de entonces, cada vez que alguien reserve o cancele, se enviará un evento a esa dirección.
Nota: la
urldebe ser una dirección HTTPS accesible desde Internet. Durante el desarrollo local, puedes usar ngrok para generar una dirección temporal:ngrok http 3000, y rellenar la direcciónhttps://xxxx.ngrok.ioresultante.
Paso 3: crea el servicio receptor del Webhook
A continuación se muestra un ejemplo completo del lado del servidor en Node.js que, tras recibir los eventos del Webhook de Calendly, llama a la API de EngageLab SMS para enviar SMS.
Instala las dependencias
npm install express
Código completo
// server.js
import express from 'express';
const app = express();
app.use(express.json());
// Autenticación de EngageLab: base64(dev_key:dev_secret)
const ENGAGELAB_AUTH = Buffer.from(
`${process.env.ENGAGELAB_DEV_KEY}:${process.env.ENGAGELAB_DEV_SECRET}`
).toString('base64');
// Llama a la API de EngageLab SMS para enviar el 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 no significa que el envío fue exitoso; hay que comprobar el campo code
if (data.code && data.code !== 0) {
console.error(`Error al enviar el SMS: code=${data.code}, message=${data.message}`);
} else {
console.log(`SMS enviado correctamente: plan_id=${data.plan_id}, message_id=${data.message_id}`);
}
}
// Convierte una cadena de tiempo ISO a la hora local (Asia/Shanghai)
function formatTime(isoString) {
return new Date(isoString).toLocaleString('es-ES', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
// Registra las tareas programadas de recordatorio previo a la reunión (una a 24 h y otra a 1 h)
function scheduleReminders({ phone, name, startTime }) {
const meetingTime = new Date(startTime).getTime();
const now = Date.now();
const reminders = [
{ advance: '24 horas', triggerAt: meetingTime - 24 * 60 * 60 * 1000 },
{ advance: '1 hora', triggerAt: meetingTime - 60 * 60 * 1000 },
];
for (const { advance, triggerAt } of reminders) {
const delay = triggerAt - now;
if (delay <= 0) continue; // El momento de activación ya pasó; se omite
setTimeout(async () => {
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_REMINDER,
params: { name, time: formatTime(startTime), advance },
});
}, delay);
}
}
// Endpoint receptor del Webhook
app.post('/webhooks/calendly', async (req, res) => {
const { event, payload } = req.body;
const phone = payload.text_reminder_number; // Número de móvil que el usuario indicó al reservar
const name = payload.invitee?.name ?? 'Usuario';
const startTime = payload.scheduled_event?.start_time;
// Si no se indicó número de móvil, se omite el envío del SMS
if (!phone) {
console.log('Esta cita no incluye número de móvil; se omite la notificación por SMS');
return res.sendStatus(200);
}
if (event === 'invitee.created') {
// Envía el SMS de confirmación de la cita
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CONFIRM,
params: { name, time: formatTime(startTime) },
});
// Registra las tareas de recordatorio previo a la reunión
scheduleReminders({ phone, name, startTime });
} else if (event === 'invitee.canceled') {
// Envía el SMS de notificación de cancelación
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CANCEL,
params: { name },
});
}
// Debe devolverse 200 en menos de 2 segundos; de lo contrario, Calendly considerará que el envío falló y reintentará
res.sendStatus(200);
});
app.listen(3000, () => console.log('Server running on port 3000'));
Configuración de las variables de entorno
Crea un archivo .env en la raíz del proyecto y rellena las siguientes variables:
ENGAGELAB_DEV_KEY=tu_dev_key
ENGAGELAB_DEV_SECRET=tu_dev_secret
TEMPLATE_ID_CONFIRM=ID_de_la_plantilla_de_confirmacion
TEMPLATE_ID_CANCEL=ID_de_la_plantilla_de_cancelacion
TEMPLATE_ID_REMINDER=ID_de_la_plantilla_de_recordatorio
Descripción de los campos clave
En el payload que envía el Webhook de Calendly, los siguientes campos 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 móvil indicado por la persona que reserva, con código de país; puede estar vacío |
payload.invitee.name |
Nombre de la persona que reserva |
payload.scheduled_event.start_time |
Hora de inicio de la reunión, en formato ISO 8601 |
Escenarios ampliados
Enviar un SMS de seguimiento tras la reunión
Añade en scheduleReminders una tarea que se active después de que finalice la reunión, para enviar una encuesta de satisfacción o una invitación a una próxima cita:
// Añadir dentro de la función scheduleReminders
const followUpAt = meetingTime + 30 * 60 * 1000; // 30 minutos después de que finalice la reunión
const followUpDelay = followUpAt - now;
if (followUpDelay > 0) {
setTimeout(async () => {
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_FOLLOWUP,
params: { name },
});
}, followUpDelay);
}
Notificar también al anfitrión
Al crear una cita, además de notificar a la persona que reserva, también puedes enviar un recordatorio al anfitrión de la reunión:
if (event === 'invitee.created') {
// Notifica a la persona que reserva
await sendSMS({
to: phone,
templateId: process.env.TEMPLATE_ID_CONFIRM,
params: { name, time: formatTime(startTime) },
});
// Notifica al anfitrión
await sendSMS({
to: process.env.HOST_PHONE,
templateId: process.env.TEMPLATE_ID_HOST_NOTIFY,
params: { name, time: formatTime(startTime) },
});
}
Consideraciones
- El Webhook debe responder con
HTTP 200en menos de 2 segundos; de lo contrario, Calendly considerará que el envío falló y reintentará. Se recomienda devolver primero 200 y, después, ejecutar de forma asíncrona la lógica de envío del SMS, para evitar que Calendly haga un juicio erróneo por la latencia de respuesta de la API de EngageLab. - El campo
text_reminder_numberpuede estar vacío; debes comprobar si está vacío en el código para evitar pasar un número vacío a la API de SMS y provocar un error. - El formato del número de móvil debe incluir el código de país, por ejemplo, un número de Singapur sería
+6591234567. Calendly guía al usuario para que escriba el formato internacional al recopilar el número, pero se recomienda hacer una validación de formato en el servidor. setTimeoutno es adecuado para entornos de producción: tras reiniciar el servidor se perderán todas las tareas programadas. En producción se recomienda usar una cola de tareas persistente (como BullMQ + Redis) o las tareas programadas de un servicio en la nube, almacenando los recordatorios en una base de datos para poder recuperarlos tras un reinicio.- La plantilla solo puede usarse después de ser aprobada; si al llamar la plantilla está pendiente de revisión o ha sido rechazada, la API devolverá el error
4001. - Que la API devuelva HTTP 200 no significa que el SMS se haya enviado con éxito; comprueba el campo
codeen el cuerpo de la respuesta y, si no es cero, consulta la descripción de los códigos de error para investigar la causa.










