Enviar mensajes
💡 🎯 Al terminar esta lección podrás…
- Describir la petición POST a /{PHONE_NUMBER_ID}/messages y sus cabeceras.
- Construir cuerpos de texto, botones y lista con construirEnvio y respetar los límites de cada tipo.
- Enviar un mensaje con enviarMensaje sin filtrar el token en los errores.
- Interpretar los códigos de error más comunes de la API y decidir qué hacer en cada caso.
- Explicar por qué una respuesta 200 no garantiza que el cliente recibió el mensaje.
⚠️ INFO REFERENCIAL: revisa los sitios oficiales
Los precios, planes, límites y tarifas de esta lección son solo una referencia (revisados en octubre de 2026) y cambian con frecuencia. Antes de decidir o cobrar a un cliente, confirma siempre en los sitios oficiales de cada proveedor (enlaces al final de la lección).
1.La petición básica
Enviar un mensaje con la Cloud API es una sola petición HTTPS. Tu bot ya sabe qué responder (el motor devuelve una lista de respuestas); esta lección es la última pieza: cómo se convierten esas respuestas en peticiones a Meta.
construirEnvioenviarMensaje (POST)mensajeSegún la documentación oficial de envío de mensajes:
- El método y la ruta son
POST /<PHONE_NUMBER_ID>/messagessobregraph.facebook.com, con la versión de la API en la ruta (por ejemplov26.0). - La cabecera
Authorization: Bearer <token>lleva tu token. - La cabecera
Content-Type: application/jsonindica el formato. - El cuerpo siempre incluye
messaging_product: "whatsapp", el destinatario entoy eltypedel mensaje.
Lo que el código del curso aplica en src/whatsapp.js:
export const GRAPH_VERSION = 'v26.0';
export async function enviarMensaje(env, cuerpo, llamar = fetch) {
const url = `https://graph.facebook.com/${GRAPH_VERSION}/${env.WA_PHONE_ID}/messages`;
const respuesta = await llamar(url, {
method: 'POST',
headers: { Authorization: `Bearer ${env.WA_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify(cuerpo),
});
const datos = await respuesta.json().catch(() => ({}));
if (!respuesta.ok) {
// Nunca incluimos el token en el error.
throw new Error(`WhatsApp respondió ${respuesta.status}: ${datos?.error?.message ?? 'sin detalle'}`);
}
return datos;
}
Tres decisiones merecen atención:
llamar = fetch: la función recibe como parámetro elfetchque usará. En producción es el real; en las pruebas es uno falso, así pruebas el envío sin tocar internet ni gastar mensajes (lo verás en la lección 23).- El mensaje de error nunca contiene el token. Un error registrado en la consola o en un panel de logs no debe filtrar una credencial.
- Se lee el
error.messagede la respuesta de Meta, que suele explicar el problema.
⚠️ Importante · Verifica este dato
La versión de la Graph API cambia y cada versión tiene fecha de retiro. El ejemplo de la documentación de envío usa una versión distinta a la del código del curso: ambas existen y funcionan, pero conviene revisar el registro de cambios y actualizar GRAPH_VERSION antes de desplegar.
2.Texto simple
El tipo más simple. El motor devuelve { tipo: 'texto', texto: '...' } y construirEnvio lo transforma:
import { construirEnvio } from './src/whatsapp.js';
const cuerpo = construirEnvio('51999000111', {
tipo: 'texto',
texto: 'Hola, bienvenido a Minimarket La Esquina.',
});
console.log(JSON.stringify(cuerpo));
{"messaging_product":"whatsapp","recipient_type":"individual","to":"51999000111","type":"text","text":{"preview_url":false,"body":"Hola, bienvenido a Minimarket La Esquina."}}
preview_url: false evita que WhatsApp genere una vista previa de enlaces. Si quieres que un enlace muestre su vista previa, tendrías que ponerlo en true (la documentación oficial usa true en su ejemplo cuando el texto incluye un enlace). Respecto al límite, el código impone 4096 caracteres de texto.
⚠️ Importante · Verifica este dato
Los límites de caracteres (4096 en texto, 1024 en el cuerpo de botones, 20 en el título de un botón, etc.) son los que figuran en las páginas oficiales de cada tipo de mensaje al escribir este curso. Confírmalos en la documentación de mensajes interactivos con botones y de listas.
Una respuesta exitosa contiene el id del mensaje (wamid...) y el contacto:
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "51999000111", "wa_id": "51999000111" }],
"messages": [{ "id": "wamid.EJEMPLO123" }]
}
Guarda ese id si más adelante quieres relacionarlo con los estados sent, delivered o read que llegarán a tu webhook (lección 13).
3.Botones de respuesta
Los botones de respuesta (reply buttons) muestran hasta tres opciones tocables bajo el mensaje. Son ideales para decisiones cortas: «Recojo» o «Delivery», «Confirmar» o «Cancelar». Cuando el cliente toca uno, tu webhook recibe un mensaje interactive con el id del botón, no con su texto. Por eso el motor usa ids como entrega:recojo y pedido_confirmar: son identificadores estables, mientras el título es solo lo que se ve.
Reglas que valida construirEnvio antes de enviar (si se rompe una, lanza un error claro antes de llamar a Meta):
| Regla | Valor en el código |
|---|---|
| Cantidad de botones | de 1 a 3 |
| Título del botón | hasta 20 caracteres, único dentro del mensaje |
| Id del botón | hasta 256 caracteres |
| Cuerpo del mensaje | de 1 a 1024 caracteres |
Estas validaciones producen mensajes como los siguientes (reales, ejecutados con el código del curso):
Respuesta inválida para WhatsApp: título de botón de más de 20 caracteres: "Un titulo demasiado largo para boton"
Respuesta inválida para WhatsApp: entre 1 y 3 botones
Y un envío válido de dos botones produce este cuerpo:
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "51999000111",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "¿Cómo quieres recibir tu pedido?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "entrega:recojo", "title": "Recojo en tienda" } },
{ "type": "reply", "reply": { "id": "entrega:delivery", "title": "Delivery" } }
]
}
}
}
💡 Falla antes, no después
Validar en tu código ahorra una ida y vuelta a Meta y te da un error legible en tus pruebas. Si dejaras que la API rechace un título largo, recibirías un error genérico de parámetros (código 100, como verás abajo) y tendrías que adivinar cuál campo falló.
4.Listas
Cuando tienes más de tres opciones, usa una lista: un mensaje con un botón que abre un menú con filas. Es lo que usa el bot para mostrar las categorías del catálogo. Límites del código:
| Regla | Valor |
|---|---|
| Texto del botón que abre la lista | hasta 20 caracteres |
| Secciones | de 1 a 10 |
| Filas en total | de 1 a 10 |
| Título de sección | hasta 24 caracteres (obligatorio si hay más de una) |
| Título de fila | hasta 24 caracteres |
| Descripción de fila | hasta 72 caracteres (opcional) |
| Id de fila | hasta 200 caracteres |
Un ejemplo mínimo con el estilo del minimarket:
const cuerpo = construirEnvio('51999000111', {
tipo: 'lista',
texto: 'Elige una categoria',
boton: 'Ver categorias',
secciones: [{
titulo: 'Categorias',
filas: [{ id: 'cat:abarrotes', titulo: 'Abarrotes', descripcion: 'Arroz, azucar, aceite' }],
}],
});
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "51999000111",
"type": "interactive",
"interactive": {
"type": "list",
"body": { "text": "Elige una categoria" },
"action": {
"button": "Ver categorias",
"sections": [{
"title": "Categorias",
"rows": [{ "id": "cat:abarrotes", "title": "Abarrotes", "description": "Arroz, azucar, aceite" }]
}]
}
}
}
(El bloque de salida está reformateado en menos líneas para ahorrar espacio; el contenido es el mismo.)
Nota la diferencia de nombres entre el motor y la API: el motor usa botones, titulo, secciones y filas; la API usa buttons, title, sections y rows. construirEnvio es precisamente el traductor, y ese es el motivo por el que el motor no sabe nada de Meta: si Meta cambia un campo, solo tocas whatsapp.js.
📌 Nota · Cuando el cliente responde
Al tocar un botón o una fila, el cliente genera un mensaje entrante de tipo interactive con button_reply o list_reply. parsearEntrada lo convierte en { tipo: 'interactivo', id, titulo }, y el motor decide según el id. Así cierras el ciclo completo: enviar opciones, recibir la elección.
5.Enviar varias respuestas en orden
Un turno del motor puede producir varias respuestas (por ejemplo, un texto y luego un menú). El Worker las envía una por una, esperando cada una, para que lleguen en el orden correcto:
await enviarMensaje(env, construirLeido(evento.id), llamar).catch((e) => console.error(e.message));
for (const respuesta of respuestas) {
await enviarMensaje(env, construirEnvio(evento.de, respuesta), llamar);
}
Antes marca el mensaje del cliente como leído (los dos checks azules) con construirLeido; si eso falla, solo se registra y no se interrumpe la respuesta, porque es un detalle cosmético. Si lanzaras todas las peticiones a la vez con Promise.all, podrían llegar desordenadas.
6.Cuando algo falla: errores comunes
Cuando la API rechaza una petición, responde con un código HTTP y un JSON con un objeto error que incluye message y un code. La página oficial de códigos de error de WhatsApp lista cientos; estos son los que más verás al empezar:
| Código | Qué significa (según la documentación) | Qué hacer |
|---|---|---|
190 |
El token de acceso venció o no es válido | Genera un token nuevo (lección 12) |
100 |
Parámetros no admitidos o mal escritos | Revisa nombres y formato del cuerpo |
131047 |
Pasaron más de 24 horas desde la última respuesta del cliente | Envía una plantilla (lección 17) |
131026 |
El mensaje no se pudo entregar (número sin WhatsApp, términos sin aceptar, cliente desactualizado) | Pide al cliente que confirme que puede escribirte |
131051 |
Tipo de mensaje no soportado | Usa un tipo admitido |
130429 |
Alcanzaste el límite de velocidad de la Cloud API | Espera o reduce el ritmo |
131056 |
Demasiados mensajes en poco tiempo al mismo destinatario | Espera y reintenta solo para ese cliente |
132000 |
Faltan o sobran valores de variables en la plantilla | Pasa todos los parámetros que la plantilla define |
El error de token lo viste en la lección 13. Esto fue lo que el código del curso registró con un token falso (real, ejecutado con el Worker):
WhatsApp respondió 401: Invalid OAuth access token - Cannot parse access token
Nota que muestra el estado HTTP y el mensaje, pero ni rastro del token.
⚠️ Importante · Verifica este dato
Hay otro error muy frecuente al usar el número de prueba: intentar escribir a un destinatario que no registraste en el panel. En muchos tutoriales aparece como código 131030, pero la página de errores que consulté al escribir esta lección no lo detallaba. Búscalo en la lista oficial antes de basarte en él, y si tu error trae otro código, confía en el message de la respuesta.
Una regla práctica para decidir qué hacer con un error:
- No reintentes los errores de petición (
100,131051,132000): el cuerpo está mal y fallará siempre. - No reintentes
190hasta renovar el token. - Reintenta con espera los de límite de velocidad (
130429,131056). - Cambia de estrategia con
131047: pasa de texto libre a plantilla.
7.Una respuesta 200 no significa «entregado»
Cuando enviarMensaje devuelve datos sin lanzar error, solo sabes que Meta aceptó tu petición. La documentación oficial lo dice así: la respuesta únicamente confirma que la API aceptó el pedido, y la entrega real se informa por webhook. Un mensaje aceptado puede fallar después (por ejemplo, el cliente cambió de número o no tiene conexión mucho tiempo). Por eso el Worker registra los eventos de estado failed con su lista de errores:
else if (evento.tipo === 'estado' && evento.estado === 'failed') console.error('Mensaje fallido', evento.id, evento.errores);
Más adelante, las lecciones de memoria y pruebas y errores usarán estos estados para reintentar o avisar al dueño.
8.Errores frecuentes
- Mandar más de tres botones. Pasa a una lista: el límite es de 3 botones de respuesta.
- Títulos demasiado largos. 20 caracteres en botones y 24 en filas de lista; acorta o mueve el detalle a la descripción.
- Usar el título del botón como identificador. El título cambia con el idioma o el diseño; decide siempre por el
id. - Enviar en paralelo varias respuestas. Pueden llegar desordenadas; envíalas en secuencia.
- Reintentar a ciegas. Un error de formato o de token se repetirá infinitamente; clasifica antes de reintentar.
- Dar por entregado un 200. Revisa los estados del webhook.
- Registrar el token en logs. Al construir mensajes de error, nunca incluyas cabeceras ni credenciales.
9.Apuntes para llevar
- Enviar es un
POSTagraph.facebook.com/<versión>/<PHONE_NUMBER_ID>/messagesconAuthorization: Bearer. construirEnviotraduce las respuestas del motor (texto, botones, lista) al cuerpo de la API y valida los límites antes de enviar.- Máximo 3 botones de respuesta; para más opciones, una lista (hasta 10 filas).
- Decide por el id de botones y filas, no por su título.
- Envía las respuestas en orden, una por una.
- Clasifica los errores: algunos se corrigen, otros se reintentan y otros piden una plantilla.
- Un
200solo significa que Meta aceptó el mensaje; la entrega la dicen los estados del webhook.
10.Glosario
| Término | Significado |
|---|---|
| Graph API | API de Meta a la que se envían los mensajes de WhatsApp. |
| Phone Number ID | Identificador del número que envía; va en la ruta de la petición. |
| Bearer token | Esquema de la cabecera Authorization que lleva el token de acceso. |
| Botón de respuesta | Opción tocable (máximo 3) bajo un mensaje interactivo. |
| Lista | Mensaje con un botón que despliega hasta 10 filas organizadas en secciones. |
| wamid | Identificador que WhatsApp asigna a cada mensaje. |
| Estado de mensaje | Aviso posterior de entrega: sent, delivered, read o failed. |
| Límite de velocidad | Tope de mensajes por periodo que Meta permite a tu número. |
🧠 Comprueba lo aprendido
Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.
¿Dónde viaja el token de acceso en la petición de envío?
La API usa el esquema Bearer en la cabecera Authorization.
Quieres ofrecer 6 categorías de productos. ¿Qué tipo de mensaje usas?
Los botones de respuesta están limitados a tres; la lista admite hasta 10 filas.
Tu código lanza "título de botón de más de 20 caracteres". ¿Qué ocurrió?
La validación local evita una llamada inútil y da un error legible.
Recibes el error 131047. ¿Qué significa y qué haces?
Fuera de la ventana de 24 horas solo se permiten plantillas aprobadas.
enviarMensaje devolvió un id wamid sin errores. ¿Qué garantiza?
Un mensaje aceptado puede fallar después; escucha los estados sent, delivered, read y failed.
📚 Para profundizar (fuentes oficiales)
Este material lo hace un estudiante con ayuda de IA: ante cualquier duda, manda la documentación oficial.
Tu progreso se guarda en este navegador y se refleja en el índice del curso.
💡 🚀 ¡Ahora te toca!
La práctica fija lo aprendido. Pasa al ejercicio de esta lección.