Ir al contenido

Catálogo de eventos

Todos los eventos llegan en el mismo sobre, por cualquier canal: GET /v1/instances/{id}/events, un webhook, un stream SSE o WebSocket, o un broker. Lo que cambia según el tipo es data. Esta página documenta el sobre y todos los tipos que el motor emite, agrupados por dominio; al final hay una tabla con todos.

{
"schema_version": 1,
"event_id": "e1f0c2d4-0000-4000-8000-000000001187",
"instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1",
"seq": 1187,
"type": "message.received",
"occurred_at": "2026-10-24T15:08:12Z",
"received_at": "2026-10-24T15:08:13Z",
"generation": 1,
"origin": "live",
"data": { }
}
Campo Qué es
schema_version La versión del contrato del evento.
event_id Identificador único del evento. Deduplica por este campo.
instance_id La instancia a la que le pasó. La app dueña no viaja: la dice tu credencial.
seq Posición en el ledger de la instancia: crece de uno en uno, sin huecos.
type El tipo de evento.
occurred_at Cuándo pasó.
received_at Cuándo lo recibió el motor.
generation La generación de la instancia: cambia cada vez que se vuelve a vincular.
origin live (en vivo), history (importado del historial) u operator (un comando del motor).
command_id Solo si el evento es efecto de un comando: el comando que lo causó.
correlation_id Solo si el comando lo traía: tu identificador, de vuelta.
data El payload del tipo. Puede venir redactado: mira Contenido redactado.

Los chats, los autores, los grupos y los canales viajan como un peer. peer_ref es estable dentro de la instancia aunque WhatsApp cambie el identificador (de número a LID, por ejemplo): úsalo como clave y para responder (destino.peer_ref).

{ "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": ["CONTACTO@lid"] }

clase es usuario, grupo, difusion, canal u otro. En los ejemplos siguientes, los peers se abrevian como { "peer_ref": "…", … }.

Un campo que la página marca como opcional solo aparece cuando tiene valor: no lo esperes siempre ni lo trates como error si falta. Los ejemplos muestran el caso más común. Los eventos de la instancia (los de Instancia, history.sync y provider.unknown) llevan además generacion dentro de data.

Los eventos no se borran del ledger, pero su contenido puede dejar de mostrarse. El evento y su seq siguen ahí; cambia data:

Caso data
Venció la retención (un estado pasado su retener_hasta). {"motivo": "retencion", "redactado": true}
Quien publicó un estado lo borró. {"motivo": "revocado", "redactado": true}
Venció un mensaje temporal. El mismo data sin contenido, con "redactado": true y "motivo": "vencido".

Los eventos del ciclo de vida de una instancia: vinculación, conexión y caídas. Todos traen generacion en data.

La instancia cambió de estado.

{ "de": "emparejando", "a": "conectado", "motivo": "", "generacion": 1 }

Los estados son nuevo, esperando_qr, esperando_codigo, accion_requerida, emparejando, emparejado, conectado, desconectado, reconectando, intento_vencido, error, cerrado_por_telefono, cerrado_por_operador, reemplazado, bloqueado y archivado. Al crear la instancia llega con "de": null, "a": "nuevo" y "motivo": "creada". motivo es un código (por ejemplo socket, keepalive, operador, pausada, logged_out, baneo_temporal) o vacío.

Hay un reto de vinculación vigente. El QR o el código no viajan en el evento: consúltalo con GET /v1/instances/{id}/pairing.

{ "modo": "qr", "reto": "qr", "expira_en": "2026-10-24T15:03:05Z", "generacion": 1 }

modo es qr o codigo; reto, qr, codigo, passkey o passkey_confirmacion. Llega una vez por cada tipo de reto del intento: un QR renovado no deja otro evento. Consulta el reto vigente cada vez que lo vayas a mostrar.

El intento de vinculación venció sin que nadie lo completara.

{ "motivo": "qr_vencido", "generacion": 2 }

motivo es qr_vencido o codigo_vencido, según el modo del intento.

El intento de vinculación terminó con un error.

{ "motivo": "telefono_rechazado", "generacion": 2 }

motivo es telefono_rechazado, cliente_desactualizado, sin_multidispositivo, estado_inesperado, error_emparejamiento, passkey, vinculo_rechazado o conexion. También puede ser un código de autorización (credencial_revocada, credencial_vencida, app_suspendida, sin_scope…) si la credencial que pidió el intento dejó de valer.

El teléfono aceptó el vínculo. Todavía no es «conectado»: eso llega con connected.

{ "plataforma": "android", "numero": "+TU_NUMERO_CON_CODIGO_DE_PAIS", "generacion": 2 }

plataforma es la que informa WhatsApp. Si el motor se reinició justo después del vínculo, lo confirma al arrancar con "conciliado": true y sin plataforma.

La sesión se conectó a WhatsApp.

{ "generacion": 2 }

La sesión perdió la conexión o el motor la cortó.

{ "motivo": "socket", "generacion": 2 }

motivo es socket (se cortó la conexión), keepalive (el motor la reinició porque dejó de responder), operador o pausada (un comando), apagado (el motor se apagó), traspaso (la sesión pasó a otro proceso del motor), captura_fallida (el motor no pudo guardar un evento y cerró para no confirmarlo) o sin_sesion. Con operador y pausada, el sobre trae origin: operator.

La sesión quedó desvinculada. Hace falta volver a vincular.

{ "origen": "telefono", "al_conectar": false, "codigo": 401, "generacion": 2 }

origen es telefono (se cerró desde el teléfono; trae al_conectar y el codigo de WhatsApp) u operador (un comando de desvincular; trae remoto, si se avisó a WhatsApp, y el sobre origin: operator).

Otra conexión abrió la misma sesión. El motor se detiene y no reconecta solo, para no pelear el socket.

{ "generacion": 2 }

WhatsApp bloqueó el número por un tiempo.

