Estructura de Payloads
Cada evento tiene una estructura de payload específica. Esta página documenta los más importantes.message.received
El payload demessage.received usa una estructura híbrida: el campo message contiene el mensaje exactamente como lo envía Meta, más IDs propios de Whaapy y extras de valor agregado.
Campos Base
Estos campos siempre están presentes:| Campo | Tipo | Descripción |
|---|---|---|
whaapy_message_id | string | ID interno del mensaje en Whaapy |
conversation_id | string | ID de la conversación en Whaapy |
contact_id | string | ID del contacto en Whaapy |
message | object | Mensaje original de Meta (estructura idéntica a la API de Meta) |
contact_name | string | Nombre del contacto (si está disponible) |
Campos Extras (Valor Agregado)
Estos campos aparecen según el tipo de mensaje:| Campo | Tipo | Cuándo aparece | Descripción |
|---|---|---|---|
media_url | string | image, video, audio, document, sticker | URL con token (24h) para descargar media |
transcription | string | audio | Texto transcrito de la nota de voz |
¿Por qué estructura híbrida? El campo
message usa exactamente el formato de Meta, permitiéndote reutilizar código existente. Los extras de Whaapy (media_url, transcription) te ahorran llamadas adicionales a la API.Ejemplos por Tipo de Mensaje
- Texto
- Audio
- Imagen
- Video
- Documento
- Ubicación
- Contacto
- Sticker
- Flow Reply
{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "text",
"text": {
"body": "Hola, necesito ayuda"
}
},
"contact_name": "Juan Pérez"
}
}
{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "audio",
"audio": {
"mime_type": "audio/ogg; codecs=opus",
"sha256": "abc123def456...",
"id": "media_id_de_meta",
"voice": true
}
},
"contact_name": "Juan Pérez",
"media_url": "https://api.whaapy.com/messages/msg-uuid/media?token=...",
"transcription": "Hola, quería preguntar sobre el precio del producto"
}
}
transcription solo aparece si la transcripción fue exitosa. El media_url tiene un token válido por 24 horas.{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "image",
"image": {
"caption": "Mira esta foto del producto",
"mime_type": "image/jpeg",
"sha256": "abc123def456...",
"id": "media_id_de_meta"
}
},
"contact_name": "Juan Pérez",
"media_url": "https://api.whaapy.com/messages/msg-uuid/media?token=..."
}
}
{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "video",
"video": {
"caption": "Video del problema que tengo",
"mime_type": "video/mp4",
"sha256": "abc123def456...",
"id": "media_id_de_meta"
}
},
"contact_name": "Juan Pérez",
"media_url": "https://api.whaapy.com/messages/msg-uuid/media?token=..."
}
}
{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "document",
"document": {
"filename": "factura-enero-2025.pdf",
"mime_type": "application/pdf",
"sha256": "abc123def456...",
"id": "media_id_de_meta"
}
},
"contact_name": "Juan Pérez",
"media_url": "https://api.whaapy.com/messages/msg-uuid/media?token=..."
}
}
{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "location",
"location": {
"latitude": 19.4326,
"longitude": -99.1332,
"name": "Mi Casa",
"address": "Av. Reforma 123, CDMX"
}
},
"contact_name": "Juan Pérez"
}
}
{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "contacts",
"contacts": [
{
"name": {
"formatted_name": "María García",
"first_name": "María",
"last_name": "García"
},
"phones": [
{ "phone": "+5215598765432", "type": "CELL", "wa_id": "5215598765432" }
],
"emails": [
{ "email": "maria@example.com", "type": "WORK" }
]
}
]
},
"contact_name": "Juan Pérez"
}
}
{
"event": "message.received",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1705312200",
"type": "sticker",
"sticker": {
"mime_type": "image/webp",
"sha256": "abc123def456...",
"id": "media_id_de_meta",
"animated": false
}
},
"contact_name": "Juan Pérez",
"media_url": "https://api.whaapy.com/messages/msg-uuid/media?token=..."
}
}
{
"event": "message.received",
"timestamp": "2026-03-09T18:30:00.000Z",
"businessId": "uuid",
"data": {
"whaapy_message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"message": {
"from": "5215512345678",
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"timestamp": "1741545000",
"type": "interactive",
"interactive": {
"type": "nfm_reply",
"nfm_reply": {
"name": "booking_flow",
"body": "Sucursal Centro · 2026-03-10 16:00",
"response_json": "{\"screen\":\"BOOK_APPOINTMENT\",\"values\":{\"branch\":\"Sucursal Centro\",\"slot\":\"2026-03-10 16:00\",\"service\":\"demo\"}}"
}
},
"context": {
"id": "wamid.HBgNflowOriginal"
}
},
"contact_name": "Juan Pérez"
}
}
Cuando Whaapy recibe un
interactive.type = "nfm_reply", lo persiste como mensaje interactive en el inbox y mantiene el payload original de Meta dentro de message.interactive.nfm_reply.Sobre media_url vs message.*.id
message.image.id (Meta)
ID de media de Meta. Requiere llamada adicional a su API para obtener la URL de descarga.
media_url (Whaapy)
URL lista para descargar con token incluido. Sin llamadas adicionales.
- Token válido: 24 horas
- Expiración de media: WhatsApp elimina media después de 30 días
Para referencia del formato Meta, consulta la documentación oficial.
message.sent
Cuando tu negocio envía un mensaje (manual o por IA).{
"event": "message.sent",
"timestamp": "2025-01-15T10:31:00.000Z",
"businessId": "uuid",
"data": {
"message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"to": "+5215512345678",
"contact_name": "Juan Pérez",
"type": "text",
"content": "¡Hola! ¿En qué puedo ayudarte?",
"sent_by_ai": true,
"timestamp": "2025-01-15T10:31:00.000Z",
"provider": "meta"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
message_id | string | ID del mensaje en Whaapy |
conversation_id | string | ID de la conversación |
to | string | Número de destino |
type | string | Tipo de mensaje (text, image, template, etc.) |
content | string | Contenido del mensaje |
sent_by_ai | boolean | true si fue enviado por el agente de IA |
conversation.created
Cuando se inicia una nueva conversación.{
"event": "conversation.created",
"timestamp": "2025-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"conversation_id": "conv-uuid",
"contact_id": "contact-uuid",
"phone_number": "+5215512345678",
"contact_name": "Juan Pérez",
"timestamp": "2025-01-15T10:30:00.000Z"
}
}
conversation.assigned
Cuando una conversación se asigna a un agente humano.{
"event": "conversation.assigned",
"timestamp": "2025-01-15T10:32:00.000Z",
"businessId": "uuid",
"data": {
"conversation_id": "conv-uuid",
"phone_number": "+5215512345678",
"contact_name": "Juan Pérez",
"assigned_to": "agent-uuid",
"assigned_by": "admin-uuid",
"method": "manual",
"timestamp": "2025-01-15T10:32:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
assigned_to | string | UUID del agente asignado |
assigned_by | string | UUID de quien asignó |
method | string | manual o auto |
broadcast.completed
Cuando un envío masivo termina.{
"event": "broadcast.completed",
"timestamp": "2025-01-15T11:00:00.000Z",
"businessId": "uuid",
"data": {
"broadcast_id": "bc-uuid",
"name": "Promoción Enero",
"total_recipients": 500,
"sent_count": 495,
"failed_count": 5,
"success_rate": 99,
"completed_at": "2025-01-15T11:00:00.000Z"
}
}
message.delivered
Cuando WhatsApp confirma que el mensaje fue entregado al dispositivo del usuario.{
"event": "message.delivered",
"timestamp": "2026-01-15T10:31:05.000Z",
"businessId": "uuid",
"data": {
"message_id": "msg-uuid",
"wamid": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"conversation_id": "conv-uuid",
"to": "+5215512345678",
"contact_name": "Juan Pérez",
"status": "delivered",
"timestamp": "2026-01-15T10:31:05.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
message_id | string | ID del mensaje en Whaapy |
wamid | string | ID del mensaje en WhatsApp |
status | string | Siempre delivered |
message.read
Cuando el usuario abre y lee el mensaje (doble check azul).{
"event": "message.read",
"timestamp": "2026-01-15T10:35:00.000Z",
"businessId": "uuid",
"data": {
"message_id": "msg-uuid",
"wamid": "wamid.HBgNNTIxNTUxMjM0NTY3OA==",
"conversation_id": "conv-uuid",
"to": "+5215512345678",
"contact_name": "Juan Pérez",
"status": "read",
"timestamp": "2026-01-15T10:35:00.000Z"
}
}
El evento
message.read solo se recibe si el usuario tiene habilitada la confirmación de lectura en WhatsApp.message.failed
Cuando un mensaje no se pudo enviar.{
"event": "message.failed",
"timestamp": "2026-01-15T10:31:00.000Z",
"businessId": "uuid",
"data": {
"message_id": "msg-uuid",
"conversation_id": "conv-uuid",
"to": "+5215512345678",
"contact_name": "Juan Pérez",
"status": "failed",
"error": {
"code": "131047",
"title": "Re-engagement message",
"message": "La ventana de conversación de 24 horas ha expirado",
"details": "More than 24 hours have passed since the customer last replied"
},
"timestamp": "2026-01-15T10:31:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
error.code | string | Código de error de WhatsApp |
error.title | string | Título corto del error |
error.message | string | Descripción del error en español |
error.details | string | Detalles técnicos (en inglés) |
Los códigos de error más comunes son
131047 (ventana cerrada), 131051 (número no válido), y 131026 (usuario bloqueó el número).conversation.unassigned
Cuando una conversación es desasignada de un agente.{
"event": "conversation.unassigned",
"timestamp": "2026-01-15T11:00:00.000Z",
"businessId": "uuid",
"data": {
"conversation_id": "conv-uuid",
"phone_number": "+5215512345678",
"contact_name": "Juan Pérez",
"previous_agent": "agent-uuid",
"unassigned_by": "admin-uuid",
"reason": "manual",
"timestamp": "2026-01-15T11:00:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
previous_agent | string | UUID del agente que tenía la conversación |
unassigned_by | string | UUID de quien desasignó (null si fue automático) |
reason | string | manual, timeout, o reassigned |
conversation.closed
Cuando una conversación es marcada como cerrada.{
"event": "conversation.closed",
"timestamp": "2026-01-15T12:00:00.000Z",
"businessId": "uuid",
"data": {
"conversation_id": "conv-uuid",
"phone_number": "+5215512345678",
"contact_name": "Juan Pérez",
"closed_by": "agent-uuid",
"reason": "resolved",
"duration_minutes": 45,
"message_count": 12,
"timestamp": "2026-01-15T12:00:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
closed_by | string | UUID de quien cerró (null si fue automático) |
reason | string | resolved, spam, timeout, manual |
duration_minutes | number | Duración total de la conversación |
message_count | number | Número total de mensajes |
contact.created
Cuando se crea un nuevo contacto (primera vez que alguien escribe).{
"event": "contact.created",
"timestamp": "2026-01-15T10:30:00.000Z",
"businessId": "uuid",
"data": {
"contact_id": "contact-uuid",
"phone_number": "+5215512345678",
"name": "Juan Pérez",
"wa_id": "5215512345678",
"source": "inbound_message",
"timestamp": "2026-01-15T10:30:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
source | string | inbound_message, import, manual |
contact.updated
Cuando se actualizan los datos de un contacto.{
"event": "contact.updated",
"timestamp": "2026-01-15T14:00:00.000Z",
"businessId": "uuid",
"data": {
"contact_id": "contact-uuid",
"phone_number": "+5215512345678",
"name": "Juan Carlos Pérez",
"previous_name": "Juan Pérez",
"updated_by": "agent-uuid",
"updated_fields": ["name", "email", "tags"],
"timestamp": "2026-01-15T14:00:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
updated_by | string | UUID de quien actualizó |
updated_fields | string[] | Lista de campos modificados |
previous_name | string | Nombre anterior (si cambió) |
contact.deleted
Cuando se elimina un contacto.{
"event": "contact.deleted",
"timestamp": "2026-01-15T15:00:00.000Z",
"businessId": "uuid",
"data": {
"contact_id": "contact-uuid",
"phone_number": "+5215512345678",
"name": "Juan Pérez",
"deleted_by": "admin-uuid",
"reason": "gdpr_request",
"timestamp": "2026-01-15T15:00:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
deleted_by | string | UUID de quien eliminó |
reason | string | gdpr_request, spam, manual, cleanup |
Al eliminar un contacto, también se eliminan todas sus conversaciones y mensajes. Esta acción es irreversible.
broadcast.sent
Cuando un broadcast comienza a enviarse.{
"event": "broadcast.sent",
"timestamp": "2026-01-15T10:00:00.000Z",
"businessId": "uuid",
"data": {
"broadcast_id": "bc-uuid",
"name": "Promoción Enero",
"template_name": "promo_enero_2026",
"total_recipients": 500,
"started_at": "2026-01-15T10:00:00.000Z",
"created_by": "admin-uuid"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
template_name | string | Nombre del template usado |
total_recipients | number | Número de destinatarios |
created_by | string | UUID de quien creó el broadcast |
broadcast.failed
Cuando un broadcast falla antes de completarse.{
"event": "broadcast.failed",
"timestamp": "2026-01-15T10:05:00.000Z",
"businessId": "uuid",
"data": {
"broadcast_id": "bc-uuid",
"name": "Promoción Enero",
"template_name": "promo_enero_2026",
"total_recipients": 500,
"sent_count": 150,
"failed_count": 350,
"error": {
"code": "rate_limit_exceeded",
"message": "Se excedió el límite de mensajes por minuto"
},
"failed_at": "2026-01-15T10:05:00.000Z"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
sent_count | number | Mensajes enviados antes del fallo |
failed_count | number | Mensajes que no se enviaron |
error.code | string | Código de error |
error.message | string | Descripción del error |
Si un broadcast falla parcialmente, los mensajes ya enviados no se revierten. Puedes crear un nuevo broadcast con los contactos pendientes.
Próximos Pasos
Seguridad
Verificar la autenticidad de los webhooks
Gestión
Actualizar, probar y eliminar webhooks