Saltar al contenido principal

Catálogo de eventos

curl https://api.joinotify.com/webhook-endpoints/events \
-H 'Authorization: Bearer sk_live_xxx'

Un endpoint con events: [] los recibe todos.

Mensajes​

EventoCuándo se dispara
messagesLlegó un mensaje, o cambió el estado de uno que enviaste
smb_message_echoesMensaje enviado desde la aplicación del móvil (coexistencia)
historyBloques del historial sincronizado
smb_app_state_syncContactos añadidos, renombrados o eliminados en el móvil

messages es el que más lleva. Su value puede traer messages[] (el cliente escribió) o statuses[] (cambió algo de lo que enviaste).

Mensaje recibido
{
"field": "messages",
"value": {
"metadata": { "phone_number_id": "106540352242922" },
"contacts": [{ "profile": { "name": "María" }, "wa_id": "5541999999999" }],
"messages": [
{
"from": "5541999999999",
"id": "wamid.HBgN...",
"timestamp": "1785312000",
"type": "text",
"text": { "body": "Hola, ¿ya salió mi pedido?" }
}
]
}
}
Estado de entrega
{
"field": "messages",
"value": {
"statuses": [
{
"id": "wamid.HBgN...",
"status": "delivered",
"timestamp": "1785312042",
"recipient_id": "5541999999999",
"conversation": { "id": "...", "origin": { "type": "service" } },
"pricing": { "billable": true, "category": "service" }
}
]
}
}
statusSignificado
sentSalió de la infraestructura de Meta
deliveredLlegó al dispositivo
readSe abrió, si el cliente no desactivó la confirmación
failedNo se entregó — el motivo está en errors[]

El objeto pricing dice si esa conversación se factura y en qué categoría. Es la fuente correcta para conciliar costes.

Plantillas​

EventoCuándo se dispara
message_template_status_updateAprobada, rechazada, pausada o deshabilitada
message_template_quality_updateCambió la calidad (GREEN / YELLOW / RED)
message_template_components_updateCambió el contenido de la plantilla
template_category_updateMeta reclasificó la categoría
{
"field": "message_template_status_update",
"value": {
"event": "APPROVED",
"message_template_id": 1234567890,
"message_template_name": "confirmacao_pedido",
"message_template_language": "pt_BR"
}
}

Suscríbete a message_template_status_update en lugar de consultar GET /templates en bucle — es por ahí por donde llega la aprobación.

template_category_update merece atención: una reclasificación de UTILITY a MARKETING aumenta el coste de cada envío de esa plantilla, sin aviso previo.

Cuenta​

EventoCuándo se dispara
account_alertsAlerta de Meta sobre la cuenta
account_updateCambió el estado de la cuenta de empresa
account_review_updateResultado de la revisión de la cuenta
account_settings_updateSe cambiaron los ajustes

account_update incluye baneos y restricciones. Vale la pena monitorizarlo: es el aviso de que la operación va a parar.

Número​

EventoCuándo se dispara
phone_number_name_updateResultado de la solicitud de nombre de visualización
phone_number_quality_updateCambió la calidad del número
{
"field": "phone_number_quality_update",
"value": {
"display_phone_number": "5541987111527",
"event": "FLAGGED",
"current_limit": "TIER_1K"
}
}

FLAGGED significa que la calidad cayó a RED y el límite de mensajes puede reducirse. Es el momento de revisar qué se está enviando, no de enviar más.

Negocio​

EventoCuándo se dispara
business_capability_updateCambiaron los límites y capacidades
business_status_updateCambió el estado de verificación del negocio

Otros​

EventoCuándo se dispara
securityEventos de seguridad de la cuenta
flowsInteracciones con WhatsApp Flows
callsEventos de llamada
user_preferencesEl cliente se dio de baja o volvió a aceptar mensajes

user_preferences es la baja. Ignorarlo es el camino corto para que la calidad del número se desplome — y, en muchos casos, un problema de cumplimiento normativo.

Facturación​

Eventos que originamos nosotros, como los de billetera e interacción más abajo — los de arriba vienen todos de Meta y se reenvían. De ahí el prefijo en el nombre y el source: "joinotify" del payload.

EventoCuándo se dispara
billing.past_dueEl pago falló y empezó la tolerancia. Todavía envía
billing.blockedAcceso cortado: las rutas de envío pasan a responder 402
billing.restoredVuelve a poder enviar