{ "codigo": 101, "expira_en": "2026-10-25T15:00:00Z", "generacion": 2 }

codigo es el que informa WhatsApp. expira_en es opcional: solo si WhatsApp dio un plazo.

Un error de la sesión.

{ "motivo": "fallo_conexion", "permanente": false, "codigo": 503, "generacion": 2 }
motivo Qué pasó Campos extra
sin_conexion Se agotaron los reintentos del SDK; el motor sigue reintentando con su espera. intentos
fallo_conexion WhatsApp rechazó la conexión con un error pasajero. codigo
rechazo_conexion WhatsApp rechazó la conexión y no se reintenta solo. codigo
cliente_desactualizado WhatsApp pide una versión más nueva del SDK. —
stream_error Un error desconocido del canal de WhatsApp. codigo
dispositivo_ausente El dispositivo vinculado ya no está en el store. —

permanente: true significa que el motor no reintenta hasta que alguien lo pida.

Cambió la configuración de la instancia (PATCH /v1/instances/{id}). Solo viajan los nombres de los campos, no sus valores.

{ "campos": ["nombre", "config.rechazar_llamadas"], "generacion": 3 }

Cuentan qué pasó con un comando. El sobre trae command_id y, si lo mandaste, correlation_id; origin es operator. En data, type es el tipo del comando (por ejemplo message.text o group.name).

El motor guardó el comando, en la misma transacción.

{ "command_id": "5c2e7a10-0000-4000-8000-0000000000c1", "type": "message.text" }

Llega con los envíos y con las operaciones remotas (grupos, canales, chats y etiquetas, perfil y privacidad, llamadas e historial). Los comandos de control de la instancia (reconnect, disconnect, logout…) no lo dejan: su resultado está en GET /v1/commands/{id} y en los eventos de Instancia. El texto de respaldo de una lista llega con respaldo_de: el command_id de la lista.

Un comando que no tuvo efecto.

{
"command_id": "5c2e7a10-0000-4000-8000-0000000000c2",
"type": "message.text",
"error_code": "destino_invalido",
"stage": "previa",
"provider_message_id": "PROVIDER_MESSAGE_ID"
}

En un envío, stage dice dónde falló: previa (no salió), servidor (WhatsApp lo rechazó) o frontera, y trae el provider_message_id. Algunos error_code de un envío: destino_invalido, rechazado_por_whatsapp, no_salio, generacion_cambiada, medio_no_disponible, medio_incompatible, conversion_fallida y comando_vencido.

En una operación remota (un grupo, un canal, el perfil…) solo viene error_code (por ejemplo sin_permiso, grupo_no_encontrado, rechazado_por_whatsapp, operacion_fallida) y, si WhatsApp respondió con un código, server_code y server_origin. Si la credencial que aceptó el comando dejó de valer antes de ejecutarlo, error_code es credencial_revocada, credencial_vencida, app_suspendida, sin_scope o sin_acceso_a_la_instancia.

No se sabe si el comando salió: se cortó justo en la frontera. El motor nunca lo reenvía solo. Tiene la misma forma que command.failed.

{
"command_id": "5c2e7a10-0000-4000-8000-0000000000c3",
"type": "message.text",
"error_code": "reinicio_en_la_frontera",
"stage": "frontera",
"provider_message_id": "PROVIDER_MESSAGE_ID"
}

En una operación remota (crear un grupo, por ejemplo) llega solo con "error_code": "resultado_incierto". Si el mensaje sí salió, su eco o su recibo lo concilian y llega un message.sent con conciliado: true.

Cancelaste el comando (POST /v1/commands/{id}/cancel) antes de que saliera.

{ "command_id": "5c2e7a10-0000-4000-8000-0000000000c4", "type": "message.text" }

Un mensaje entrante o un eco de uno propio enviado desde el teléfono (from_me: true). operacion es mensaje; contenido.tipo es texto, imagen, video, audio, documento, sticker, ubicacion, ubicacion_en_vivo, contactos, encuesta o no_soportado.

