Mensajes de WhatsApp por plantilla
El canal de mensajería de WhatsApp permite despachar mensajes de plantilla ya aprobados por Meta por el número de WhatsApp de tu tenant, desde tu propio sistema y con una API key. Cada despacho queda registrado y se puede consultar después por su identificador.
Hasta dónde llega este servicio, hoy
Sección titulada «Hasta dónde llega este servicio, hoy»Un despacho avanza hasta «entregado», y también se registra cuándo el destinatario lo leyó. Ese avance lo puedes consultar o recibir en tu webhook.
Cuando el endpoint responde 201, lo que la plataforma afirma es que Meta aceptó el mensaje y
devolvió su identificador — todavía no que haya llegado al teléfono. Después llegan los acuses de
WhatsApp y el estado avanza solo: submitted → sent → delivered. El instante de lectura se
guarda aparte: leer no es un estado. Pero una lectura implica la entrega: si WhatsApp informa
que el mensaje se leyó y el acuse de entrega no había llegado, el mensaje avanza a delivered.
Si enciendes la notificación de estados en el webhook del canal, cada avance te llega como el
evento whatsapp.mensaje.estado: ver Recibir el estado de tus despachos.
Y por el mismo camino llegan los mensajes que te escriben: se registran, se te cobran, y se entregan al webhook de recepción que configures para el canal.
Requisitos Previos
Sección titulada «Requisitos Previos»Antes de despachar tu primer mensaje necesitas cuatro cosas:
1. El servicio WHATSAPP_MENSAJERIA habilitado
Sección titulada «1. El servicio WHATSAPP_MENSAJERIA habilitado»El tenant tiene que tener contratado el servicio de mensajería de WhatsApp. Si no lo tiene, la
llamada responde 403 diciéndolo.
2. El número de WhatsApp conectado
Sección titulada «2. El número de WhatsApp conectado»El canal se conecta desde el panel, en Comunicación → Canales. Un canal que no quedó conectado
—o cuyo token de Meta expiró— responde 422 al despachar.
3. Una API key con el rol WHATSAPP_MENSAJERIA_DESPACHO
Sección titulada «3. Una API key con el rol WHATSAPP_MENSAJERIA_DESPACHO»No sirve FULL-API, y es a propósito. El rol es angosto: habilita despachar y consultar un
despacho, y nada más. No habilita configurar el canal, conectar un número, consultar su salud ni
administrar plantillas.
La llave la crea un administrador del tenant desde el panel, en Configuración → API Keys. En el
desplegable de rol, el que corresponde es Despacho Mensajería WhatsApp
(WHATSAPP_MENSAJERIA_DESPACHO).
Ver Autenticación para el manejo general de API keys.
4. Una plantilla aprobada por Meta
Sección titulada «4. Una plantilla aprobada por Meta»Las plantillas las administra el tenant desde el panel, en el canal de WhatsApp → Plantillas. Esta guía no cubre cómo crearlas; lo que sí importa para integrar es:
- La plantilla tiene que estar aprobada por Meta antes de despacharla. Una plantilla que no
existe o no está aprobada produce un
422al enviar. - Se identifica por el par nombre + idioma (
templateName+languageCode), no por un id. - 🔴 Meta puede reclasificar la categoría de una plantilla. Está medido: una plantilla enviada
como
UTILITYvolvióMARKETING. El tenant no controla la categoría, y la categoría es lo que Meta cobra y limita distinto. Si un despacho se te empieza a rechazar por límites, ésa es la primera hipótesis.
Y el channelId
Sección titulada «Y el channelId»El identificador del canal va en la ruta. Lo obtienes del panel: es el segmento de la URL cuando
abres el canal en Comunicación → Canales
(/{tenantSlug}/comunicacion/canales/{channelId}).
La API key no lo lleva adentro: la llave no está atada a ningún canal, el canal lo determina la ruta y su pertenencia al tenant se comprueba en cada llamada.
Flujo del Despacho
Sección titulada «Flujo del Despacho»POST /{tenantSlug}/canales/whatsapp/{channelId}/enviar │ ▼ El canal es de este tenant, o no existe (404) │ ▼ Se escribe la fila del registro (estado: queued) │ ▼ POST a Meta con los componentes declarados │ ▼ Meta acusa → fila con wamid, estado: submitted │ ▼ Se emite el evento de cobro │ ▼ Respuesta: { id, wamid, estado, ... } │ ▼ Después, por su cuenta: los acuses de WhatsApp avanzan el estado a sent → delivered, y llenan la marca de lectura sin cambiar el estadoLa respuesta es síncrona: cuando la recibes, el mensaje ya salió (o ya falló).
La fila del registro se escribe antes de llamar a Meta, a propósito: el envío es irreversible, así que un despacho que falla igual queda registrado, con la causa exacta que devolvió Meta, en vez de desaparecer.
Request
Sección titulada «Request»Endpoint
Sección titulada «Endpoint»POST /{tenantSlug}/canales/whatsapp/{channelId}/enviarHeaders
Sección titulada «Headers»Authorization: Bearer {api_key}Content-Type: application/json{ "destinatario": "+56912345678", "templateName": "documento_firmado_v1", "languageCode": "es", "componentes": { "cuerpo": { "variables": [ { "posicion": 1, "valor": "María" }, { "posicion": 2, "valor": "Contrato 4821" } ] }, "boton": { "indice": "0", "variables": [ { "posicion": 1, "valor": "f/abc123XYZ" } ] } }, "referenciaExterna": "contrato-4821"}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
destinatario | string | Sí | Número del destinatario, con código de país |
templateName | string | Sí | Nombre exacto de la plantilla aprobada |
languageCode | string | Sí | Idioma exacto de la plantilla (es, es_CL, …) |
componentes | object | No | Los componentes de la plantilla. Ver abajo |
variables | array | No | Forma anterior: equivale a componentes.cuerpo.variables |
referenciaExterna | string | No | Tu referencia para reconocer el despacho. Ver abajo |
referenciaExterna vuelve en la respuesta, en la consulta del despacho
y en cada evento whatsapp.mensaje.estado. Hasta 128
caracteres: letras, dígitos, punto (.), guion bajo (_), dos puntos (:) y guion (-). Fuera de
esa forma el despacho responde 400 y no se envía nada.
- No pongas datos personales —RUT, teléfono, correo, nombre—: la referencia se registra en el log y se guarda junto al mensaje. Usa el identificador de tu sistema.
- No se envía a Meta: el destinatario no la ve.
- No es clave de idempotencia: dos despachos con la misma referencia son dos mensajes, y se cobran los dos.
El destinatario
Sección titulada «El destinatario»El número se normaliza a sólo dígitos —se le quitan +, espacios y guiones— y no se le agrega
código de país. Manda el número internacional completo:
| Lo que mandas | Lo que sale a Meta |
|---|---|
+56912345678 | 56912345678 |
56 9 1234 5678 | 56912345678 |
912345678 | 912345678 ❌ — sin código de país, Meta no lo va a resolver |
La respuesta trae destinatarioNormalizado con el número tal como Meta lo normalizó, que puede
diferir del que enviaste.
🔴 El contrato de variables: posicional POR COMPONENTE
Sección titulada «🔴 El contrato de variables: posicional POR COMPONENTE»Es la parte que más confunde, y conviene leerla dos veces.
Una plantilla de WhatsApp puede tener variables en tres lugares distintos: el encabezado, el
cuerpo y el botón de URL. Cada uno numera sus variables desde {{1}} por separado — así lo
define la API de Meta, no es una decisión de esta plataforma.
Toma esta plantilla:
Cuerpo: Hola {{1}}, tu documento {{2}} está listo para firmar.Botón: https://validafirma.cl/{{1}}Hay tres valores que llenar, pero la variable del botón es su propio {{1}}, no el {{3}}
del cuerpo. Un arreglo plano de tres variables no puede expresar esto.
{ "componentes": { "cuerpo": { "variables": [ { "posicion": 1, "valor": "María" }, { "posicion": 2, "valor": "Contrato 4821" } ] }, "boton": { "indice": "0", "variables": [ { "posicion": 1, "valor": "f/abc123XYZ" } ] } }}Las tres reglas que rompen la intuición
Sección titulada «Las tres reglas que rompen la intuición»1. Cada componente numera desde {{1}}. El posicion es 1-based y corresponde al {{n}} de
ese componente, no del mensaje completo.
2. El indice del botón viaja como cadena. "0", nunca 0. Es lo que exige Meta; declararlo
como número produce un cuerpo que Meta rechaza, y el rechazo llega recién al despachar. El índice es
la posición del botón en la plantilla: el primero es "0".
3. 🔴 Un componente que la plantilla no tiene se OMITE, no se manda vacío. Una plantilla sin
botón rechaza el envío que lo incluya. No mandes "boton": { "indice": "0", "variables": [] }
«por las dudas»: no es inofensivo, es un envío fallido.
Una plantilla sin botón, entonces, simplemente no declara ese componente:
{ "destinatario": "+56912345678", "templateName": "aviso_simple_v1", "languageCode": "es", "componentes": { "cuerpo": { "variables": [{ "posicion": 1, "valor": "María" }] } }}Una plantilla sin ninguna variable se despacha sin componentes:
{ "destinatario": "+56912345678", "templateName": "aviso_fijo_v1", "languageCode": "es"}Encabezado de imagen
Sección titulada «Encabezado de imagen»Si la plantilla lleva un encabezado de imagen, el componente acepta el identificador de material de Meta que tú ya subiste:
{ "componentes": { "encabezado": { "mediaId": "1639350477750383" }, "cuerpo": { "variables": [{ "posicion": 1, "valor": "María" }] } }}La plataforma acepta ese identificador; no lo produce. La subida del material a Meta la haces
tú, contra la API de Meta. Y es el id del material —lo que Meta devuelve al subirlo—, nunca un
header_handle (que es de la creación de la plantilla) ni una URL de descarga.
La forma plana variables
Sección titulada «La forma plana variables»El campo variables sigue aceptado y es exactamente componentes.cuerpo.variables:
{ "destinatario": "+56912345678", "templateName": "aviso_simple_v1", "languageCode": "es", "variables": [{ "posicion": 1, "valor": "María" }]}Ejemplo con curl
Sección titulada «Ejemplo con curl»curl -X POST "https://api.redcumbre.cl/{tenantSlug}/canales/whatsapp/{channelId}/enviar" \ -H "Authorization: Bearer {api_key}" \ -H "Content-Type: application/json" \ -d '{ "destinatario": "+56912345678", "templateName": "documento_firmado_v1", "languageCode": "es", "componentes": { "cuerpo": { "variables": [ { "posicion": 1, "valor": "María" }, { "posicion": 2, "valor": "Contrato 4821" } ] }, "boton": { "indice": "0", "variables": [{ "posicion": 1, "valor": "f/abc123XYZ" }] } } }'Response
Sección titulada «Response»Respuesta Exitosa (201)
Sección titulada «Respuesta Exitosa (201)»{ "success": true, "data": { "id": "cmtugt4ln0003se8nqas72beu", "wamid": "wamid.HBgLNTY5ODQzMDYyNDQVAgARGBI0QTMxRjNBREJBNDEyMjMxQUYA", "estado": "submitted", "destinatarioNormalizado": "56912345678", "sandbox": false, "cobrado": true, "noCobradoMotivo": null, "referenciaExterna": "contrato-4821" }}| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador de la fila del registro |
wamid | string | Identificador del mensaje en Meta |
estado | string | submitted cuando Meta aceptó. Avanza a sent y delivered con los acuses. Ver estados |
destinatarioNormalizado | string | El número tal como Meta lo normalizó |
sandbox | boolean | true si el despacho fue simulado |
cobrado | boolean | Si se emitió el evento de facturación |
noCobradoMotivo | string | null | sandbox o sin_suscripcion cuando cobrado es false |
referenciaExterna | string | null | La que mandaste en el body, tal cual. null si no la mandaste |
Estados del Mensaje
Sección titulada «Estados del Mensaje»| Estado | Terminal | Descripción |
|---|---|---|
queued | No | La fila se escribió, el despacho está en curso |
submitted | No | Aceptado por Meta. Todavía sin acuse |
sent | No | WhatsApp acusó que el mensaje salió |
delivered | Sí | WhatsApp acusó la entrega al destinatario, o informó que lo leyó. Es hasta donde llega un saliente |
rejected | Sí | Meta rechazó el envío por una causa de negocio |
failed | Sí, salvo lectura | La llamada falló sin respuesta de negocio de Meta, o WhatsApp acusó que no pudo entregarlo. Si después WhatsApp informa que se leyó, avanza a delivered |
failed y rejected no son lo mismo, y la diferencia importa para atender un reclamo: rejected es
«Meta dijo que no», failed es «no sabemos si Meta lo vio».
Recibir los mensajes que te escriben
Sección titulada «Recibir los mensajes que te escriben»Cada vez que alguien le escribe al número del canal, la plataforma registra el mensaje, te lo cobra y te avisa al webhook que configures para ese canal.
El destino se configura desde el panel, en el canal: Canales → tu número → Webhook de recepción. No hay endpoint de API para configurarlo: es una superficie humana a propósito.
Lo que recibes
Sección titulada «Lo que recibes»POST https://tu-sistema.cl/hooks/whatsappContent-Type: application/jsonX-Webhook-Signature: t=1789008127,v1=3f2a…{ "event": "whatsapp.mensaje.entrante", "canalId": "080a7de6-02e2-4682-8c0a-e27ba4a2d623", "contenidoIncluido": true, "mensaje": { "wamid": "wamid.HBgLNTY5ODQzMDYyNDQVAgARGBI…", "de": "56984306244", "tipo": "text", "cuerpo": "hola, necesito ayuda", "recibidoEn": "2026-09-10T02:42:07.000Z", "respondeA": null, "numeroReceptor": "15550823902" }}| Campo | Siempre viene | Qué es |
|---|---|---|
event | Sí | Siempre whatsapp.mensaje.entrante |
canalId | Sí | El canal que recibió el mensaje. Es el mismo de la ruta de despacho |
contenidoIncluido | Sí | false cuando el mensaje no es texto: ahí cuerpo viene null |
mensaje.wamid | Sí | Identificador del mensaje en WhatsApp. Es único: úsalo para deduplicar |
mensaje.de | Sí | Quién escribió, en formato internacional sin + |
mensaje.tipo | Sí | El tipo tal como lo nombra WhatsApp: text, audio, image, location… |
mensaje.cuerpo | No | El texto. Sólo en los mensajes de texto; null en todos los demás |
mensaje.recibidoEn | Sí | Cuándo lo recibió WhatsApp, en ISO-8601 |
mensaje.respondeA | No | El wamid del mensaje al que responde, si responde a alguno |
mensaje.numeroReceptor | No | Tu número, tal como WhatsApp lo declara |
Cómo verificas que el aviso es nuestro
Sección titulada «Cómo verificas que el aviso es nuestro»La cabecera X-Webhook-Signature viene en el formato t=<unix>,v1=<hmac>, donde hmac es un
HMAC-SHA256 de <unix>.<cuerpo crudo> con el secreto del canal.
import { createHmac } from 'node:crypto';
function firmaValida(cabecera, cuerpoCrudo, secreto) { const [t, v1] = cabecera.split(','); const unix = t.replace('t=', ''); const esperado = createHmac('sha256', secreto) .update(`${unix}.${cuerpoCrudo}`) .digest('hex'); return v1 === `v1=${esperado}`;}Es el mismo formato que el resto de los webhooks de la plataforma. Verifica siempre: sin la firma, cualquiera que conozca tu URL puede mandarte un aviso.
Reintentos
Sección titulada «Reintentos»Si tu endpoint no responde 2xx, se reintenta 12 veces con espera creciente de 1 s a 1 min: una
ventana total de unos 6 minutos. Agotados los intentos, el mensaje queda marcado como perdido en
el registro del canal —y ahí lo puedes ver— pero no se vuelve a intentar.
El mensaje se cobra igual, incluso si tu endpoint nunca lo aceptó: lo recibiste.
Si el canal no tiene webhook configurado
Sección titulada «Si el canal no tiene webhook configurado»El mensaje se registra y se cobra, y no se entrega a ninguna parte. Lo ves en el panel, en el registro del canal, con el desenlace «Sin webhook configurado». No cae al webhook general del tenant: son cosas distintas y mezclarlas mandaría los mensajes de un número al sistema equivocado.
Recibir el estado de tus despachos
Sección titulada «Recibir el estado de tus despachos»Además de los mensajes que te escriben, el webhook del canal puede recibir qué pasó con cada mensaje que despachaste: enviado, entregado o leído, o fallido con su causa. Es opcional y viene apagado.
Cómo lo enciendes
Sección titulada «Cómo lo enciendes»En el panel, en el canal: Canales → tu número → Webhook de recepción, tarjeta «Estado de tus despachos», interruptor «Notificar el estado de tus despachos». La tarjeta aparece cuando el canal ya tiene un webhook configurado: los eventos llegan a esa misma URL, firmados con el mismo secreto.
Son unos tres avisos por mensaje. Encenderlo o apagarlo no cambia los mensajes que te escriben ni los ecos: siguen llegando igual.
Lo que recibes del estado
Sección titulada «Lo que recibes del estado»POST https://tu-sistema.cl/hooks/whatsappContent-Type: application/jsonX-Webhook-Signature: t=1789400356,v1=9c1d…{ "event": "whatsapp.mensaje.estado", "canalId": "601b51f8-4422-4394-85fb-9cfc3eb73f92", "aviso": { "avisoId": "3b9f0c6e-6a51-4c0e-9d1a-2f4b7c8e5a10", "mensajeId": "cmu1eqvzj000ljw66po1sduu3", "wamid": "wamid.HBgLNTY5MTIzNDU2NzgVAgARGBI…", "referenciaExterna": "contrato-4821", "procedencia": "api", "plantilla": { "nombre": "documento_firmado_v1", "idioma": "es" }, "contraparteNormalizada": "56912345678", "destinatarioProveedor": "56912345678", "estado": "delivered", "estadoCrudo": "read", "estadoAnterior": "sent", "secuencia": 40, "ocurridoEn": "2026-09-14T15:39:15.000Z", "recibidoEn": "2026-09-14T15:39:16.204Z", "entregadoEn": "2026-09-14T15:39:15.000Z", "leidoEn": "2026-09-14T15:39:15.000Z", "causa": null, "proveedor": { "id": "wamid.HBgLNTY5MTIzNDU2NzgVAgARGBI…", "status": "read", "timestamp": "1789400355", "recipient_id": "56912345678" } }}| Campo | Siempre viene | Qué es |
|---|---|---|
event | Sí | Siempre whatsapp.mensaje.estado |
canalId | Sí | El canal que despachó el mensaje |
aviso.avisoId | Sí | Identificador único del aviso. Úsalo para deduplicar |
aviso.mensajeId | Sí | El id que devolvió el despacho: la clave de GET …/mensajes/{id} |
aviso.wamid | Sí | Identificador del mensaje en WhatsApp |
aviso.referenciaExterna | No | La que mandaste al despachar, tal cual. null si no la mandaste |
aviso.procedencia | Sí | Siempre api: sólo se notifican los mensajes que despachó la plataforma |
aviso.plantilla | Sí | Nombre e idioma de la plantilla enviada |
aviso.contraparteNormalizada | Sí | El destinatario, en formato internacional sin + |
aviso.destinatarioProveedor | No | El destinatario tal como lo informa WhatsApp. Puede diferir del anterior por la normalización del país |
aviso.estado | Sí | El estado del mensaje justo después de este aviso: sent, delivered o failed. No cambia si el evento se reintenta |
aviso.estadoCrudo | Sí | Lo que WhatsApp mandó: sent, delivered, read o failed |
aviso.estadoAnterior | No | El estado del mensaje antes de este aviso |
aviso.secuencia | Sí | El orden del estado: a mayor secuencia, más avanzado |
aviso.ocurridoEn | Sí | Cuándo ocurrió, según WhatsApp, en ISO-8601 |
aviso.recibidoEn | Sí | Cuándo lo recibió la plataforma |
aviso.entregadoEn | No | ocurridoEn si este aviso produjo la entrega, acusada o implícita por una lectura. Si no, null |
aviso.leidoEn | No | ocurridoEn si este aviso registró la lectura. Si no, null |
aviso.causa | No | Sólo con estado: failed. Ver abajo |
aviso.proveedor | Sí | El aviso de WhatsApp tal como llegó, sin sus datos de tarificación |
Con estado: failed, causa trae la razón que dio WhatsApp:
{ "codigo": "131026", "titulo": "Message undeliverable", "detalle": "Message Undeliverable.", "elegibleParaRespaldo": true, "requiereIntervencion": false, "desconocido": false}elegibleParaRespaldo dice si conviene enviar el mismo contenido por otro medio —un SMS, un
correo—; requiereIntervencion, si hay que corregir algo antes de reintentar; y desconocido, si
el código no está clasificado.
Las reglas del evento
Sección titulada «Las reglas del evento»- Al menos una vez. El mismo aviso puede llegar más de una vez: deduplica por
avisoId. - Sin orden garantizado. WhatsApp desordena los avisos, y además un reintento puede llegar
después de un aviso posterior. Aplica el estado de mayor
secuenciay descarta el resto. readpuede llegar sindelivered, pero el evento ya viene conestado: delivered. UnleidoEncon valor implica entregado.- Tu temporizador manda. El evento acelera tu decisión, no la reemplaza. Un evento perdido
(12 intentos en unos 6 minutos) nunca debe dejar un mensaje sin respaldo. Al vencer tu plazo,
GET …/mensajes/{id}es la fuente de verdad. - Responde 2xx a todo evento, también a los que ignoras. Con otra respuesta, la plataforma reintenta, deja el aviso perdido y alerta a operación.
- Los mensajes que te escriben y los ecos no cambian con esta opción.
- Sin eventos en sandbox ni para los rechazos del despacho: esos ya están en la respuesta del
POST. - Un destino por canal. Si tienes varios entornos, usa un canal por entorno.
- Cada intento se firma de nuevo. En
X-Webhook-Signature: t=<unix>,v1=<hmac>,tes el instante de ese intento, y el HMAC-SHA256 se calcula sobre<t>.<cuerpo crudo>con el secreto del canal. Una ventana de tolerancia de ±300 s sobretalcanza también para el intento 12. Verifica sobre los bytes crudos del cuerpo, nunca sobre un JSON vuelto a serializar: ver Cómo verificas que el aviso es nuestro. - Toda respuesta que no sea 2xx se reintenta, incluidas
401y503, hasta el intento 12. Ningún status descarta el aviso en el primer intento. Un401persistente —por ejemplo, un secreto mal configurado en tu receptor— agota los 12 intentos y deja el aviso perdido. - Responde rápido. Cada intento espera hasta 30 segundos. Un timeout cuenta como fallo y se reintenta. Guarda el aviso, responde, y procésalo después.
- Un reintento de un aviso que ya guardaste debe recibir 2xx: tu deduplicación por
avisoIdlo reconoce y responde éxito. Si respondes error, el aviso termina perdido aunque lo tengas.
Número en coexistencia con WhatsApp Business App
Sección titulada «Número en coexistencia con WhatsApp Business App»Si conectaste el número en coexistencia —sigues atendiendo desde WhatsApp Business App en tu celular y además usas la plataforma—, hay dos cosas más que llegan a tu sistema y al registro del canal.
Los mensajes que envías desde el celular
Sección titulada «Los mensajes que envías desde el celular»Cada mensaje que el negocio envía desde WhatsApp Business App (o un dispositivo vinculado) se
registra en el canal, se cobra a un precio menor que un despacho y se avisa al mismo webhook de
recepción, firmado igual y con los mismos reintentos, con el evento whatsapp.mensaje.eco:
{ "event": "whatsapp.mensaje.eco", "canalId": "080a7de6-02e2-4682-8c0a-e27ba4a2d623", "contenidoIncluido": true, "mensaje": { "wamid": "wamid.HBgLNTY5ODQzMDYyNDQVAgARGBI…", "para": "56984306244", "tipo": "text", "cuerpo": "hola, te escribo desde el celular", "enviadoEn": "2026-09-13T15:55:37.000Z", "numeroEmisor": "56900000000" }}| Campo | Siempre viene | Qué es |
|---|---|---|
event | Sí | Siempre whatsapp.mensaje.eco |
canalId | Sí | El canal del número |
contenidoIncluido | Sí | false cuando el mensaje no es texto: ahí cuerpo viene null |
mensaje.wamid | Sí | Identificador del mensaje en WhatsApp. Es único: úsalo para deduplicar |
mensaje.para | Sí | A quién se lo enviaste, en formato internacional sin + |
mensaje.tipo | Sí | El tipo tal como lo nombra WhatsApp |
mensaje.cuerpo | No | El texto, sólo en los mensajes de texto |
mensaje.enviadoEn | Sí | Cuándo se envió desde la app, en ISO-8601 |
mensaje.numeroEmisor | No | Tu número, tal como WhatsApp lo declara |
El event es lo que distingue un eco de un entrante: ramifica por event, no por la forma del
cuerpo. Los mensajes que despacha la plataforma no generan eco.
El historial sincronizado
Sección titulada «El historial sincronizado»Al conectar en coexistencia, la plataforma pide a WhatsApp tus contactos y hasta 180 días de historial de la app. El historial queda en el registro del canal con procedencia «Historial sincronizado», no se cobra y no se avisa a tu webhook.
De dónde llegó cada mensaje: procedencia
Sección titulada «De dónde llegó cada mensaje: procedencia»El detalle de un mensaje trae el campo procedencia:
procedencia | Qué es | Se cobra | Se avisa a tu webhook |
|---|---|---|---|
api | Lo despachó la plataforma (tu API key o el panel) | Sí | Sólo su estado, whatsapp.mensaje.estado, si lo encendiste |
webhook | Alguien le escribió al número | Sí | Sí, whatsapp.mensaje.entrante |
app_celular | Lo enviaste desde WhatsApp Business App | Sí, precio de eco | Sí, whatsapp.mensaje.eco |
historial | Llegó con la sincronización del historial | No | No |
Si desvinculas el número desde la app
Sección titulada «Si desvinculas el número desde la app»Si desconectas la plataforma desde WhatsApp Business App, WhatsApp nos avisa: el canal queda
desvinculado, los administradores del tenant reciben un correo y una notificación, y el despacho
responde 409 whatsapp_canal_desvinculado hasta que alguien vuelva a conectar el número desde el
panel.
Códigos de Error
Sección titulada «Códigos de Error»| Código | Descripción | Qué significa para ti |
|---|---|---|
| 400 | Payload inválido | Falta un campo, o mandaste variables y componentes juntos |
| 401 | No autorizado | API key inválida, expirada o revocada |
| 403 | Prohibido | El rol de la key no alcanza este endpoint, o el servicio WHATSAPP_MENSAJERIA no está habilitado |
| 404 | No encontrado | El canal no existe o no es de tu tenant — indistinguibles a propósito |
| 409 | whatsapp_canal_desvinculado | El número estaba en coexistencia y se desvinculó desde WhatsApp Business App. No se escribió ningún mensaje ni se llamó a WhatsApp. El administrador tiene que volver a conectarlo desde el panel |
| 422 | Meta rechazó el envío | Trae la causa real de Meta. Ver abajo |
| 429 | Límite de mensajería alcanzado | Es el límite del número de tu tenant, no un fallo de la plataforma |
422 — la causa real de Meta, sin traducir
Sección titulada «422 — la causa real de Meta, sin traducir»El 422 es el error que más vas a ver, y el cuerpo trae el texto exacto que devolvió Meta, no un
mensaje genérico. El campo error te da un código con el que ramificar:
{ "statusCode": 422, "error": "whatsapp_envio_rechazado", "message": "(#132001) Template name does not exist in the translation", "mensajeId": "cmtusmgup0003see2h7njed6t"}El mensajeId es la fila que quedó escrita con esta misma causa: pásaselo a GET /{tenantSlug}/canales/whatsapp/{channelId}/mensajes/{id} y te devuelve errorCodigo y errorMensaje sin que tengas que abrir el panel.
error | Qué pasó | Qué hacer |
|---|---|---|
whatsapp_envio_rechazado | Problema de la plantilla —no existe, no está aprobada, los parámetros no calzan— o la ventana de conversación de 24 h está cerrada | Lee message: viene de Meta y dice cuál de los casos es. No reintentes igual |
whatsapp_token_invalido | El token del número expiró o fue revocado | No lo puedes arreglar desde tu sistema. El administrador del tenant tiene que reconectar el número en el panel |
whatsapp_channel_sin_credenciales | El canal no está conectado | Ídem: se reconecta desde el panel |
429 — el límite es del número, no de la plataforma
Sección titulada «429 — el límite es del número, no de la plataforma»{ "statusCode": 429, "error": "whatsapp_limite_alcanzado", "message": "...", "mensajeId": "cmtusmgup0003see2h7njed6t"}Meta le asigna a cada número un límite de mensajería que depende de su calidad y su
historial, y ese límite es del tenant. La plataforma no tiene un rate limit propio sobre este
endpoint: el 429 que recibes viene de Meta.
Es reintentable con backoff, pero no ese mismo segundo: si agotaste el límite del día, reintentar en un bucle sólo agrega llamadas rechazadas.
Un error de Meta que no cae en ninguna de las categorías de arriba llega como 500. Es reintentable
con backoff exponencial, igual que cualquier 5xx. Ver
Reglas transversales.
Consultar el Estado de un Despacho
Sección titulada «Consultar el Estado de un Despacho»GET /{tenantSlug}/canales/whatsapp/{channelId}/mensajes/{id}El {id} es el de la fila del registro —el que devolvió el despacho—, no el wamid.
curl "https://api.redcumbre.cl/{tenantSlug}/canales/whatsapp/{channelId}/mensajes/cmtugt4ln0003se8nqas72beu" \ -H "Authorization: Bearer {api_key}"{ "success": true, "data": { "id": "cmtugt4ln0003se8nqas72beu", "createdAt": "2026-09-09T19:01:56.400Z", "updatedAt": "2026-09-09T19:01:56.931Z", "direccion": "saliente", "procedencia": "api", "canalId": "080a7de6-02e2-4682-8c0a-e27ba4a2d623", "contraparte": "+56912345678", "contraparteNormalizada": "56912345678", "plantillaNombre": "documento_firmado_v1", "plantillaIdioma": "es", "plantillaComponentes": { "cuerpo": { "variables": [{ "posicion": 1, "valor": "María" }, { "posicion": 2, "valor": "Contrato 4821" }] }, "boton": { "indice": "0", "variables": [{ "posicion": 1, "valor": "f/abc123XYZ" }] } }, "estado": "delivered", "wamid": "wamid.HBgLNTY5…", "errorCodigo": null, "errorMensaje": null, "sandbox": false, "cobrado": true, "noCobradoMotivo": null, "aceptadoEn": "2026-09-09T19:01:56.931Z", "entregadoEn": "2026-09-09T19:01:59.000Z", "leidoEn": "2026-09-09T19:03:12.000Z", "tipoMensaje": null, "cuerpo": null, "entregaWebhookEstado": "no_aplica", "entregaWebhookIntentos": 0, "entregaWebhookEn": null }}Lo que trae, y nada más: la plantilla, las variables que enviaste, el estado, el wamid, la
causa exacta del error si lo hubo, las marcas de tiempo —incluidas la de entrega y la de
lectura— y la marca de sandbox. Es lo que necesitas para responder un reclamo.
Los cinco campos del final describen la mitad entrante del registro, y en un saliente vienen como en el ejemplo:
| Campo | En un saliente | En un entrante |
|---|---|---|
entregadoEn | El acuse de entrega de WhatsApp, o null | null |
leidoEn | El acuse de lectura, o null. No cambia el estado | null |
tipoMensaje | null — un saliente es una plantilla | El tipo tal como lo nombra WhatsApp |
cuerpo | null | El texto, sólo si el mensaje es de texto |
entregaWebhookEstado | no_aplica — no hay aviso que entregar | entregado, entregado_sin_contenido, perdido o sin_webhook |
entregaWebhookIntentos | 0 | Cuántas veces se intentó avisarle a tu sistema |
entregaWebhookEn | null | Cuándo quedó cerrado el desenlace |
Un mensaje de otro tenant, de otro canal, o que no existe, responde 404 — los tres
indistinguibles, nunca 403.
Modo Sandbox
Sección titulada «Modo Sandbox»Si la API key se creó en modo sandbox, el despacho:
- no llama a Meta — no sale ningún mensaje;
- no genera cobro;
- sí escribe la fila del registro, marcada como sandbox.
La respuesta declara el modo, para que tu sistema pueda distinguir lo simulado de lo real sin cambiar de endpoint:
{ "success": true, "data": { "id": "cmtugtxaq0005se8n63n27jx3", "wamid": "wamid.SANDBOX-cmtugtxaq0005se8n63n27jx3", "estado": "submitted", "sandbox": true, "cobrado": false, "noCobradoMotivo": "sandbox" }}El wamid de sandbox lleva el prefijo wamid.SANDBOX-. Ver
Entornos y Sandbox.
Integración JavaScript
Sección titulada «Integración JavaScript»const BASE = 'https://api.redcumbre.cl';
async function despacharWhatsapp({ tenantSlug, channelId, apiKey, destinatario, template }) { const response = await fetch( `${BASE}/${tenantSlug}/canales/whatsapp/${channelId}/enviar`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ destinatario, templateName: template.nombre, languageCode: template.idioma, componentes: template.componentes, }), } );
const body = await response.json();
if (!response.ok) { // `message` puede ser string o array de strings (validación). // En 422 y 429, `error` trae el código y `message` la causa real de Meta. throw new Error(`${body.error ?? response.status}: ${body.message}`); }
const { id, wamid, estado } = body.data; // Guarda `id`: es con lo que consultas después. El `estado` de la respuesta es "submitted" y // avanza solo con los acuses de WhatsApp — consultalo, no lo supongas. return { id, wamid, estado };}Preguntas frecuentes
Sección titulada «Preguntas frecuentes»¿Puedo mandar texto libre en vez de una plantilla? No. Este endpoint despacha plantillas aprobadas, que es lo que Meta permite para iniciar una conversación con alguien que no te escribió primero.
¿Cómo sé si el mensaje llegó?
Por el estado del mensaje: cuando WhatsApp acusa la entrega, pasa a delivered. Y si el
destinatario lo leyó, el campo leidoEn se llena — sin cambiar el estado. Los acuses llegan en
segundos, pero desordenados: consultá el estado, no supongas el orden.
¿Puedo recibir las respuestas de mis destinatarios? Sí. Configurá el webhook de recepción del canal desde el panel: cada mensaje que te escriban se te entrega firmado, con reintentos. Ojo con una cosa: un mensaje que no es texto llega sin su contenido — recibís el tipo, quién escribió y cuándo, y el archivo queda en WhatsApp. Se cobra igual. Está explicado arriba y en la sección del webhook.
¿La API key sirve para varios canales? Sí. La llave no está atada a ningún canal: el canal va en la ruta, y en cada llamada se comprueba que sea de tu tenant.
¿Puedo crear plantillas por API con esta llave?
No. El rol WHATSAPP_MENSAJERIA_DESPACHO no alcanza la administración de plantillas. Se crean desde
el panel.
Siguiente paso
Sección titulada «Siguiente paso»- Autenticación — API keys y modo sandbox
- Reglas transversales — forma de los errores, reintentos, paginación
- Envío de SMS — el otro canal de mensajería del tenant