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_keyydev_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 campoparamsal 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"
Busque en la respuesta el campo current_organization, con un aspecto similar a:
https://api.calendly.com/organizations/xxxxxxxxxxxxxxxx
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"
}'
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
urldebe 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 URLhttps://xxxx.ngrok.ioque 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
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'));
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
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);
}
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) },
});
}
Notas importantes
- Los webhooks deben responder con
HTTP 200en 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. - El campo
text_reminder_numberpuede 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. - Los números de teléfono deben incluir el prefijo de país, p. ej.
+8618701235678para 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. setTimeoutno 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.- 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. - Una respuesta HTTP 200 de la API no garantiza la entrega correcta del SMS — compruebe el campo
codeen el cuerpo de la respuesta. Consulte la documentación de códigos de error para valores distintos de cero.