{
"chat": { "peer_ref": "…", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": [] },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"es_grupo": false,
"operacion": "mensaje",
"contenido": { "tipo": "texto", "texto": "Hola, ¿tienen envío a domicilio?" },
"nombre_perfil": "Nombre del contacto",
"momento": "2026-10-24T15:08:12Z"
}

Campos opcionales:

Campo Cuándo aparece
autor En un grupo: quien escribió (es_grupo: true).
server_id En la publicación de un canal: el ID de servidor, distinto de provider_id.
nombre_perfil Si WhatsApp mandó el nombre del remitente.
contenido.cita Si cita un mensaje: provider_id, de_mi y, a veces, autor. citado_vencido: true si el citado era temporal y venció.
contenido.menciones, contenido.reenviado Si menciona a alguien o es un reenvío.
contenido.medio En un medio: media_id, estado, mime, bytes y, según el tipo, nombre, ancho, alto, duracion_seg, ptt, ptv, gif, animado, sha256.
contenido.ubicacion, contenido.contactos, contenido.vista_previa, contenido.encuesta Según el tipo de contenido.
contenido.expiracion_seg Si el mensaje es temporal: vence a los N segundos de su momento.
efimero, ver_una_vez, pedido_al_telefono, estado_de_grupo Banderas del mensaje: temporal, de ver una vez, recuperado del teléfono o un estado publicado en un grupo.

El archivo de un medio se descarga aparte, cuando llega media.ready. medio.estado es pendiente hasta entonces; un mensaje del historial importado sin multimedia trae no_descargado y no tiene media_id. Lo importado del historial llega con origin: history. Un estado recibido no llega aquí: llega como status.received, y una respuesta a un interactivo, como interactive.response.

Un cambio sobre un mensaje que ya existe. operacion dice cuál: estado (un recibo), edicion o ubicacion_actualizada. No existe un message.edited aparte.

{
"chat": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": true,
"es_grupo": false,
"operacion": "estado",
"estado": "leido",
"anterior": "entregado",
"momento": "2026-10-24T15:11:40Z"
}

Con operacion: "estado", estado es entregado, leido o reproducido, y solo sube: un recibo más bajo que el actual no deja evento. En un mensaje propio (from_me: true) es el avance del otro lado; en uno entrante (from_me: false), que lo leíste o reprodujiste desde otro dispositivo del número.

Una edición trae operacion: "edicion", el objetivo (el mensaje editado), el contenido nuevo y version: la última versión es la vigente. Una actualización de ubicación en vivo trae operacion: "ubicacion_actualizada", el objetivo y la ubicación nueva en contenido.

Alguien borró un mensaje para todos.

{
"chat": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID_DEL_BORRADO",
"from_me": false,
"es_grupo": false,
"operacion": "revocacion",
"objetivo": { "provider_id": "PROVIDER_MESSAGE_ID", "de_mi": false },
"momento": "2026-10-24T15:13:20Z",
"version": 1
}

objetivo es el mensaje borrado. En un grupo llega además autor (quien lo borró).

Una reacción a un mensaje. reaccion es el emoji; una reacción quitada llega vacía.

{
"chat": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_REACCION",
"from_me": false,
"es_grupo": false,
"operacion": "reaccion",
"objetivo": { "provider_id": "PROVIDER_MESSAGE_ID", "de_mi": true },
"reaccion": "👍",
"momento": "2026-10-24T15:12:03Z",
"version": 1
}

El servidor de WhatsApp aceptó un envío del motor. No es entregado ni leído: eso llega como message.updated. El sobre trae command_id.

{
"chat": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID",
"command_id": "5c2e7a10-0000-4000-8000-0000000000c1",
"aceptado_en": "2026-10-24T15:10:01Z"
}

Campos opcionales: operacion y objetivo en una edición, un borrado o una reacción propios; server_id en una publicación en un canal; conciliado: true si el comando había quedado ambiguo y su eco o su recibo lo confirmó. Un estado que publica el motor también termina aquí, con el chat status@broadcast.

Llegó un mensaje que no se pudo descifrar. Nunca trae texto. Si después llega legible (el remitente lo reenvía, o el teléfono), es otro hecho: un message.received con el mismo provider_id.

{
"chat": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"es_grupo": false,
"no_disponible": true,
"tipo_no_disponible": "TIPO",
"momento": "2026-10-24T15:14:00Z"
}

no_disponible: true significa que el servidor lo marcó así y el motor le pidió el mensaje al teléfono. tipo_no_disponible y modo_fallo son códigos acotados del proveedor y son opcionales. En un grupo llega además autor.

Venció un mensaje temporal. WhatsApp no avisa el vencimiento: el motor lo calcula (momento + contenido.expiracion_seg), borra el contenido y los medios, y emite este evento una vez.

{
"chat": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"message_ref": "6f0a1b2c-0000-4000-8000-0000000000d1",
"momento": "2026-10-24T15:08:12Z",
"vence_en": "2026-10-25T15:08:12Z"
}

message_ref es la referencia del mensaje en GET /v1/instances/{id}/messages/{message_ref}. En un grupo llega además autor. Desde ese momento, el message.received de ese mensaje (y sus ediciones) sale sin contenido, con redactado: true y motivo: "vencido".

El recibo tal cual llegó de WhatsApp, para uno o varios mensajes. El cambio de estado de cada mensaje llega aparte, como message.updated; usa receipt si necesitas el detalle por destinatario.

{
"chat": { "peer_ref": "…", … },
"from_me": false,
"es_grupo": false,
"provider_ids": ["PROVIDER_MESSAGE_ID"],
"tipo": "leido",
"momento": "2026-10-24T15:11:40Z",
"aplicados": 1,
"pendientes": 0
}

tipo es entregado, leido, reproducido, leido_propio, reproducido_propio, remitente, reintento u otro (con el tipo crudo en tipo_proveedor). aplicados cuenta los mensajes conocidos; pendientes, los que todavía no llegaron (se aplican cuando lleguen). En un grupo llega además remitente: quien lo leyó.

Un voto de una encuesta. Reemplaza el voto anterior del mismo votante y trae el resultado agregado.

{
"chat": { "peer_ref": "…", … },
"votante": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID_DEL_VOTO",
"encuesta": "PROVIDER_MESSAGE_ID_DE_LA_ENCUESTA",
"from_me": false,
"descifrado": true,
"opciones": ["Mañana"],
"vigente": true,
"momento": "2026-10-24T15:20:00Z",
"resultado": {
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_ENCUESTA",
"conocida": true,
"pregunta": "¿Cuándo pasas a retirar?",
"seleccionables": 1,
"opciones": [{ "opcion": "Mañana", "votos": 1 }, { "opcion": "Tarde", "votos": 0 }],
"votantes": 1,
"sin_descifrar": 0
}
}

opciones vacío: quitó su voto. vigente: false: llegó un voto más viejo que el que ya estaba. descifrado: false: el motor no tiene el secreto de la encuesta; el voto cuenta en sin_descifrar y nunca se inventa una opción. conocida: false: el motor no vio crear la encuesta.

La presencia de un contacto al que te suscribiste (POST /v1/instances/{id}/presence-subscriptions). La de los demás no se guarda.

{
"de": { "peer_ref": "…", … },
"estado_chat": "escribiendo",
"momento": "2026-10-24T15:21:00Z",
"vence_en": "2026-10-24T15:31:00Z"
}

Campos opcionales: chat (si es lo que hace en un chat), estado_chat (escribiendo, grabando o pausado), disponible y ultima_vez. vence_en es hasta cuándo sigue la suscripción.

Alguien respondió a un interactivo (botones, lista, carrusel o native flow). Es un hecho propio: no llega como message.received. Si se correlacionó con un interactivo que mandó esta instancia, el sobre trae su command_id.

{
"chat": { "peer_ref": "…", … },
"autor": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_RESPUESTA",
"from_me": false,
"formato": "native_flow",
"mensaje": { "provider_id": "PROVIDER_MESSAGE_ID", "command_id": "5c2e7a10-0000-4000-8000-0000000000c5" },
"accion": { "id": "envio_domicilio", "titulo": "Envío a domicilio", "accion": "list_row", "seccion": "Entrega" },
"correlacion": "correlacionada",
"momento": "2026-10-24T15:22:00Z"
}

formato es buttons_response, list_response, template_button_reply, native_flow o texto (un número contestado al respaldo de texto de una lista, con via: "texto"). correlacion es correlacionada, accion_desconocida, sin_mensaje, otro_chat, otro_autor o no_respondible. Opcionales: descripcion, tarjeta e indice en accion, y nombre_flujo y parametros en native flow.

Llegó una respuesta a un interactivo que el motor no pudo leer. Tiene la forma de interactive.response, con correlacion: "desconocida", el motivo y lo que llegó, saneado, en crudo.

{
"chat": { "peer_ref": "…", … },
"autor": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_RESPUESTA",
"from_me": false,
"formato": "desconocida",
"accion": {},
"correlacion": "desconocida",
"motivo": "formato_desconocido",
"crudo": { "…": "…" },
"momento": "2026-10-24T15:22:30Z"
}

motivo es formato_desconocido, flujo_desconocido, sin_id, id_invalido, contexto_invalido o parametros_invalidos.

Cambió la correlación de una respuesta ya publicada (de otro_chat u otro_autor a otra), porque el motor comprobó después que dos identificadores eran el mismo contacto. No es otra respuesta: trae la respuesta entera con sus mismas identidades.

{
"response_id": "7a2b3c4d-0000-4000-8000-0000000000e1",
"response_event_id": "e1f0c2d4-0000-4000-8000-000000001300",
"correlacion_anterior": "otro_chat",
"chat": { "peer_ref": "…", … },
"autor": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_RESPUESTA",
"from_me": false,
"formato": "buttons_response",
"mensaje": { "provider_id": "PROVIDER_MESSAGE_ID", "command_id": "5c2e7a10-0000-4000-8000-0000000000c5" },
"accion": { "id": "si", "titulo": "Sí", "accion": "reply" },
"correlacion": "correlacionada",
"momento": "2026-10-24T15:22:00Z"
}

response_event_id es el event_id del interactive.response original: úsalo para actualizar lo que ya guardaste.

Un medio quedó listo en el almacenamiento privado del motor: el de un mensaje recibido, uno importado de una URL o uno convertido.

{
"media_id": "9c8d7e6f-0000-4000-8000-0000000000f1",
"origen": "entrante",
"estado": "listo",
"mime": "image/jpeg",
"bytes": 183245,
"sha256": "SHA256_DEL_ARCHIVO",
"provider_id": "PROVIDER_MESSAGE_ID"
}

origen es entrante, url o conversion. Un medio entrante trae provider_id (el mensaje que lo trajo); uno que usa un envío trae command_id, también en el sobre. Descárgalo con GET /v1/media/{id}/content.

El medio no se pudo bajar, importar o convertir.

{
"media_id": "9c8d7e6f-0000-4000-8000-0000000000f2",
"origen": "entrante",
"estado": "fallido",
"motivo": "vencido_en_whatsapp",
"provider_id": "PROVIDER_MESSAGE_ID"
}

motivo es, por ejemplo, vencido_en_whatsapp, invalido, demasiado_grande, cuota_excedida, reintentos_agotados, url_no_permitida, url_rechazada, multimedia_deshabilitada o, en una conversión, formato_no_convertible, tiempo_excedido, memoria_excedida, conversion_no_disponible o medio_no_disponible.

El motor no atiende ni hace llamadas: solo las registra y las puede rechazar. Todas las señales de WhatsApp comparten la forma de call.offer: call_id, de, video, momento y, en una llamada de grupo, grupo.

Una llamada entrante. video dice si es videollamada. Puedes rechazarla con POST /v1/instances/{id}/calls/{call_id}/reject.

{
"call_id": "CALL_ID",
"de": { "peer_ref": "…", … },
"video": false,
"momento": "2026-10-24T15:20:00Z",
"elementos": ["audio", "net"]
}

elementos es diagnóstico: los nombres de los elementos de la oferta, sin sus datos.

El aviso de una llamada. Según el SDK, WhatsApp lo usa sobre todo para las llamadas de grupo (trae grupo).

{ "call_id": "CALL_ID", "de": { "peer_ref": "…", … }, "grupo": { "peer_ref": "…", … }, "video": false, "momento": "2026-10-24T15:20:00Z" }

Una señal intermedia: la llamada fue preaceptada.

{ "call_id": "CALL_ID", "de": { "peer_ref": "…", … }, "video": false, "momento": "2026-10-24T15:20:02Z" }

La llamada se atendió (en otro dispositivo del número).

{ "call_id": "CALL_ID", "de": { "peer_ref": "…", … }, "video": false, "momento": "2026-10-24T15:20:05Z" }

La llamada terminó. motivo es el código que informa WhatsApp.

{ "call_id": "CALL_ID", "de": { "peer_ref": "…", … }, "video": false, "motivo": "MOTIVO_DE_WHATSAPP", "momento": "2026-10-24T15:21:30Z" }

WhatsApp informa que la llamada se rechazó fuera del motor.

{ "call_id": "CALL_ID", "de": { "peer_ref": "…", … }, "video": false, "momento": "2026-10-24T15:20:04Z" }

Cualquier otra señal de la llamada. Sirve para diagnóstico.

{ "call_id": "CALL_ID", "de": { "peer_ref": "…", … }, "video": false, "momento": "2026-10-24T15:20:01Z" }

El motor rechazó la llamada. rechazo dice quién lo pidió: automatico (la instancia tiene rechazar_llamadas), operador o app. El sobre trae el command_id del rechazo.

{ "call_id": "CALL_ID", "de": { "peer_ref": "…", … }, "video": false, "rechazo": "app", "momento": "2026-10-24T15:20:03Z" }

En estos eventos, desde es whatsapp (lo hizo alguien desde un teléfono), motor (un comando del motor, con su command_id en el sobre) o consulta (lo trajo una consulta al servidor).

Cambiaron datos del grupo. campos trae solo lo que cambió, con su valor nuevo.

{
"grupo": { "peer_ref": "…", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] },
"campos": { "nombre": "Pedidos de la semana" },
"desde": "whatsapp",
"autor": { "peer_ref": "…", … }
}