El disparador es el derecho de uso, no el estado del medio de pago. La diferencia importa: puedes dejar de enviar porque falló la tarjeta, porque caducó la licencia, porque terminó la prueba o porque alcanzaste el tope de mensajes del plan. Para quien integra, los cuatro son el mismo hecho, y reason dice cuál fue.

billing.blocked
{
"field": "billing.blocked",
"value": {
"state": "blocked",
"reason": "license_expired",
"message": "The license for this account has expired. Renew it to keep sending messages.",
"previous_state": "ok",
"billable_numbers": 2,
"grace_ends_at": null,
"source": "joinotify",
"occurred_at": "2026-08-15T21:00:00.000Z"
}
}

El evento sale en la transición, nunca repetido: un bloqueo que dura un mes genera un billing.blocked, no uno por hora.

Son eventos de cuenta

Salen sin wabaId y sin phoneNumberId: la facturación es de la cuenta, no de un número. La consecuencia es que un endpoint filtrado por número no los recibe. Para recibir facturación, registra un endpoint sin filtro.

Cartera y recomendaciones​

Si la cuenta participa en el programa de recomendación, cinco eventos más describen el dinero que entra y sale de la cartera. También son de cuenta y también llevan source: "joinotify".

EventoCuándo se dispara
wallet.creditedEntró saldo: comisión, ajuste del operador o devolución de un retiro fallido
wallet.debitedSalió saldo: aplicado a una factura o reservado por una solicitud de retiro
affiliate.approvedLa inscripción fue aprobada y el código empezó a valer
affiliate.rejectedLa inscripción fue rechazada o suspendida — reason dice por qué
payout.paidEl retiro fue pagado, con method (pix/paypal) y payment_ref
wallet.credited
{
"field": "wallet.credited",
"value": {
"amount_cents": 2000,
"currency": "brl",
"bucket": "pending",
"available_at": "2026-09-18T12:00:00.000Z",
"reason": "commission",
"referral_id": "ref_1a2b3c",
"source": "joinotify",
"occurred_at": "2026-08-19T12:00:00.000Z"
}
}

payout.paid lleva además paid_amount_cents, paid_currency y fee_cents cuando el retiro salió por PayPal: allí el importe que llega no es el que salió, porque la comisión y la conversión ocurren del otro lado. En un Pix los tres vienen nulos.

bucket es lo que evita la confusión más cara de este flujo: la comisión nace retenida (pending) y solo pasa a ser saldo retirable (available) cuando vence la retención, que es lo que absorbe un reembolso que llega después. Un wallet.credited con bucket: "pending" todavía no es dinero disponible, y available_at dice cuándo lo será.

Interacción​

Nueve eventos cuentan lo que pasó aquí dentro con tus contactos, campañas, flujos y conversaciones. También llevan source: "joinotify", y son lo que permite reaccionar sin consultar la API en bucle — "el flujo capturó un lead → crear el cliente en el ERP".

EventoCuándo se dispara
contact.createdUn contacto nuevo: primer mensaje, API, flujo o panel. Las importaciones no anuncian fila por fila
contact.opted_inConsentimiento de marketing registrado — desde el panel, la API o un flujo
contact.opted_outBaja: palabra clave, rechazo 131050 de Meta, panel, API o flujo
broadcast.completedUna campaña terminó de enviar
broadcast.pausedUna campaña se detuvo — por alguien o sola
flow.run.completedUn contacto llegó al final de un flujo
flow.answer.capturedUn paso de Pregunta o de Esperar respuesta guardó una respuesta
conversation.assignedUna conversación se entregó a alguien del equipo
conversation.closedUna conversación se cerró, por una persona o por un flujo

Estos eventos solo se generan si alguien se suscribe a ellos: sin ningún endpoint activo que los reciba, no se graba nada. Después de crear o editar un endpoint, cuenta hasta treinta — es lo que tarda en releerse la lista de suscripciones.

Contactos​

Todo evento sobre un contacto trae el mismo objeto contact: lo suficiente para volver a encontrarlo, sin más datos personales.

contact.opted_out
{
"field": "contact.opted_out",
"value": {
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"via": "keyword",
"reason": "PARAR",
"source": "joinotify",
"occurred_at": "2026-09-19T14:02:11.000Z"
}
}

via dice de dónde vino el cambio: manual (panel), api, flow, inbound (el primer mensaje del cliente creó el contacto), keyword (la persona envió una palabra de baja) o meta_131050 (Meta rechazó un envío porque la persona dejó de aceptar marketing). reason solo aparece en la baja — con una palabra clave, es el texto que envió la persona.

Los eventos de contacto son de la cuenta

