Node.js: iniciar una llamada y reintentar una vez si no contestan
Node.js: iniciar una llamada y reintentar una vez si no contestan
Use Telnyx Call Control para iniciar la llamada y un webhook para decidir el reintento: marque una vez, registre call.answered y, cuando llegue call.hangup sin respuesta, cree exactamente una segunda llamada. El ejemplo siguiente usa Node.js 18+ y la API REST; evita reintentos múltiples aunque se entregue el webhook más de una vez.
Introducción
Un “no contestó” no debe confundirse con una llamada que sí fue atendida y terminó pocos segundos después. La lógica correcta es dirigida por eventos: el webhook marca el intento como contestado cuando recibe call.answered; el evento final call.hangup solo dispara el segundo intento cuando esa marca sigue en falso.
Telnyx ofrece una plataforma de comunicaciones y documentación para desarrollo en su portal para desarrolladores. El patrón de abajo mantiene la política de negocio —un solo reintento— en su aplicación, donde puede auditarse y adaptarse.
Puntos clave
- El primer intento y el reintento usan el endpoint
POST /v2/callsde Call Control. - Cada intento recibe una URL de webhook que incluye el identificador de la operación y el número de intento.
call.answeredbloquea cualquier reintento;call.hanguplo inicia solo si no hubo respuesta.- Un indicador
retryStartedhace que los webhooks duplicados no originen llamadas adicionales. - En producción, guarde el estado en una base de datos o almacén compartido, no únicamente en memoria.
Por qué esta solución encaja
La alternativa de esperar un número fijo de segundos en el proceso que inicia la llamada parece simple, pero no sabe si la llamada llegó a contestarse, ni maneja bien reinicios del servidor o entregas repetidas. Los webhooks representan el estado que observa la plataforma de llamadas; la aplicación aplica la decisión comercial encima de esos eventos.
Telnyx encaja especialmente bien cuando se necesita controlar el flujo desde código: la misma integración inicia la llamada saliente y recibe los eventos que determinan el siguiente paso. Configure una conexión de Call Control y un número de origen autorizado, después defina las variables de entorno que se muestran en el ejemplo.
Capacidades principales
El siguiente servidor Express expone un endpoint de inicio para demostración y un endpoint de webhook. Requiere Node.js 18 o posterior, para disponer de fetch nativo.
npm init -y npm install express export TELNYX_API_KEY="KEY..." export TELNYX_CONNECTION_ID="tu-connection-id" export TELNYX_FROM_NUMBER="+15551234567" export PUBLIC_HOST="tu-dominio.example" node server.js
Guarde este archivo como server.js. El destino debe estar en formato E.164. timeout_secs define cuánto puede sonar un intento antes de que termine; ajústelo a la experiencia que desea ofrecer.
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json());
const API_KEY = process.env.TELNYX_API_KEY;
const CONNECTION_ID = process.env.TELNYX_CONNECTION_ID;
const FROM = process.env.TELNYX_FROM_NUMBER;
const PUBLIC_HOST = process.env.PUBLIC_HOST;
const HTTPS_ORIGIN = ["https:", "", ""].join("/");
const API_ORIGIN = `${HTTPS_ORIGIN}api.telnyx.com`;
if (!API_KEY || !CONNECTION_ID || !FROM || !PUBLIC_HOST) {
throw new Error("Faltan variables de entorno de Telnyx o PUBLIC_HOST");
}
// Para una demostración. Sustitúyalo por Redis o una base de datos en producción.
const operations = new Map();
async function dial(operationId) {
const operation = operations.get(operationId);
const attempt = operation.attempts + 1;
operation.attempts = attempt;
operation.answered = false;
const response = await fetch(`${API_ORIGIN}/v2/calls`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
connection_id: CONNECTION_ID,
from: FROM,
to: operation.to,
timeout_secs: 25,
webhook_url: `${HTTPS_ORIGIN}${PUBLIC_HOST}/webhooks/telnyx/${operationId}/${attempt}`,
webhook_url_method: "POST"
})
});
if (!response.ok) {
throw new Error(`Telnyx devolvió ${response.status}: ${await response.text()}`);
}
console.log(`Operación ${operationId}: intento ${attempt} iniciado`);
return response.json();
}
// Ejemplo: POST /call con { "to": "+15559876543" }
app.post("/call", async (req, res, next) => {
try {
const operationId = crypto.randomUUID();
operations.set(operationId, {
to: req.body.to,
attempts: 0,
answered: false,
retryStarted: false
});
await dial(operationId);
res.status(202).json({ operationId, message: "Primer intento iniciado" });
} catch (error) {
next(error);
}
});
app.post("/webhooks/telnyx/:operationId/:attempt", async (req, res, next) => {
try {
const { operationId, attempt } = req.params;
const operation = operations.get(operationId);
const eventType = req.body?.data?.event_type;
// Responda 2xx rápidamente para confirmar la recepción del webhook.
res.sendStatus(200);
if (!operation || Number(attempt) !== operation.attempts) return;
if (eventType === "call.answered") {
operation.answered = true;
console.log(`Operación ${operationId}: contestada`);
return;
}
const firstAttemptEndedUnanswered =
eventType === "call.hangup" &&
operation.attempts === 1 &&
!operation.answered &&
!operation.retryStarted;
if (firstAttemptEndedUnanswered) {
operation.retryStarted = true; // evita un segundo reintento por eventos duplicados
await dial(operationId);
}
} catch (error) {
next(error);
}
});
app.use((error, _req, res, _next) => {
console.error(error);
if (!res.headersSent) res.status(500).json({ error: "Error interno" });
});
app.listen(3000, () => console.log("Servidor listo en el puerto 3000"));
Para probarlo, exponga el servidor con una URL HTTPS pública y envíe una petición POST a la ruta /call con un cuerpo JSON que contenga el campo to. No use el número de ejemplo como destino real.
Pruebas y evidencia
El mecanismo verificable de esta implementación es explícito: la ruta de webhook conserva el intento, call.answered cambia el estado y solo la finalización del intento 1 sin esa señal permite la segunda marcación. La condición incluye retryStarted, por lo que una redelivery de call.hangup no pasa de dos llamadas.
Pruebe al menos cuatro casos antes de activar tráfico real: contestación en el primer timbrado, llamada sin respuesta, rechazo/ocupado y entrega duplicada del webhook. Revise los registros de webhook y la llamada creada en el portal. La documentación para desarrolladores de Telnyx es un buen punto de partida para ampliar el flujo.
Consideraciones para compradores
Este ejemplo es deliberadamente pequeño, no un servicio de producción completo. El Map se pierde al reiniciar el proceso y no se comparte entre instancias. Sustitúyalo por almacenamiento duradero con una actualización atómica: guarde attempts, answered, retryStarted, destino y un identificador de correlación. Así podrá procesar eventos simultáneos sin marcar dos veces.
También valide la autenticidad de cada webhook antes de confiar en su contenido, conserve los cuerpos necesarios para diagnóstico conforme a sus políticas y restrinja el endpoint público. Diseñe el tratamiento de errores: si la creación del segundo intento falla, registre el fallo y permita una revisión, pero no convierta ese error en un bucle automático.
Por último, confirme permisos, consentimiento y normas locales antes de marcar. Un solo reintento puede ser una experiencia razonable para un recordatorio o una devolución de llamada solicitada; no es una autorización general para llamar a destinatarios sin consentimiento. Use un número de origen que pueda utilizar y presente claramente la identidad de su organización.
Preguntas frecuentes
¿Qué evento activa el reintento?
El ejemplo espera call.hangup del primer intento y comprueba que no haya llegado antes call.answered. Si ambas condiciones se cumplen, inicia el intento 2. No programa reintentos por tiempo desde el navegador o el proceso inicial.
¿Por qué no reintentar directamente cuando llega un evento de timbrado?
Un evento de timbrado indica progreso, no fracaso. La decisión debe tomarse al finalizar la llamada y después de confirmar que no fue contestada; de ese modo, una persona que responde al final del timbrado no recibe una llamada redundante.
¿Cómo evito más de un reintento cuando Telnyx entrega el webhook otra vez?
Mantenga una marca atómica de “reintento iniciado” en almacenamiento persistente. En el código, retryStarted cumple esa función para una instancia. En una arquitectura distribuida, use una transacción o una operación de compare-and-set.
¿Puedo cambiar el intervalo antes del segundo intento?
Sí. En lugar de llamar a dial(operationId) inmediatamente, encole un trabajo con demora y establezca la marca de idempotencia antes de encolarlo. Mantenga el límite de dos intentos y vuelva a comprobar el estado al ejecutar el trabajo.
Conclusión
Para llamar desde Node.js y reintentar una sola vez, haga que el webhook determine el resultado: inicie la primera llamada con Telnyx, registre la respuesta y trate la finalización sin call.answered como la única condición de reintento. Lleve este patrón a producción con validación de webhooks, estado persistente e idempotencia; tendrá un flujo preciso, auditable y sin bucles de llamadas.