Las claves posibles de campos son nombre, descripcion, anuncio, bloqueado, aprobacion, suspendido, modo_agregar, efimero_seg, foto_id, invitacion (solo true: el enlace no viaja), eliminado, miembro (false: el número salió o lo sacaron), rol_propio, comunidad y es_comunidad. autor es opcional: solo si WhatsApp dice quién fue.

El número entró a un grupo, o lo creó.

{
"grupo": { "peer_ref": "…", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] },
"nombre": "Pedidos de la semana",
"motivo": "",
"tipo": "new",
"participantes": 3,
"es_comunidad": false,
"desde": "motor"
}

tipo y motivo son los que informa WhatsApp (tipo: "new": el número lo creó). participantes es la cantidad. autor es opcional. Un aviso repetido de un grupo del que el número ya es miembro no deja otro evento.

Entraron, salieron o cambiaron de rol participantes de un grupo. Las listas sin movimiento llegan vacías o en null; motivo es el que informa WhatsApp y puede venir vacío.

{
"grupo": { "peer_ref": "…", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] },
"desde": "whatsapp",
"autor": { "peer_ref": "…", … },
"entran": [{ "peer_ref": "…", … }],
"salen": null,
"promovidos": null,
"degradados": null,
"motivo": ""
}

autor y motivo solo aparecen en los avisos de WhatsApp; una consulta (desde: consulta) trae solo las listas.

