Inicio / Artículos

Envío y recepción de SMS en Node.js con Twilio Webhooks y Express

Crea un pequeño servidor Express que envíe textos a través de Twilio, reciba respuestas mediante un webhook expuesto con ngrok y las responda automáticamente con TwiML.

Editor: REACTAPP.TOP6 min de lectura1483 palabras

Fuentes: **Build a Two-Way SMS with Node.js and Twilio (Full Code Included)**

La mayoría de los tutoriales sobre SMS se detienen una vez que un mensaje abandona su servidor. La parte más útil es la de recepción: detectar que un usuario ha respondido, leer el texto y responder. Ese ciclo impulsa los flujos de OTP, los bots de soporte y los recordatorios de citas que las personas confirman con una sola palabra. Al final de esta guía, tendrá un servidor Express que envía mensajes a un teléfono y reacciona cada vez que ese teléfono responde.

Requisitos previos

  • Una cuenta de Twilio. En el momento de escribir esto, la prueba gratuita incluye aproximadamente 15 dólares en crédito, suficiente para experimentar.
  • Postman o cualquier cliente HTTP.
  • Node.js instalado localmente.
  • ngrok, que proporciona a Twilio una dirección pública que redirige hacia su ordenador portátil.

Un servidor Express mínimo

Cree una carpeta con un archivo server.js. El servidor se conecta a localhost en el puerto 3000, habilita el análisis de cuerpos en formato JSON y codificado URL, y expone una ruta de verificación de estado bajo /api/v1/. El analizador de codificación URL es importante: Twilio envía los mensajes entrantes como cuerpos POST en formato de formulario, por lo que sin él req.body quedaría vacío más tarde.

const express = require("express");
const bodyParser = require("body-parser");
const hostname = "127.0.0.1";
const port = 3000;
const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const baseUrl = "/api/v1/";

app.get(`${baseUrl}`, (req, res) => {
  res.status(200).json({ message: "Hello World" });
});
app.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Instale las dependencias:

npm init -y
npm install express body-parser

Ejecute node server.js y abra http://127.0.0.1:3000/api/v1/; una respuesta de “Hello World” confirma que todo funciona correctamente. Express 4.16+ también incluye express.json() y express.urlencoded(), por lo que body-parser es opcional en proyectos nuevos.

Exponer localhost con ngrok

Twilio opera en Internet público mientras que tu servidor está en una máquina privada, por lo que Twilio no puede acceder a él directamente. ngrok abre un túnel y te proporciona una URL HTTPS pública que redirige a un puerto local. Heroku solía ser la opción gratuita para esto, pero ya no existe su plan gratuito.

Instala y autentica ngrok utilizando la documentación de ngrok, luego agrega un script a package.json:

"scripts": {
  "ngrok": "ngrok http http://127.0.0.1:3000/"
}

Mantén el servidor en ejecución en una terminal y luego inicia el túnel en otra:

npm run ngrok

ngrok muestra una dirección como https://abcd-1234.ngrok-free.app. Deja esa terminal abierta.

Cuidado: en el plan gratuito, la URL generada suele cambiar cada vez que ngrok se reinicia. Cuando un webhook funcional de repente deja de funcionar, es probable que la causa sea una URL obsoleta en la consola de Twilio; por lo tanto, actualízala después de cada reinicio.

Configuración de Twilio

En el panel de control de la consola, copie su SID de cuenta y su token de autenticación a un archivo .env. El token es información confidencial: nunca lo incluya en los commits, y añada .env a .gitignore.

ACCOUNT_SID_TWILIO=your_account_sid
AUTH_TOKEN_TWILIO=your_auth_token

Puede fijar productos en la navegación lateral; solo Messaging y Números de teléfono son relevantes aquí.

Obtener un número de teléfono

  1. Abra Números de teléfono, luego Comprar un número.
  2. Elija cualquier número disponible; los números de EE. UU. suelen ser los más económicos.
  3. El costo se deduce del saldo de prueba y el número aparece entre sus números activos.