contact.* sale sin wabaId ni phoneNumberId, como la facturación: un contacto es de la cuenta, no de un número. Un endpoint filtrado por número no los recibe.

Campañas​

broadcast.paused
{
"field": "broadcast.paused",
"value": {
"broadcast": {
"id": "cm1bro4dc4st01",
"name": "Black Friday",
"template": "promo_black_friday",
"language": "pt_BR",
"phone_number_id": "106540352242922",
"flow_id": null,
"recipients": 4800,
"sent": 1210,
"delivered": 1175,
"read": 640,
"failed": 12,
"clicked": 88,
"replied": 31,
"opted_out": 4
},
"reason": "throughput",
"detail": "130429",
"resume_after": "2026-11-27T13:55:00.000Z",
"source": "joinotify",
"occurred_at": "2026-11-27T13:40:00.000Z"
}
}

Los números son los del momento en que salió el evento. Los estados de una campaña terminada siguen llegando durante días, así que broadcast.completed trae la foto del cierre, no el resultado final. Lleva finished_at en lugar de reason.

reasonPor qué se detuvo la campaña
manualAlguien la pausó
qualityLa calidad del número pasó a rojo, o Meta aplicó su límite de spam (131048)
templateLa plantilla se pausó, desactivó, borró o pasó a calidad roja
accountEl número se desconectó, o la cuenta está bloqueada, restringida o sin pago en Meta
quotaEl plan alcanzó su techo mensual de mensajes
billingLa cuenta perdió el derecho de uso
errorsDemasiados fallos seguidos, o errores de parámetro justo al inicio — suele ser el mapeo de variables
throughputMeta retuvo el caudal durante minutos. Se reanuda sola a los 15 min, 1 h y 4 h; resume_after dice cuándo
send_windowFuera de la ventana de envío configurada. Se reanuda sola en la próxima apertura

detail complementa el motivo — el código de Meta, el estado de la plantilla (PAUSED, DELETED, RED…) — y puede venir nulo.

Flujos​

flow.run.completed
{
"field": "flow.run.completed",
"value": {
"flow": { "id": "cm1fl0w000001", "name": "Captura de lead", "version": 4 },
"run": {
"id": "cm1run0000001",
"trigger_type": "api",
"end_reason": "end",
"started_at": "2026-09-19T14:00:02.000Z",
"finished_at": "2026-09-19T14:03:40.000Z"
},
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"variables": { "empresa": "Padaria Central", "interesse": "orçamento" },
"source": "joinotify",
"occurred_at": "2026-09-19T14:03:40.000Z"
}
}

variables trae todas las variables del flujo al final de la ejecución — incluidas las respuestas que dio el contacto. Las ejecuciones de prueba nunca generan eventos.

flow.answer.captured
{
"field": "flow.answer.captured",
"value": {
"flow": { "id": "cm1fl0w000001", "name": "Captura de lead", "version": 4 },
"run_id": "cm1run0000001",
"node_id": "question_2",
"variable": "empresa",
"field": null,
"value": "Padaria Central",
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"source": "joinotify",
"occurred_at": "2026-09-19T14:01:15.000Z"
}
}

variable es el nombre de la variable del flujo que recibió la respuesta; field, el campo del contacto, cuando el paso escribe directo en uno. Cualquiera de los dos puede venir nulo.

Conversaciones​

conversation.assigned
{
"field": "conversation.assigned",
"value": {
"conversation": { "id": "cm1c0nv000001", "phone_number_id": "106540352242922" },
"contact": {
"id": "cm1c0ntact0001",
"phone": "+5541987111527",
"wa_id": "5541987111527",
"bsuid": null,
"first_name": "Ana",
"last_name": "Souza",
"email": null
},
"assigned_user_id": "cm1us3r000001",
"assigned_by": "flow:cm1fl0w000001",
"source": "joinotify",
"occurred_at": "2026-09-19T14:03:41.000Z"
}
}

assigned_by y closed_by dicen quién actuó: el id de una persona del equipo, flow:<id> cuando fue un flujo, o null cuando fue una API key. conversation.closed trae solo closed_by.

Campañas, flujos y conversaciones salen con el número (phoneNumberId) y su cuenta de empresa, así que un endpoint filtrado por número o por WABA recibe los eventos de ese número.

Consultar el historial​

Si un evento se perdió antes de que tuvieras un endpoint en producción:

curl 'https://api.joinotify.com/events?field=messages&limit=50' \
-H 'Authorization: Bearer sk_live_xxx'

El campo payload es el change.value original, sin tocar.