Un grupo se vinculó a una comunidad o se desvinculó.

{
"comunidad": { "peer_ref": "…", … },
"grupo": { "peer_ref": "…", … },
"nombre": "Sucursal centro",
"vinculado": true,
"anuncios": false,
"version": 2,
"desde": "motor"
}

anuncios: true es el grupo de anuncios de la comunidad. version sube con cada vínculo o desvínculo de ese par. Si un grupo se muda de comunidad, la vieja recibe un evento con vinculado: false y motivo: "otra_comunidad" (sin nombre ni anuncios).

desde es whatsapp, motor o consulta, como en los grupos.

El número empezó a seguir un canal, o lo creó.

{
"canal": { "peer_ref": "…", "jid": "CANAL@newsletter", "clase": "canal", "aliases": [] },
"nombre": "Novedades",
"rol": "subscriber",
"desde": "motor"
}

rol es owner, admin, subscriber o guest.

El número dejó de seguir un canal.

{ "canal": { "peer_ref": "…", … }, "rol": "guest", "desde": "whatsapp" }

rol es el que informa WhatsApp; vacío si lo dejó un comando del motor.

Cambió el silencio de un canal.

{ "canal": { "peer_ref": "…", … }, "silenciado": true, "desde": "motor" }

Cambiaron datos de un canal. campos trae solo lo que cambió.

{ "canal": { "peer_ref": "…", … }, "campos": { "descripcion": "Ofertas de la semana" }, "desde": "consulta" }

Las claves posibles son nombre, descripcion, rol, siguiendo y silenciado.

Cambiaron las vistas o las reacciones de publicaciones de un canal. Trae solo las publicaciones que cambiaron.

{
"canal": { "peer_ref": "…", … },
"publicaciones": [
{ "server_id": 118, "provider_id": "PROVIDER_MESSAGE_ID", "vistas": 42, "reacciones": { "👍": 5, "❤️": 2 } }
],
"desde": "consulta"
}

desde es whatsapp (en vivo) o consulta. provider_id es opcional.

Llegó un estado (una historia) de un contacto, o uno que el número publicó desde el teléfono (from_me: true, sin autor). Un estado que publica el motor no llega aquí: su resultado es message.sent.

{
"status_id": "3e4f5a6b-0000-4000-8000-0000000000a2",
"autor": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"tipo": "texto",
"texto": "Abrimos el domingo",
"vence_en": "2026-10-25T15:30:00Z",
"retener_hasta": "2026-10-25T15:30:00Z"
}

tipo es el tipo de contenido. texto (el texto o el caption) y mime son opcionales: el archivo de un estado no se descarga. Pasado retener_hasta, el evento queda redactado (motivo: "retencion").

Quien publicó un estado lo borró. El status.received de ese estado queda redactado (motivo: "revocado").

{ "status_id": "3e4f5a6b-0000-4000-8000-0000000000a2", "autor": { "peer_ref": "…", … }, "provider_id": "PROVIDER_MESSAGE_ID", "from_me": false }

En uno propio, autor es null y from_me, true.

Lo que cambia en el estado de la app de WhatsApp (fijar, archivar, etiquetas…). desde es whatsapp (el teléfono u otro dispositivo) o motor (un comando, con su command_id en el sobre). full_sync: true significa que llegó en una sincronización completa.

Cambió el estado de un chat o de un mensaje.

{
"accion": "archivar",
"chat": { "peer_ref": "…", … },
"momento": "2026-10-24T15:30:00.123Z",
"coleccion": "regular_low",
"version": 57,
"full_sync": false,
"desde": "whatsapp",
"estado": { "chat": { "peer_ref": "…", … }, "fijado": false, "archivado": true, "silenciado": false, "no_leido": false, "borrado": false, "candado": false, "etiquetas": [], "actividad_en": "2026-10-24T15:08:12Z", "actualizado_en": "2026-10-24T15:30:01Z" }
}

