Webhooks — Eventos de mensaje
receive_message, send_message, send_bot_message, status_message y los eventos de WhatsApp Flows.
Es la familia que cubre la conversación en sí: lo que entra, lo que sale y qué pasó con
cada envío.
| Evento | Cuándo se dispara |
|---|---|
receive_message | Entra un mensaje de un contacto |
send_message | Sale un mensaje desde la plataforma, la app o la API |
send_bot_message | Un chatbot responde. Payload anidado, ver abajo |
status_message | Cambia el estado de un mensaje que usted envió |
wa_flow_sent | Se envía un WhatsApp Flow |
wa_flow_response_received | El contacto responde un Flow |
Los seis se filtran por línea: solo recibe los de los números que marcó al configurar el
webhook.
Campos garantizados
Estos campos están siempre, con null cuando no hay valor:
| Campo | Tipo | Notas |
|---|---|---|
id | int | Id del mensaje en Wasapi |
user_id | int | Cuenta dueña |
sender_id | int | null | Agente que lo envió. null en los entrantes y en los que envía un bot, una campaña o la API |
from_id | int | La línea de WhatsApp por la que entró o salió |
message | string | null | El texto |
type | string | in (entrante), out (saliente), system (nota interna del sistema) |
message_type | string | Ver la tabla de tipos |
wa_id | string | ⚠️ La clave de conversación: teléfono o BSUID. Ver Identidad del contacto |
wam_id | string | null | Id del mensaje en Meta. Es la mejor llave para descartar duplicados |
context_wam_id | string | null | wam_id del mensaje citado, si es una respuesta |
status | string | sent, delivered, read, played, failed, n/a |
caption | string | null | Texto que acompaña a un archivo |
filename | string | null | Nombre del archivo adjunto |
data | string | ⚠️ Campo interno. No lo parsee — ver la advertencia abajo |
reactions | string | null | Reacciones al mensaje |
origin | string | null | Por dónde se originó: web, app, api, chatbot, campaign, webhook, wa_business, survey, make, n8n, system |
created_at | string | |
updated_at | string | |
deleted_at | string | null | |
bsuid | string | null | Ver Identidad del contacto |
wa_username | string | null | |
version | int | 1 |
dataes un campo interno de Wasapi, no JSON.Viene vacío en la mayoría de los mensajes y, cuando trae algo, su formato no forma parte
del contrato y puede cambiar sin aviso. No dependa de él. Para adjuntos usefilename
ycaption.
Valores de message_type
message_type| Valor | Qué es |
|---|---|
text | Texto |
image, audio, video, document, sticker | Multimedia |
template | Plantilla aprobada por Meta |
interactive | Mensaje con botones o lista |
button | Respuesta del contacto a un botón |
cta_url | Mensaje con botón de enlace |
flow_response | Respuesta de un WhatsApp Flow |
system | Nota interna (asignación de agente, cierre de conversación…) |
unsupported | Tipo que WhatsApp no soporta reenviar |
Pueden aparecer valores nuevos: Meta agrega tipos. Trate un valor desconocido como genérico
en vez de rechazar el evento.
wa_flow_response_received trae además flow_id y bsuids
wa_flow_response_received trae además flow_id y bsuids| Campo | Tipo | Notas |
|---|---|---|
flow_id | string | null | Id del Flow en Meta que el contacto respondió |
bsuids | array | Los BSUIDs del contacto, uno por portafolio de negocio de Meta, con la línea donde se observó cada uno |
Los dos están siempre en este evento. wa_flow_sent no los trae.
flow_id llega en null cuando no pudimos resolverlo: lo resolvemos correlacionando el
flow_token —que viaja dentro del JSON de message— con el envío que lo originó, así que si
ese envío no está registrado el campo va vacío. En ese caso el flow_token del propio
message sigue siendo la llave para correlacionar del lado de su integración.
bsuids responde «¿por cuál de mis números me escribió esta persona?». Un contacto que oculta
su teléfono tiene un identificador distinto por cada portafolio de negocio de Meta, y
bsuid (el campo suelto) trae solo el de esta conversación:
"wa_id": "MX.7788990011223344",
"bsuid": "MX.7788990011223344",
"wa_username": "ana.gomez",
"flow_id": "9876543210987654",
"bsuids": [
{
"bsuid": "CO.1234567890123456",
"line": { "display_name": "Ventas Colombia", "phone_number": "+57 300 123 4567", "phone_id": "109876543210987" }
},
{ "bsuid": "MX.7788990011223344", "line": null }
]line viene en null cuando no sabemos por qué línea se observó ese identificador — pasa con
los que aprendimos antes de guardar ese dato. phone_id es el id de la línea en Meta, el
mismo que usted ve en sus propios webhooks de Meta. La lista llega vacía ([]) si el contacto
ya no existe en su cuenta.
Campos extra, según el camino
Además del piso garantizado, cada evento puede traer campos propios. No están declarados y
pueden faltar: trátelos como opcionales.
| Campo extra | Aparece en |
|---|---|
error_code, error_message | status_message cuando status es failed |
context_message, context_type | Respuestas a un mensaje citado |
Ejemplo — receive_message
receive_message{
"event": "receive_message",
"data": {
"id": 900001,
"user_id": 1234,
"sender_id": null,
"from_id": 42,
"message": "Hola, quiero información de precios",
"type": "in",
"message_type": "text",
"wa_id": "573001234567",
"wam_id": "wamid.HBgMNTczMDAxMjM0NTY3FQIAEhgU…",
"context_wam_id": null,
"status": "n/a",
"caption": null,
"filename": null,
"data": "",
"reactions": null,
"origin": "webhook",
"created_at": "2026-08-07T15:04:11.000000Z",
"updated_at": "2026-08-07T15:04:11.000000Z",
"deleted_at": null,
"bsuid": null,
"wa_username": null,
"version": 1
}
}Ejemplo — status_message con fallo
status_message con fallo{
"event": "status_message",
"data": {
"id": 900002,
"user_id": 1234,
"from_id": 42,
"wa_id": "573001234567",
"wam_id": "wamid.HBgMNTczMDAxMjM0NTY3FQIAERgS…",
"type": "out",
"message_type": "template",
"status": "failed",
"error_code": 131047,
"error_message": "Re-engagement message",
"bsuid": null,
"wa_username": null,
"version": 1
}
}status_message es el único evento que le avisa que un envío no llegó. Si le importa la
entrega, actívelo.
send_bot_message tiene el payload anidado
send_bot_message tiene el payload anidadoEs la excepción de la familia: trae dos mensajes, el del contacto y la respuesta del bot.
{
"event": "send_bot_message",
"data": {
"user": {
"id": 900003,
"type": "in",
"message": "1",
"wa_id": "573001234567",
"bsuid": null,
"wa_username": null,
"...": "el resto del piso garantizado"
},
"bot": {
"id": 900004,
"type": "out",
"message": "Perfecto, le comparto el catálogo 👇",
"wa_id": "573001234567",
"bsuid": null,
"wa_username": null,
"...": "el resto del piso garantizado"
},
"version": 1
}
}Cada uno de los dos objetos cumple el piso garantizado por su cuenta. version va en la
raíz, no dentro de user ni de bot.
// El acceso NO es data['message'], sino:
$textoDelContacto = $data['user']['message'] ?? null;
$respuestaDelBot = $data['bot']['message'] ?? null;Notas de comportamiento
receive_message no distingue de qué contacto viene por sender_id
receive_message no distingue de qué contacto viene por sender_idsender_id es siempre null en los entrantes: identifica al agente que envía, no al
contacto. Para saber de quién es el mensaje use wa_id (más bsuid / wa_username).
Los mensajes de tipo system también disparan eventos
system también disparan eventosWasapi registra notas internas —asignación de agente, cierre de conversación— como mensajes
con type: "system" y message_type: "system". Si su integración solo quiere conversación
real, fíltrelos:
if ($data['type'] === 'system') {
return; // nota interna, no es un mensaje de la conversación
}origin le dice quién originó el mensaje
origin le dice quién originó el mensajeÚtil para no reprocesar sus propios envíos: un mensaje que usted mandó por la API vuelve
como send_message con origin: "api".
Ver también
- Identidad del contacto —
wa_idno siempre es un teléfono - Introducción — reintentos e idempotencia
Updated 11 days ago