Crear un servicio de mensajería

  1. Dentro de Messaging, cree un nuevo Servicio de mensajería.
  2. En el paso de Pool de remitentes, agregue su número.
  • En el paso de Integración, elija Enviar un webhook y establezca tanto URL de solicitud como URL de respaldo con su dirección ngrok, añadiendo el camino /api/v1/sms. Ese enlace se define a continuación.
  • Salte el paso opcional y termine.
  • Guarde el Messaging Service SID y su número en .env:

    FROM_TWILIO=+1XXXXXXXXXX
    MESSAGING_SERVICE_SID_TWILIO=your_messaging_service_sid
    

    Conexión del SDK de Twilio con Node

    Instale el SDK junto con dotenv, que carga los valores de .env en process.env:

    npm install twilio dotenv
    

    Ponga esto en la parte superior de server.js. El orden es importante: el cliente lee las credenciales al crearse, por lo que si dotenv se carga más tarde ambos valores serán undefined y todas las llamadas fallarán en la autenticación.

    require("dotenv").config();
    
    const client = require("twilio")(
      process.env.ACCOUNT_SID_TWILIO,
      process.env.AUTH_TOKEN_TWILIO
    );
    

    Envío de un mensaje de texto saliente

    Esta ruta llama a client.messages.create() y reporta el resultado en formato JSON. El bloque try/catch devuelve los errores de Twilio, como un número mal formado, al llamante en lugar de hacer que el proceso se bloquee.

    app.post(`${baseUrl}sendSms`, async (req, res) => {
      try {
        await client.messages.create({
          body: "Hello",
          from: process.env.FROM_TWILIO,
          messagingServiceSid: process.env.MESSAGING_SERVICE_SID_TWILIO,
          to: "+92XXXXXXXXXX", // whoever you're texting
        });
        res.status(200).json({ message: "Text sent successfully" });
      } catch (err) {
        res.status(500).json({ error: err.message });
      }
    });
    

    Significado de los campos:

    • body: el texto del mensaje.
    • from: su número de Twilio.
    • messagingServiceSid: relaciona el mensaje con el servicio y su grupo de remitentes.
    • to: el destinatario en formato E.164 (+1..., +92...).

    Llame a POST /api/v1/sendSms desde Postman y el teléfono debería recibir “Hello”. En el código real, lea to y body desde la solicitud y véalos, en lugar de usar valores codificados directamente.

    Las cuentas de prueba solo pueden enviar mensajes de texto a números verificados en la consola de Twilio. Si la solicitud tiene éxito pero no llega nada, verifique al destinatario o realice una actualización.

    Recibir respuestas a través del webhook

    Cuando alguien envía un mensaje de texto a su número de Twilio, Twilio envía el mensaje al webhook configurado en el Servicio de Mensajería. Su servidor lo acepta y responde con TwiML, el formato XML de Twilio para describir la siguiente acción. Importe la herramienta auxiliar junto con los demás componentes necesarios:

    const MessagingResponse = require("twilio").twiml.MessagingResponse;
    

    Luego agregue el receptor. El error más común es una discrepancia en la ruta: la ruta debe ser igual a la que se ingresó en la consola, en este caso /api/v1/sms. El manejador registra req.body.Body y devuelve un documento TwiML vacío en formato text/xml, lo que significa que no hay respuesta.

    app.post(`${baseUrl}sms`, (req, res) => {
      // Fires every time someone replies to your number
      const twiml = new MessagingResponse();
    console.log("INCOMING ----->", req.body.Body);
      res.writeHead(200, { "Content-Type": "text/xml" });
      res.end(twiml.toString());
    });
    

    Escribe tu número de Twilio y el mensaje aparecerá en la terminal; así se cierra el bucle. La carga también incluye From, que es el número del remitente, y es necesario para rastrear las conversaciones por usuario.

    Dado que el endpoint es público, cualquiera que encuentre la URL puede enviar mensajes falsos. Twilio firma cada webhook con tu token de autenticación en el encabezado X-Twilio-Signature, y la SDK de Node incluye herramientas de validación. Actívalas antes de desplegarlo más allá de pruebas locales.

    Respuestas automáticas

    El servidor escucha pero permanece en silencio. Al agregar un mensaje a la respuesta TwiML, se convierte en un bot basado en palabras clave. Coloca esto dentro del manejador /sms después de que se cree twiml; este proceso elimina espacios en blanco y convierte el texto a minúsculas antes de elegir una respuesta:

    const incoming = req.body.Body.trim().toLowerCase();
    if (incoming === "hi") {
      twiml.message("Hey there! How can I help?");
    } else {
      twiml.message("Hmm, I didn't catch that.");
    }
    

    Twilio envía la respuesta devuelta al remitente, por lo que no se necesita una llamada adicional a messages.create().

    El servidor completo

    Todo en un solo archivo, con la respuesta automática comentada:

    require("dotenv").config();
    
    const express = require("express");
    const bodyParser = require("body-parser");
    const MessagingResponse = require("twilio").twiml.MessagingResponse;
    const client = require("twilio")(
      process.env.ACCOUNT_SID_TWILIO,
      process.env.AUTH_TOKEN_TWILIO
    );
    const hostname = "127.0.0.1";
    const port = 3000;
    const app = express();
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));
    
    const baseUrl = "/api/v1/";
    // Health check
    app.get(`${baseUrl}`, (req, res) => {
      res.status(200).json({ message: "Hello World" });
    });
    
    // Goal 1: Send an SMS
    app.post(`${baseUrl}sendSms`, async (req, res) => {
      try {
        await client.messages.create({
          body: "Hello",
          from: process.env.FROM_TWILIO,
          messagingServiceSid: process.env.MESSAGING_SERVICE_SID_TWILIO,
          to: "+92XXXXXXXXXX", // whoever you're texting
        });
        res.status(200).json({ message: "Text sent successfully" });
      } catch (err) {
        res.status(500).json({ error: err.message });
      }
    });
    
    // Goal 2: Receive replies (this endpoint must match your Twilio webhook)
    app.post(`${baseUrl}sms`, (req, res) => {
      const twiml = new MessagingResponse();
      console.log("INCOMING ----->", req.body.Body);
      // Optional auto-reply:
      // const incoming = req.body.Body.trim().toLowerCase();
      // if (incoming === "hi") {
      //   twiml.message("Hey there! How can I help?");
      // } else {
      //   twiml.message("Hmm, I didn't catch that.");
      // }
      res.writeHead(200, { "Content-Type": "text/xml" });
      res.end(twiml.toString());
    });
    
    app.listen(port, hostname, () => {
      console.log(`Server running at http://${hostname}:${port}/`);
    });
    

    Puntos clave

    • Los SMS salientes son una sola llamada al SDK; los SMS entrantes son un webhook, por lo que el servidor debe ser accesible y entender TwiML.
    • Twilio envía datos codificados en formato formulario, de ahí el parser de URL encoding.
    • La ruta del webhook en la consola y la ruta de Express deben coincidir, y un túnel gratuito reiniciado implica una nueva URL.
    • Los fallos de prueba silenciosos suelen deberse a la restricción de números verificados, y no a un error.
    • Valide las firmas y elimine los valores hardcodeados antes de usarlo en la práctica.

    Desde aquí puede guardar conversaciones, mejorar el bot de palabras clave o agregar inicio de sesión con OTP. Para conocer patrones más amplios relacionados con llamadas de retorno y reintentos, consulte seis patrones de integración para conectar servicios Node.js de forma confiable.

    Lecturas relacionadas

  • Eliminando el código base de try/catch de las rutas de Express con asyncHandler — Aprenda cómo los middleware de errores, un wrapper asyncHandler y una clase AppError personalizada trasladan las respuestas de error de Express fuera de cada ruta y a un lugar consistente.
  • Desplegando una app de Node.js en hosting compartido de cPanel con Passenger — Una guía paso a paso para ejecutar una app de Express en hosting compartido de cPanel con Application Manager y Passenger, incluyendo reinicios, variables de entorno y soluciones para errores 503.
  • Seis patrones de integración para conectar servicios de Node.js de manera fiable — Aprenda los patrones fundamentales detrás de integraciones sólidas con Node.js: solicitud-respuesta, polling, webhooks, clave API, autenticación JWT y OAuth, reintentos con retroceso temporal, y mapeo de datos.
  • Eliminación de consultas N+1 y subconsultas correlacionadas en una API Postgres de Node.js — Análisis detallado de dos endpoints Express lentos en Postgres: cómo la falta de índices, los bucles anidados N+1 y las subconsultas por fila contribuyen a la lentitud, y cómo reducir su tiempo de respuesta a menos de 200 ms.
  • Eliminar el código genérico try/catch de las rutas de Express con asyncHandler — Aprenda cómo el middleware de errores, un envoltorio asyncHandler y una clase AppError personalizada trasladan las respuestas de errores de Express fuera de cada ruta y a un lugar consistente.
  • Dentro de un servicio de notificaciones multi-proveedor: colas, soluciones alternativas y confianza — Un recorrido por el diseño de un servicio de notificaciones fiable: almacenamiento de credenciales, solución alternativa de proveedores, reintentos con BullMQ y listas de espera para mensajes defectuosos, equidad entre usuarios y verificación de firmas de webhook.
  • Notificaciones de React Native: permisos, canales y el ciclo de vida de FCM — Aprende cómo las permisos, los canales de Android, los tokens FCM y los manejadores de estado en primer plano, segundo plano y de cierre se integran en un sistema de notificaciones de React Native con Notifee.