accion es fijar, desfijar, archivar, desarchivar, silenciar, reactivar, marcar_leido, marcar_no_leido, destacar, quitar_destacado, borrar_chat, poner_candado o quitar_candado. Opcionales: estado (el chat después del cambio; no viene en destacar ni en quitar_destacado), mensaje (en destacar y quitar_destacado), silenciado_hasta (en silenciar) y borrar_medios. «Archivado» en WhatsApp no es «conversación resuelta» de tu CRM.

Cambió el temporizador de mensajes temporales de un chat.

{
"chat": { "peer_ref": "…", … },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"es_grupo": false,
"segundos": 86400,
"momento": "2026-10-24T15:31:00Z"
}

segundos es 0 (desactivado), 86400 (24 h), 604800 (7 días) o 7776000 (90 días). Si lo cambió el motor, llega con from_me: true y el command_id en el sobre.

Se creó, editó o eliminó la nota de un chat de WhatsApp Business. El motor no escribe notas: desde siempre es whatsapp.

{
"chat": { "peer_ref": "…", … },
"nota": { "nota_id": "NOTA_ID", "texto": "Prefiere entregas por la tarde", "tipo": "no_estructurada", "eliminada": false, "creada_en": "2026-10-24T15:32:00Z" },
"momento": "2026-10-24T15:32:00Z",
"coleccion": "regular",
"version": 12,
"full_sync": false,
"desde": "whatsapp"
}

nota.texto viene vacío si se eliminó. nota.tipo es no_estructurada o estructurada.

Se creó una etiqueta de WhatsApp Business.

{
"etiqueta": { "label_id": "12", "nombre": "Pendiente de pago", "color": 3, "eliminada": false, "chats": 0, "mensajes": 0, "actualizada_en": "2026-10-24T15:33:00Z" },
"momento": "2026-10-24T15:33:00Z",
"version": 58,
"full_sync": false,
"desde": "motor",
"coleccion": "regular"
}

color es el índice de la paleta de WhatsApp (0 a 19). etiqueta.predefinida, etiqueta.orden y etiqueta.cambio_en son opcionales.

Cambió el nombre o el color de una etiqueta. Misma forma que label.created, con anterior: el nombre y el color de antes.

{
"etiqueta": { "label_id": "12", "nombre": "Pagado", "color": 5, "eliminada": false, "chats": 4, "mensajes": 0, "actualizada_en": "2026-10-24T15:34:00Z" },
"anterior": { "label_id": "12", "nombre": "Pendiente de pago", "color": 3 },
"momento": "2026-10-24T15:34:00Z",
"version": 59,
"full_sync": false,
"desde": "whatsapp",
"coleccion": "regular"
}

Se eliminó una etiqueta. Misma forma que label.created, con etiqueta.eliminada: true.

{
"etiqueta": { "label_id": "12", "nombre": "Pagado", "color": 5, "eliminada": true, "chats": 0, "mensajes": 0, "actualizada_en": "2026-10-24T15:35:00Z" },
"momento": "2026-10-24T15:35:00Z",
"version": 60,
"full_sync": false,
"desde": "whatsapp",
"coleccion": "regular"
}

Se puso una etiqueta en un chat o en un mensaje.

{
"label_id": "12",
"etiqueta": { "label_id": "12", "nombre": "Pagado", "color": 5 },
"chat": { "peer_ref": "…", … },
"momento": "2026-10-24T15:36:00Z",
"version": 61,
"full_sync": false,
"desde": "motor"
}

mensaje aparece si la etiqueta es de un mensaje y no del chat. etiqueta falta si el motor todavía no conoce esa etiqueta.

Se quitó una etiqueta de un chat o de un mensaje. Misma forma que label.associated.

{
"label_id": "12",
"etiqueta": { "label_id": "12", "nombre": "Pagado", "color": 5 },
"chat": { "peer_ref": "…", … },
"momento": "2026-10-24T15:37:00Z",
"version": 62,
"full_sync": false,
"desde": "whatsapp"
}

Cambió el estado de la sincronización de una colección del estado de la app.

{ "coleccion": "regular_high", "estado": "al_dia", "version": 140, "full_sync": true, "recuperacion": false }

coleccion es regular, regular_low o regular_high; estado, al_dia, error o clave_faltante. Con error llegan además error (clave_faltante, mac_invalido, tiempo_excedido, sin_sesion, rechazado, no_capturado u otro) y reintentos. No llega uno por cada reintento: solo cuando cambia el estado o el código. recuperacion: true es la copia que mandó el teléfono a pedido del motor.

Lo que el número sabe de cada contacto (no son contactos de tu CRM). Los cinco tipos comparten la forma: contacto (la ficha completa, después del cambio), campo, momento y origen (whatsapp o consulta). La primera libreta de una sincronización completa no deja eventos: es el estado inicial, y se consulta con GET /v1/instances/{id}/contacts.

Cambió el contacto en la libreta del teléfono (campo: "libreta").

{
"contacto": {
"contacto": { "peer_ref": "…", … },
"telefono": "+NUMERO_DEL_CONTACTO",
"en_libreta": true,
"nombre": "Nombre Apellido",
"primer_nombre": "Nombre",
"push_name": "Nombre del contacto",
"actualizado_en": "2026-10-24T15:40:00Z",
"creado_en": "2026-10-20T10:00:00Z"
},
"campo": "libreta",
"accion": "guardado",
"momento": "2026-10-24T15:40:00Z",
"origen": "whatsapp"
}

accion es guardado, renombrado o borrado (con contacto.nombre vacío). full_sync: true aparece si llegó en una sincronización completa. telefono solo aparece si el motor comprobó su número: un LID nunca se convierte en teléfono.

Cambió el nombre que manda el contacto (campo: "push_name").

{ "contacto": { "contacto": { "peer_ref": "…", … }, "en_libreta": false, "push_name": "Nombre nuevo", "actualizado_en": "2026-10-24T15:41:00Z", "creado_en": "2026-10-20T10:00:00Z" }, "campo": "push_name", "momento": "2026-10-24T15:41:00Z", "origen": "whatsapp" }

