Eventos de WhatsApp
Bird registra eventos para mensajes WhatsApp entrantes y salientes. Una línea de tiempo saliente muestra lo que ocurrió después de que un envío devolvió 202: aceptación, traspaso a WhatsApp, entrega, lectura o fallo. Una línea de tiempo entrante registra cuándo Bird recibió el mensaje.
La envoltura de evento
Los eventos públicos de entrega saliente usan la envoltura de webhook estándar: un type, un timestamp y un objeto data específico del tipo.
Ejemplo de código
{
"data": {
"direction": "outbound",
"from": { "phone_number": "+13124495569" },
"metadata": { "session_id": "sess_4821" },
"tags": [{ "name": "flow", "value": "login-otp" }],
"to": { "phone_number": "+14155550100" },
"whatsapp_id": "wam_01ky7qbvswf3fvyaw3az90391c",
"workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
},
"timestamp": "2026-07-23T14:51:39.913Z",
"type": "whatsapp.delivered"
}Cada payload público de webhook WhatsApp de un mensaje contiene whatsapp_id, workspace_id, direction, from, to, tags y metadata. whatsapp.reacted es la excepción, porque una reacción es una anotación sobre un mensaje y no un mensaje en sí; Reacciones más abajo describe su forma. Una dirección puede incluir un phone_number E.164, un ID de usuario con alcance de negocio de Meta en bsuid, o ambos. Un mensaje recibido de un usuario de WhatsApp también incluye el perfil que publica, en username y display_name. tags y metadata son null cuando el envío no incluyó ninguno. Un mensaje enviado como respuesta también incluye in_reply_to_message_id en cada evento saliente de su línea de tiempo, desde whatsapp.accepted hasta whatsapp.read, whatsapp.failed o whatsapp.rejected, indicando el mensaje que responde.
El API de eventos devuelve registros de línea de tiempo más ligeros con un id, type y una marca de tiempo occurred_at. El ID del mensaje ya está en la URL de la solicitud.
Eventos de ciclo de vida
Los eventos aparecen en orden cronológico. Un mensaje saliente puede detenerse en whatsapp.failed o whatsapp.rejected, y whatsapp.read aparece solo si el destinatario abre el mensaje. Un mensaje entrante tiene un único evento de línea de tiempo whatsapp.received.
| Evento | Significado |
|---|---|
| whatsapp.accepted | Bird aceptó la solicitud de envío. Es lo que reportó el 202. |
| whatsapp.sent | Bird entregó el mensaje a la red de WhatsApp. |
| whatsapp.delivered | WhatsApp confirmó la entrega al dispositivo del destinatario. |
| whatsapp.read | El destinatario abrió el mensaje. |
| whatsapp.failed | El mensaje no fue entregado. error.code indica qué lo impidió. |
| whatsapp.rejected | Bird rechazó el mensaje antes de enviarlo. No se cobró. |
| whatsapp.received | Bird recibió un mensaje entrante de un contacto. |
whatsapp.delivered es también el momento en que se cobra la parte de Meta del precio del mensaje, aunque el evento no lo reporta: los payloads de eventos WhatsApp no incluyen costo. Vuelve a leer el mensaje con GET /v1/whatsapp/messages/{message_id} para ver cuánto costó. Consulta Costo y facturación.
whatsapp.read no cambia el status del mensaje. Un mensaje entregado permanece en delivered; el mensaje también registra la lectura en read_at.
whatsapp.delivered puede omitirse por completo. Cuando el destinatario ya tiene el chat abierto en su dispositivo, Meta reporta la lectura sin reportar nunca una entrega, de modo que la línea de tiempo muestra whatsapp.accepted → whatsapp.sent → whatsapp.read sin whatsapp.delivered entre ellos. Trata read como prueba de entrega: un consumidor que espera delivered antes de considerar el mensaje como llegado se quedará esperando exactamente a los destinatarios que lo vieron más rápido, y uno que calcule una tasa de entrega solo a partir de delivered la subestimará. El status del mensaje permanece en sent en este caso, ya que solo un acuse de entrega lo avanza.
La omisión también tiene una consecuencia de facturación: la parte de Meta del precio se cobra del acuse de delivered, así que un mensaje leído de esta forma no genera passthrough_amount. Consulta Costo y facturación.
La lista de tipos de evento es abierta: pueden añadirse nuevos tipos con el tiempo, así que trata un valor no reconocido como un evento futuro en lugar de un error.
Eventos de fallo
whatsapp.failed y whatsapp.rejected son terminales. Un rechazo significa que Bird detuvo el mensaje antes de enviarlo a WhatsApp, por lo que no se cobró. Las causas incluyen un destinatario suprimido o dado de baja, saldo insuficiente en la billetera o un destino sin precio configurado. Un fallo significa que el mensaje no fue entregado, y error.code indica quién lo decidió. La mayoría de los códigos transmiten el veredicto de WhatsApp, mapeado desde el código que reportó. internal_error es la excepción: significa que el mensaje nunca llegó a WhatsApp, ya sea porque el número de envío no tenía una credencial utilizable o porque Bird reintentó el envío hasta desistir. meta_error_code contiene el código de WhatsApp cuando está disponible, y un fallo internal_error no tiene ninguno por definición.
Ambos eventos contienen un objeto error con un code Bird estable, un description legible, un meta_error_code opcional y occurred_at. El objeto aparece en los registros de API y en los payloads de webhook solo para estos tipos de evento.
Eventos de reacción
Una reacción con emoji es una anotación sobre un mensaje y no un mensaje en sí, por lo que no aparece en ninguna línea de tiempo. Una reacción no genera un evento whatsapp.*, una entrante no dispara un webhook whatsapp.received, y ningún webhook incluye una reacción. En su lugar, cada cambio se registra en el propio registro de reacciones del mensaje reaccionado: cada emoji colocado, cada uno reemplazado por otro emoji distinto y cada uno retirado, tanto por el contacto como por tu número de negocio.
Hay un caso que no se registra en ningún lugar. Bird asocia una reacción con su mensaje a través de un ID de proveedor que conserva durante 15 días, mientras que WhatsApp acepta una reacción sobre un mensaje de hasta 30 días de antigüedad, así que una reacción sobre un mensaje más antiguo no puede asociarse y no llega ni al registro ni a reactions. Un mensaje sin entradas, por lo tanto, no es prueba de que nadie haya reaccionado.
Lee ese registro con GET /v1/whatsapp/messages/{message_id}/reaction-events, del más reciente al más antiguo. Una entrada indica el emoji, quién hizo el cambio y un status de received, sent, failed o rejected; una entrada failed o rejected incluye la razón en error. Una reacción nunca se cobra, así que ningún fallo en ellas es de facturación. Para ver lo que permanece actualmente en el mensaje en lugar del historial de cambios, lee su reactions con GET /v1/whatsapp/messages/{message_id}, que reduce el registro a una entrada por remitente.
Eventos de supresión
Más allá del ciclo de vida por mensaje, un evento reporta un cambio en la lista de supresión del espacio de trabajo: whatsapp_suppression.created se dispara cuando se abre una supresión. El payload incluye el suppression_id, el address suprimido en formato E.164, el waba al que se limita el bloqueo (null cuando cubre todo el espacio de trabajo, sin importar qué cuenta envíe), el reason y el workspace_id, para que tu propio sistema pueda ver nuevos bloqueos sin hacer polling. Solo las aperturas disparan un evento: finalizar una supresión aún no lo hace, así que vuelve a leer la lista antes de dar por vigente un bloqueo replicado:
Ejemplo de código
{
"type": "whatsapp_suppression.created",
"timestamp": "2026-08-25T14:52:03.192524705Z",
"data": {
"suppression_id": "was_01krdgeqcxet5s7t44vh8rt9mg",
"address": "+14155550100",
"waba": null,
"reason": "manual",
"workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf"
}
}Una baja que el propio destinatario declaró es una preferencia y no una supresión, y dispara preference.revoked en su lugar.
Lectura de eventos desde el API
GET /v1/whatsapp/messages/{message_id}/events devuelve la línea de tiempo en orden cronológico. La lista acotada no está paginada. Leer eventos requiere una clave API con whatsapp:read:
const { data } = await bird.whatsapp.listEvents("wa_abc123");
for (const event of data) console.log(event.type, event.occurred_at);events = client.whatsapp.list_events("wa_abc123")
for event in events.data:
print(event.type, event.occurred_at)events, err := client.Whatsapp.ListEvents(context.Background(), "wam_01krdgeqcxet5s7t44vh8rt9mg", bird.WhatsappListEventsParams{})
if err != nil {
log.Fatal(err)
}
for _, e := range events.Data {
fmt.Println(e.Id, e.Type)
}$events = $bird->whatsapp->listEvents('wamid_01krdgeqcxet5s7t44vh8rt9mg');
foreach ($events->getData() ?? [] as $event) {
echo $event->getType(), ' ', $event->getId(), "\n";
}bird whatsapp list-events <message-id>{
"name": "whatsapp_list_events",
"arguments": {
"message_id": "<message-id>"
}
}curl https://us1.platform.bird.com/v1/whatsapp/messages/wam_.../events \
-H "Authorization: Bearer $BIRD_API_KEY"Un mensaje que fue aceptado, enviado, entregado y leído devuelve cuatro eventos:
Ejemplo de código
{
"data": [
{
"id": "ev_01ky7q6a1fejfbvs0myn41hj41",
"occurred_at": "2026-07-23T14:48:34.71Z",
"type": "whatsapp.accepted"
},
{
"id": "ev_01ky7q6a2denvtd6jg1vqwmg13",
"occurred_at": "2026-07-23T14:48:35.671Z",
"type": "whatsapp.sent"
},
{
"id": "ev_01ky7q6a2zff9r2qm74mmg1g6z",
"occurred_at": "2026-07-23T14:48:36.642Z",
"type": "whatsapp.delivered"
},
{
"id": "ev_01ky7q6c21frssf0vj8h50qysw",
"occurred_at": "2026-07-23T14:48:38.65Z",
"type": "whatsapp.read"
}
]
}Pasa type para obtener un tipo exacto de evento público, como ?type=whatsapp.failed o ?type=whatsapp.read. Omítelo para la línea de tiempo completa.
Esta misma línea de tiempo es lo que muestra la página del registro de WhatsApp cuando abres un mensaje.

