WhatsApp Flows te permite crear mensajes interactivos para los usuarios que completen acciones directamente en WhatsApp. Los flujos te permiten crear pantallas para que los usuarios interactúen; por ejemplo, puedes crear formularios de respuesta sencilla para captar clientes potenciales o recopilar opiniones. Además, tienes la opción de diseñar flujos complejos en varias pantallas para agendar una cita.
En esta guía se describen todos los pasos para crear una aplicación Node.js con la que los usuarios pueden concertar citas a través de WhatsApp Flows. Vamos a crear un flujo en la Plataforma de WhatsApp Business para después configurar un webhook con el que recibir la respuesta del flujo y concertar la cita.
Para continuar con este tutorial, asegúrate de que cumples los siguientes requisitos:
Hay dos formas de crear un flujo de este tipo: con el creador de flujos, al que se puede acceder desde el Administrador de WhatsApp, o con la API de flujos. En este tutorial usaremos el creador de flujos.
En el menú que encontrarás a la izquierda del panel del Administrador de WhatsApp, selecciona Herramientas de la cuenta. A continuación, haz clic en Flujos.
Haz clic en Crear flujo, en la esquina superior derecha.
Aparecerá un cuadro de diálogo donde deberás rellenar los datos para el flujo de cita:
Haz clic en Enviar para crear el flujo.
Puedes ver una vista previa del flujo a la derecha de la interfaz del creador de flujos. En la pantalla de la cita, el usuario puede elegir los detalles de la cita, como la ubicación y la fecha. La pantalla de detalles es donde el usuario debe introducir su información. En la pantalla de resumen se muestra la información general de la reserva. La última pantalla contiene las condiciones de la empresa.
El flujo se mantiene en estado de borrador mientras lo editas. Por el momento, solo puedes compartirlo con tu equipo para hacer pruebas. Si quieres compartirlo con otros destinatarios, tendrás que publicarlo, pero no podrás editar el flujo una vez publicado. Puesto que te falta añadir la URL del extremo para este flujo de citas, déjalo como borrador por ahora y continúa con el siguiente paso, en el que vamos a configurar el extremo.
WhatsApp Flows te permite conectarte a un extremo externo, que puede proporcionar datos dinámicos para tu flujo y controlar el enrutamiento. También recibe las respuestas que envíen los usuarios desde el flujo.
Con el propósito de hacer pruebas, en este artículo se usa Glitch para alojar el extremo. Utilizar Glitch es totalmente opcional, y no es necesario para usar los flujos. Puedes clonar el código del extremo desde GitHub y ejecutarlo en el entorno que prefieras.
Accede al código del extremo en Glitch y crea un remix para obtener un dominio único. Para ello, haz clic en Remix en la parte superior de la página. Aparecerá un dominio único como marcador de posición en el elemento de entrada que encontrarás en la parte derecha de la página de Glitch.
Antes de continuar, veamos el código. Hay cuatro archivos JavaScript en el directorio src
: encryption.js
, flow.js
, keyGenerator.js
y server.js
. El archivo de entrada es server.js
, así que comencemos por él.
El archivo server.js
comienza configurando la aplicación Express para que utilice el middleware express.json
para analizar las peticiones JSON entrantes. A continuación, carga las variables de entorno necesarias para el extremo.
const { APP_SECRET, PRIVATE_KEY, PASSPHRASE, PORT = "3000" } = process.env;
APP_SECRET
se utiliza en la verificación de firmas. Te ayuda a comprobar si un mensaje llega desde WhatsApp y, por tanto, es seguro procesarlo. Añádelo al archivo .env
.
Para acceder a tu APP_SECRET
, navega hasta el panel de control de la aplicación en Meta for Developers. En el panel de navegación izquierdo, en Configuración de la aplicación, elige Información básica. Haz clic en Mostrar debajo de Clave secreta de la aplicación y cópiala. Después, regresa a Glitch, abre el archivo .env
y crea una variable con el nombre APP_SECRET
y el valor de la clave secreta que has copiado.
PRIVATE_KEY
ayuda a descifrar los mensajes recibidos. PASSPHRASE
se usa para verificar la clave privada. Además de la clave privada, necesitarás la clave pública correspondiente para cargarla más adelante. No uses nunca aquí las claves privadas de tus cuentas de producción. Crea una clave privada temporal para probarla en Glitch, y luego sustitúyela por tu clave de producción en tu propia infraestructura.
Genera el par de claves pública-privada ejecutando el siguiente comando en el terminal de Glitch. Reemplaza <your-passphrase>
por el código de acceso que hayas escogido. Haz clic en la pestaña TERMINAL situada en la parte inferior de la página para acceder al terminal de Glitch.
node src/keyGenerator.js <your-passphrase>
Copia el código de acceso y la clave privada y pégalos en el archivo .env
. Haz clic en el archivo que tiene la etiqueta .env en la barra lateral izquierda y luego en ✏️ Plain text (texto sin formato) en la parte superior. No lo edites directamente desde la IU, porque se dañaría el formato de tu clave.
Una vez configuradas las variables de entorno, copia la clave pública que has generado y súbela con la API Graph.
El archivo server.js
también contiene un extremo POST
que realiza distintas acciones:
if (!PRIVATE_KEY) { throw new Error('Private key is empty. Please check your env variable "PRIVATE_KEY".'); }
isRequestSignatureValid
que se encuentra al final del archivo: if(!isRequestSignatureValid(req)) { // Return status code 432 if request signature does not match. // To learn more about return error codes visit: https://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codes return res.status(432).send(); }
decryptRequest
que se encuentra en el archivo encryption.js
: let decryptedRequest = null; try { decryptedRequest = decryptRequest(req.body, PRIVATE_KEY, PASSPHRASE); } catch (err) { console.error(err); if (err instanceof FlowEndpointException) { return res.status(err.statusCode).send(); } return res.status(500).send(); } const { aesKeyBuffer, initialVectorBuffer, decryptedBody } = decryptedRequest; console.log("💬 Decrypted Request:", decryptedBody);
getNextScreen
. const screenResponse = await getNextScreen(decryptedBody);
console.log("👉 Response to Encrypt:", screenResponse);
res.send(encryptResponse(screenResponse, aesKeyBuffer, initialVectorBuffer));
Este archivo contiene la lógica para cifrar y descifrar los mensajes intercambiados por motivos de seguridad. En este tutorial no nos centraremos en el funcionamiento del archivo.
Este archivo ayuda a generar las claves privada y pública, como hemos visto antes. Al igual que sucede con el archivo encryption.js
, en este tutorial no vamos a ver el archivo keyGenerator.js
en detalle.
La lógica para gestionar el flujo se aloja en este archivo. Comienza con un objeto al que se le asigna el nombre SCREEN_RESPONSES
. El objeto contiene identificadores de pantalla con sus detalles correspondientes, como los datos predefinidos que se usan en los intercambios de datos. Este objeto se genera desde el creador de flujos en “...” > Extremo > Fragmentos > Respuestas. En el mismo objeto, también tienes otro identificador, SUCCESS
, que se envía de nuevo al dispositivo cliente cuando el flujo se completa correctamente. Con esta acción se cierra el flujo.
La función getNextScreen
contiene la lógica que orienta al extremo sobre qué datos del flujo mostrar al usuario. Comienza extrayendo los datos necesarios del mensaje descifrado.
const { screen, data, version, action, flow_token } = decryptedBody;
Los extremos de WhatsApp Flows suelen recibir tres solicitudes:
data_exchange
. data.error
. ping
.Encontrarás más información en la documentación del extremo.
La función gestiona las notificaciones de error y comprobación de estado mediante instrucciones if
y responde en consecuencia, como se muestra en el siguiente fragmento:
// handle health check request if (action === "ping") { return { version, data: { status: "active", }, }; } // handle error notification if (data?.error) { console.warn("Received client error:", data); return { version, data: { acknowledged: true, }, }; }
Cuando un usuario hace clic en el botón de llamada a la acción (CTA) del flujo, se inicia una acción INIT
, que devuelve la pantalla de la cita junto con los datos. También desactiva los campos desplegables de ubicación, fecha y hora para garantizar que el usuario rellene todos los campos.
Por ejemplo, el campo desplegable de fecha solo se activa una vez rellenado el de ubicación. La activación y desactivación de los campos se gestiona cuando se recibe una solicitud data_exchange
.
// handle initial request when opening the flow and display APPOINTMENT screen if (action === "INIT") { return { ...SCREEN_RESPONSES.APPOINTMENT, data: { ...SCREEN_RESPONSES.APPOINTMENT.data, // these fields are disabled initially. Each field is enabled when previous fields are selected is_location_enabled: false, is_date_enabled: false, is_time_enabled: false, }, }; }
En el caso de las acciones data_exchange
, se usa una estructura switch case para determinar qué datos se devuelven en función del identificador de pantalla. Si el identificador es APPOINTMENT
, los campos desplegables solo se activan si se han seleccionado los campos desplegables anteriores.
// Each field is enabled only when previous fields are selected is_location_enabled: Boolean(data.department), is_date_enabled: Boolean(data.department) && Boolean(data.location), is_time_enabled: Boolean(data.department) && Boolean(data.location) && Boolean(data.date)
Para las pantallas de detalles, los títulos de las propiedades de los objetos de datos, como ubicación y departamento, se extraen del objeto SCREEN_RESPONSES.APPOINTMENT.data
. Este código asume que hay una coincidencia válida, por lo que debes tener en cuenta que puede lanzar un error si no se encuentra ningún objeto que coincida.
Ahora, toma una instancia del objeto ubicación. La selección del objeto de ubicación en concreto se determina igualando la propiedad id
de los objetos de la matriz con el valor de data.location
.
const departmentName = SCREEN_RESPONSES.APPOINTMENT.data.department.find( (dept) => dept.id === data.department ).title; const locationName = SCREEN_RESPONSES.APPOINTMENT.data.location.find( (loc) => loc.id === data.location ).title; const dateName = SCREEN_RESPONSES.APPOINTMENT.data.date.find( (date) => date.id === data.date
).title;
A continuación, los valores se concatenan y se devuelven en la respuesta para mostrar la pantalla de resumen.
const appointment = `${departmentName} at ${locationName} ${dateName} at ${data.time}`; const details = `Name: ${data.name} Email: ${data.email} Phone: ${data.phone} "${data.more_details}"`; return { ...SCREEN_RESPONSES.SUMMARY, data: { appointment, details, // return the same fields sent from client back to submit in the next step ...data, }, };
Una vez enviada la pantalla de resumen desde el cliente, se envía una respuesta de confirmación al dispositivo cliente para marcar el flujo como completado. El flow_token
es un identificador único que puedes configurar al enviar el flujo al usuario.
// send success response to complete and close the flow return { ...SCREEN_RESPONSES.SUCCESS, data: { extension_message_response: { params: { flow_token, }, }, }, };
La pantalla de condiciones no contiene datos para intercambiar, por lo que el extremo no la gestiona.
En la parte superior derecha de la página de Glitch, haz clic en el icono del menú de los tres puntos verticales y selecciona Copy Link (copiar enlace) para copiar la URL. También puedes obtener el enlace si haces clic en Share (compartir) en la parte superior derecha.
Accede al editor de flujos. Haz clic en Configurar en el panel marrón que encontrarás en la parte superior del editor.
Aparecerá un mensaje emergente que te permite configurar el URI del extremo, el número de teléfono de la empresa y la aplicación en Meta for Developers. Cuando hayas configurado lo necesario, lleva a cabo una comprobación de estado. En primer lugar, selecciona Solicitar datos, en Solicitar datos de la primera pantalla, en la configuración de la vista previa interactiva y ejecuta esta vista. De este modo se envía una solicitud al extremo para recuperar los datos de la primera pantalla, a la vez que se verifica que el extremo esté disponible y que hayas llevado a cabo una comprobación de estado.
A continuación, publica el flujo haciendo clic en el menú de los tres puntos horizontales (...) y selecciona Publicar. De esta forma se enviará una solicitud de comprobación de estado a tu extremo con action === "ping"
para verificar que el extremo esté configurado antes de publicarlo.
Cuando hayas terminado de configurarlo todo, vuelve a activar la vista previa interactiva en la interfaz de usuario del creador de WhatsApp para probar el flujo. En la ventana emergente que aparece, selecciona el número de teléfono y elige la opción Solicitar datos en Solicitar datos de la primera pantalla. Haz clic en el icono X para cerrar el flujo y empezar a probarlo de nuevo desde el botón de llamada a la acción.
Abre el registro de Glitch haciendo clic en la pestaña LOGS (registros). Haz clic en Clear (borrar) para borrarlo. Después, vuelve a la vista previa de la IU del creador de WhatsApp. Haz clic en Vista previa del flujo. Verás algo parecido a lo que se muestra a continuación:
Ahora regresa a los registros de Glitch, donde verás una acción INIT
, el identificador del flujo y otros detalles en la solicitud descifrada. También hay una respuesta al cifrado que se envía de nuevo al flujo del usuario una vez seleccionado el campo desplegable del departamento.
Continúa y selecciona el departamento. Fíjate en cómo is_location_enabled
está establecido en true
y la acción ha cambiado a data_exchange
.
Sigue probando el flujo y observa los cambios de datos en los registros de Glitch. Se generarán registros similares cuando los usuarios interactúen con el flujo desde sus dispositivos móviles.
En la siguiente sección, vamos a crear un webhook que envíe un mensaje de confirmación al usuario cuando reserve una cita.
Cuando un usuario completa el flujo, se envía un mensaje que marca el flujo como completado al webhook suscrito. Desde este webhook, notificarás al usuario que ha reservado la cita correctamente mediante un mensaje en el chat. Como con el extremo, también vamos a usar Glitch para las pruebas. Puedes acceder al código y hacer un remix aquí.
Utilizar Glitch es totalmente opcional, y no es necesario para usar los flujos. Puedes clonar el código del webhook desde GitHub y ejecutarlo en el entorno que prefieras.
Para establecer las variables de entorno, abre el archivo .env
en Glitch. Establece VERIFY_TOKEN
en la cadena que prefieras, FLOW_ID
en el identificador de tu flujo y GRAPH_API_TOKEN
en el identificador de acceso de la cuenta de WhatsApp Business. Puedes obtener el identificador de acceso en el panel de tu aplicación en Meta for Developers haciendo clic en Configuración de la API en la sección WhatsApp del panel de navegación izquierdo.
En la página que se muestra, haz clic en el botón Copiar debajo de la tarjeta Identificador de acceso temporal. Pega la clave en tu archivo .env
.
En tu cuenta de Meta for Developers, haz clic en el menú Configuración, debajo de WhatsApp, en el panel de navegación izquierdo.
En el apartado Webhook, haz clic en Editar. En el cuadro de diálogo que se abre, pega la URL de Glitch que has copiado y añádele /webhook
en el campo URL de devolución de llamada. En el campo Identificador de verificación, añade el token de la variable TOKEN
en tu archivo .env
. Cuando hayas acabado, haz clic en Verificar y guardar. El cuadro de diálogo se cerrará y volverás a la pantalla principal. Haz clic en Administrar y comprueba el campo Mensajes. Tu webhook ya está listo.
El código contiene dos rutas: POST /webhook
y GET /webhook
. La ruta GET
gestiona las solicitudes de verificación de webhook comparando el identificador proporcionado con un identificador de verificación predefinido y respondiendo con los códigos de estado adecuados y un identificador de comprobación.
const verify_token = process.env.VERIFY_TOKEN; // Parse params from the webhook verification request let mode = req.query["hub.mode"]; let token = req.query["hub.verify_token"]; let challenge = req.query["hub.challenge"]; if (mode && token) { if (mode === "subscribe" && token === verify_token) { console.log("WEBHOOK_VERIFIED"); res.status(200).send(challenge); } else { res.sendStatus(403); } }
La ruta POST /webhook
procesa las notificaciones de webhook entrantes. Las solicitudes de webhook pueden tener distintas cargas útiles. De este modo, el código siguiente lee el mensaje y el número de teléfono de la empresa accediendo a los campos de la solicitud de forma segura en caso de que no se hayan definido.
const message = req.body.entry?.[0]?.changes[0]?.value?.messages?.[0]; const business_phone_number_id =
req.body.entry?.[0].changes?.[0].value?.metadata?.phone_number_id;
A continuación, comprueba si la solicitud entrante es para un mensaje de tipo "text"
que contenga la palabra “appointment” (“cita”). Si el mensaje contiene esta palabra, se envía el flujo al usuario. El mensaje del flujo se envía con flow_action: "data_exchange
"
, lo que significa que el flujo hará una petición INIT
al extremo cuando se lance para obtener la pantalla inicial y los datos.
if ( message.type === "text" && // for demo purposes, send the flow message whenever a user sends a message containing "appointment" message.text.body.toLowerCase().includes("appointment") ) { // send flow message as per the docs here https://developers.facebook.com/docs/whatsapp/flows/gettingstarted/sendingaflow#interactive-message-parameters await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, type: "interactive", interactive: { type: "flow", header: { type: "text", text: "Hello there 👋", }, body: { text: "Ready to transform your space? Schedule a personalized consultation with our expert team!", }, footer: { text: "Click the button below to proceed", }, action: { name: "flow", parameters: { flow_id: FLOW_ID, flow_message_version: "3", // replace flow_token with a unique identifier for this flow message to track it in your endpoint & webhook flow_token: "<FLOW_TOKEN_PLACEHOLDER>", flow_cta: "Book an appointment", flow_action: "data_exchange", }, }, }, }, }); } ...
Si el mensaje entrante no es de tipo "text"
, el código comprueba si es "interactive
"
. Si el mensaje entrante es del tipo interactivo "nfm_reply"
, significa que se trata de una respuesta del flujo. Entonces, devuelve al usuario el mensaje “You’ve successfully booked an appointment” (“Has reservado una cita correctamente”).
... if ( message.type === "interactive" && message.interactive?.type === "nfm_reply" ) { // send confirmation message await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, text: { body: "You've successfully booked an appointment" }, }, }); } ...
A continuación, marca el mensaje entrante como leído para que el usuario vea las marcas azules.
... // mark incoming message as read await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", status: "read", message_id: message.id, }, }); ...
En este ejemplo, el usuario envía un mensaje a tu número que contiene la palabra “appointment” (“cita”) y, a continuación, recibe el mensaje del flujo. También puedes configurarlo para que envíe el flujo después de otra interacción o como plantilla de mensaje.
El usuario recibirá un mensaje del flujo con un botón de llamada a la acción para concertar una cita, donde podrá introducir sus datos. Cuando complete el flujo, recibirá un mensaje de confirmación.
En esta guía, has aprendido a configurar un flujo de WhatsApp para programar citas de forma cómoda. Has usado la interfaz de usuario del creador de flujos para crear un formulario con el que recopilar los datos de la cita facilitados por los usuarios.
Gracias a los flujos, no tendrás que redirigir a los usuarios a un sitio web externo para reservar citas, lo que mejora la experiencia del cliente. El proceso es muy sencillo y permite a los usuarios hacer las reservas directamente desde WhatsApp. Además de para concertar citas, puedes usar los flujos de WhatsApp para recopilar opiniones del servicio de atención al cliente, o ayudar a los usuarios a inscribirse en promociones o listas de correo. Los flujos de WhatsApp también te ofrecen la posibilidad de conectarlos con API externas u otras aplicaciones en tu extremo.
Crear flujos de WhatsApp es muy sencillo con la interfaz de usuario del creador de flujos, aunque también puedes usar la API de flujos para crearlos mediante lenguaje de programación. Si quieres obtener más información, consulta la documentación de WhatsApp Flows.