Cambió el nombre verificado de una cuenta de WhatsApp Business (campo: "negocio").

{ "contacto": { "contacto": { "peer_ref": "…", … }, "en_libreta": false, "nombre_negocio": "Ferretería Centro", "actualizado_en": "2026-10-24T15:42:00Z", "creado_en": "2026-10-20T10:00:00Z" }, "campo": "negocio", "momento": "2026-10-24T15:42:00Z", "origen": "whatsapp" }

Cambió la foto de perfil del contacto (campo: "foto"). Trae el ID de la foto; la imagen se consulta aparte.

{ "contacto": { "contacto": { "peer_ref": "…", … }, "en_libreta": true, "foto_id": "FOTO_ID", "actualizado_en": "2026-10-24T15:43:00Z", "creado_en": "2026-10-20T10:00:00Z" }, "campo": "foto", "momento": "2026-10-24T15:43:00Z", "origen": "whatsapp" }

Cambió la Info del contacto (campo: "recado"), si la comparte.

{ "contacto": { "contacto": { "peer_ref": "…", … }, "en_libreta": true, "recado": "Disponible", "actualizado_en": "2026-10-24T15:44:00Z", "creado_en": "2026-10-20T10:00:00Z" }, "campo": "recado", "momento": "2026-10-24T15:44:00Z", "origen": "consulta" }

Dos peers resultaron el mismo contacto. El motor los había visto por separado (por ejemplo, uno por su número y otro por su LID) y después el servidor los informó juntos: los une en el más antiguo. Los peer_ref de unidos dejan de existir y todo lo suyo (mensajes, chats, etiquetas, contacto) pasa a peer. Si guardas conversaciones por peer_ref, júntalas al recibir este evento; un envío a un peer_ref unido responde 422. Llega con origin: live, en la misma transacción que la unión, y una sola vez por unión.

{
"peer": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"unidos": ["4b7d9e01-0000-4000-8000-0000000000b2"]
}

desde es whatsapp (otro dispositivo), motor (un comando, con su command_id) o, en el perfil, consulta.

Cambió el perfil propio de la cuenta vinculada. campo es nombre, recado (la Info) o foto.

{ "campo": "nombre", "valor": "Sucursal centro", "desde": "motor", "momento": "2026-10-24T15:25:00Z" }

En foto, valor es el ID de la foto, y quitada: true si se quitó. El primer valor que trae una sincronización completa o una consulta no deja evento: es el estado inicial.

Cambió la privacidad de la cuenta. Trae la privacidad completa y qué opciones cambiaron.

{
"privacidad": { "last": "contacts", "online": "match_last_seen", "readreceipts": "all", "profile": "contacts" },
"cambiadas": ["last"],
"desde": "whatsapp"
}

Las opciones son las del SDK: groupadd, last, status, profile, readreceipts, online, calladd, messages, defense y stickers. Si cambió el temporizador por defecto de los mensajes temporales, cambiadas trae temporizador_por_defecto y llega temporizador_por_defecto_seg. Cuando lo cambia otro dispositivo, WhatsApp a veces solo avisa que cambió: entonces llega sin temporizador_por_defecto_seg.

Cambió la lista de bloqueados.

{
"cambios": [
{ "contacto": { "peer_ref": "…", … }, "telefono": "+NUMERO_DEL_CONTACTO", "bloqueado": true, "cambio_en": "2026-10-24T15:45:00Z", "desde": "motor" }
],
"desde": "motor"
}

telefono es opcional, como en los contactos.

Llegó un lote de historial de WhatsApp. Llega siempre, esté o no encendida la importación; los mensajes importados llegan aparte, como message.received con origin: history.

{
"tipo_sync": "INITIAL_BOOTSTRAP",
"fragmento": 1,
"progreso": 35,
"conversaciones": 12,
"mensajes": 480,
"contenido_importado": false,
"generacion": 2
}

tipo_sync es el de WhatsApp (por ejemplo INITIAL_BOOTSTRAP o ON_DEMAND). contenido_importado: false: el lote no se importó (importación apagada). sync_job_id aparece si el lote es de un trabajo de historial.

Avanzó un trabajo de historial.

{
"sync_job_id": "2d3e4f50-0000-4000-8000-0000000000b2",
"tipo": "inicial",
"estado": "en_curso",
"motivo": "",
"lotes_recibidos": 3,
"lotes_importados": 3,
"importados": 1450,
"duplicados": 12,
"omitidos_fecha": 0,
"omitidos_cuota": 0,
"no_soportados": 40,
"progreso": 60,
"mas_viejo_en": "2026-07-26T09:00:00Z",
"mas_nuevo_en": "2026-10-24T15:00:00Z",
"pedidos": 0,
"sin_respuesta": 0,
"chats": 0,
"chat_actual": 0,
"historia_completa": false,
"aviso": "WhatsApp no garantiza la historia completa: es lo que el teléfono tenía y envió."
}

tipo es inicial, pedido o importacion; estado, en_curso, esperando, en_pausa, completado, cancelado o fallido. historia_completa siempre es false: WhatsApp no garantiza la historia completa. El sobre trae el command_id del trabajo, si lo pediste.

Terminó un trabajo de historial. Misma forma que sync.progress, con estado: "completado" y el motivo del cierre: pedido_atendido, sin_mas_historial, sin_mas_lotes, chats_recorridos, cuota, corte, sin_mensajes, fuera_del_historial o solo_en_el_telefono (el teléfono tiene mensajes más viejos de ese chat, pero no se pueden pedir).

{ "sync_job_id": "2d3e4f50-0000-4000-8000-0000000000b2", "tipo": "pedido", "estado": "completado", "motivo": "sin_mas_historial", "importados": 50, "historia_completa": false, "…": "…" }