Webhooks
Suscríbete a whatsapp.accepted, whatsapp.sent, whatsapp.delivered, whatsapp.read, whatsapp.failed, whatsapp.rejected, whatsapp.received y whatsapp.reacted desde la página de Webhooks o el API de webhooks. La guía de Webhooks cubre endpoints, firmas y reintentos.
whatsapp.received incluye el contenido del mensaje sobre la envoltura anterior, para que un endpoint pueda actuar sobre un mensaje entrante sin volver a leerlo. Un toque en un mensaje interactivo llega como interactive_reply, y in_reply_to_message_id indica el mensaje que responde:
Ejemplo de código
{
"data": {
"direction": "inbound",
"from": {
"display_name": "Alex Rivera",
"phone_number": "+14155550100",
"username": "alexr"
},
"in_reply_to_message_id": "wam_01ky7qbvswf3fvyaw3az90391c",
"interactive_reply": {
"list": {
"description": "Next day to 2 days",
"slug": "priority_express",
"text": "Priority Mail Express"
},
"type": "list"
},
"metadata": null,
"tags": null,
"to": { "phone_number": "+13124495569" },
"whatsapp_id": "wam_01ky8b3xq4gd7pmzn2ka51f7te",
"workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
},
"timestamp": "2026-07-23T14:52:04.118Z",
"type": "whatsapp.received"
}Las otras ramas de contenido siguen la misma forma de uno-de-estos: text, image, video, audio, sticker, document, location, contact_cards y unsupported para un tipo que el API no modela. GET /v1/whatsapp/messages/{message_id} documenta cada uno.
Reacciones
whatsapp.reacted se dispara cuando un usuario de WhatsApp reacciona a uno de tus mensajes. Es el único evento WhatsApp que no forma parte de la línea de tiempo de entrega de un mensaje: no aparece en GET /v1/whatsapp/messages/{message_id}/events, y no hay nada por lo que filtrarlo allí.
whatsapp_id nombra el mensaje que recibió la reacción, no la reacción en sí, y emoji es el cambio que hizo el usuario. Un usuario que reacciona, cambia su emoji y luego retira la reacción produce tres eventos sobre ese mismo mensaje. WhatsApp no envía una eliminación entre los dos primeros, así que un cambio llega como un único evento con el nuevo emoji.
Ejemplo de código
{
"data": {
"emoji": "👍",
"from": { "phone_number": "+14155550100" },
"to": { "phone_number": "+13124495569" },
"whatsapp_id": "wam_01ky8b3xq4gd7pmzn2ka51f7te",
"workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
},
"timestamp": "2026-07-23T14:52:11.000Z",
"type": "whatsapp.reacted"
}WhatsApp reporta la hora de la reacción con precisión de segundo, por lo que dos de esos tres eventos pueden compartir un mismo timestamp. Ordenarlos por ese valor no los secuenciará, ni tampoco el orden de entrega, que los reintentos hacen poco fiable. Actúa sobre la reacción que lleva cada evento, como el cambio que describe. No reconstruyas la secuencia a partir de los eventos ni trates el último en llegar como la reacción vigente del mensaje, porque ni las marcas de tiempo ni el orden de llegada lo respaldan. Vuelve a leer el mensaje para ver las reacciones vigentes: GET /v1/whatsapp/messages/{message_id} devuelve una entrada por remitente en reactions, y el registro de reacciones del mensaje contiene cada cambio.
emoji está presente y es null cuando el usuario retiró su reacción, así que un null es la eliminación en sí y no un valor ausente. El emoji se entrega exactamente como lo envió WhatsApp y no se normaliza, por lo que ❤ y ❤️ te llegan como cadenas distintas.
Próximos pasos
- Registro de WhatsApp: la vista por mensaje que muestra esta línea de tiempo
- Enviar mensajes WhatsApp: donde comienza el ciclo de vida de un mensaje
- Guía de Webhooks: endpoints, firmas, reintentos y el catálogo completo de eventos
Recursos relacionados
Continúa con la documentación, guías y ejemplos sobre este tema. Los recursos están en inglés.
Ver la guíaConnecting WhatsApp to Bird: from buying a number to a live channelComprender el conceptoWhat is the 24-hour customer service window on WhatsApp?Usar la herramientaWhatsApp message builderExplorar la funcionalidadWhatsApp
Prueba el ejercicio y obtén un resumen de implementación