Un trabajo de historial terminó cancelado o fallido. Misma forma que sync.progress, con estado: "cancelado" (motivo: "cancelado") o estado: "fallido" y su motivo: sin_respuesta (el teléfono no contestó el pedido), pedido_rechazado o no_autorizado (la credencial que lo pidió ya no lo autoriza).

{ "sync_job_id": "2d3e4f50-0000-4000-8000-0000000000b3", "tipo": "pedido", "estado": "fallido", "motivo": "sin_respuesta", "importados": 0, "historia_completa": false, "…": "…" }

Uno por cada consulta remota al catálogo o a las colecciones de un negocio. No traen productos: esos se leen en la respuesta de la consulta.

La consulta terminó bien.

{
"consulta_id": "1a2b3c4d-0000-4000-8000-0000000000c6",
"operacion": "catalogo",
"resultado": "ok",
"cantidad": 20,
"vence_en": "2026-10-24T16:00:00Z",
"continuacion": true
}

operacion es catalogo o colecciones. continuacion solo aparece en catalogo: hay más páginas.

La consulta falló.

{ "consulta_id": "1a2b3c4d-0000-4000-8000-0000000000c7", "operacion": "colecciones", "codigo": "catalogo_no_disponible" }

codigo es el mismo de la respuesta HTTP (por ejemplo catalogo_no_disponible, catalogo_no_autorizado, limitado_por_whatsapp o consulta_agotada). Opcionales: via (la vía de consulta de Ajustes) y, si WhatsApp respondió con un código, server_code y server_origin.

Un evento del proveedor que el motor no clasifica. Se guarda para no perderlo, nunca con un texto inventado.

{ "sdk_tipo": "ProtocolMessage", "protocolo": "TIPO_DE_PROTOCOLO", "generacion": 2 }

sdk_tipo es el tipo del SDK. Según el caso trae protocolo (un aviso de protocolo), secreto (un mensaje cifrado que no es una edición) o sdk: el evento del SDK serializado tal cual, que puede traer identificadores de WhatsApp y cambia de forma con el SDK. Úsalo solo para diagnóstico, no para lógica de tu app.

El webhook de prueba (POST /v1/subscriptions/{id}/test) manda un webhook.test con data: {"prueba": true}, fuera del ledger: su instance_id va en ceros, su seq y su generation en 0, y su origin es operator. No lo cuentes como un evento de una instancia.

{
"schema_version": 1,
"event_id": "e1f0c2d4-0000-4000-8000-000000009999",
"instance_id": "00000000-0000-0000-0000-000000000000",
"seq": 0,
"type": "webhook.test",
"occurred_at": "2026-10-24T15:50:00Z",
"received_at": "2026-10-24T15:50:00Z",
"generation": 0,
"origin": "operator",
"data": { "prueba": true }
}

Los 75 tipos que el motor emite hoy, más el webhook.test. Todos con schema_version: 1. «Parcial» marca lo que las pruebas con el número real no vieron llegar siempre.

Tipo Dominio Versión Nota
state.changed Instancia 1
pairing.available Instancia 1
pairing.expired Instancia 1
pairing.failed Instancia 1
paired Instancia 1
connected Instancia 1
disconnected Instancia 1
logged_out Instancia 1
stream_replaced Instancia 1
temporary_ban Instancia 1
error Instancia 1
instance.updated Instancia 1
command.accepted Comandos 1
command.failed Comandos 1
command.ambiguous Comandos 1
command.cancelled Comandos 1
message.received Mensajes 1
message.updated Mensajes 1 Parcial: ubicacion_actualizada
message.revoked Mensajes 1
message.reaction Mensajes 1
message.sent Mensajes 1 Parcial: conciliado
message.undecryptable Mensajes 1
message.expired Mensajes 1
receipt Mensajes 1
poll.vote Mensajes 1
presence Mensajes 1 Parcial
interactive.response Interactivos 1 Parcial: listas en iOS
interactive.unknown Interactivos 1
interactive.response.updated Interactivos 1
media.ready Multimedia 1
media.failed Multimedia 1
call.offer Llamadas 1
call.offer_notice Llamadas 1
call.preaccept Llamadas 1
call.accept Llamadas 1
call.terminate Llamadas 1
call.reject Llamadas 1
call.signal Llamadas 1
call.rejected Llamadas 1
group.changed Grupos y comunidades 1
group.joined Grupos y comunidades 1
group.participants Grupos y comunidades 1
community.changed Grupos y comunidades 1 Parcial: mudanza
newsletter.joined Canales 1
newsletter.left Canales 1
newsletter.muted Canales 1 Parcial: desde el teléfono
newsletter.changed Canales 1
newsletter.updates Canales 1 Parcial: en vivo
status.received Estados 1
status.revoked Estados 1
appstate.changed Chats y etiquetas 1
chat.ephemeral Chats y etiquetas 1
chat.note Chats y etiquetas 1
label.created Chats y etiquetas 1 Parcial: ID reusado desde el teléfono
label.updated Chats y etiquetas 1
label.deleted Chats y etiquetas 1
label.associated Chats y etiquetas 1
label.dissociated Chats y etiquetas 1
appstate.sync Chats y etiquetas 1
contact.updated Contactos 1
contact.push_name Contactos 1
contact.business_name Contactos 1
contact.picture Contactos 1
contact.about Contactos 1
peer.merged Contactos 1
profile.updated Perfil y privacidad 1 Parcial: Info desde el teléfono
privacy.changed Perfil y privacidad 1
blocklist.changed Perfil y privacidad 1
history.sync Historial 1
sync.progress Historial 1
sync.completed Historial 1 Parcial: solo_en_el_telefono
sync.failed Historial 1
query.completed Catálogo comercial 1
query.failed Catálogo comercial 1
provider.unknown Otros 1 Solo diagnóstico
webhook.test Otros 1 Fuera del ledger

Puedes filtrar por tipo en GET /v1/instances/{id}/events (types, hasta 20), en una suscripción de webhook, en un stream y en un broker.