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
| Evento | Cuándo se dispara |
|---|---|
messages | Llegó un mensaje, o cambió el estado de uno que enviaste |
smb_message_echoes | Mensaje enviado desde la aplicación del móvil (coexistencia) |
history | Bloques del historial sincronizado |
smb_app_state_sync | Contactos 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).
{
"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?" }
}
]
}
}
{
"field": "messages",
"value": {
"statuses": [
{
"id": "wamid.HBgN...",
"status": "delivered",
"timestamp": "1785312042",
"recipient_id": "5541999999999",
"conversation": { "id": "...", "origin": { "type": "service" } },
"pricing": { "billable": true, "category": "service" }
}
]
}
}
status | Significado |
|---|---|
sent | Salió de la infraestructura de Meta |
delivered | Llegó al dispositivo |
read | Se abrió, si el cliente no desactivó la confirmación |
failed | No 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
| Evento | Cuándo se dispara |
|---|---|
message_template_status_update | Aprobada, rechazada, pausada o deshabilitada |
message_template_quality_update | Cambió la calidad (GREEN / YELLOW / RED) |
message_template_components_update | Cambió el contenido de la plantilla |
template_category_update | Meta 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
| Evento | Cuándo se dispara |
|---|---|
account_alerts | Alerta de Meta sobre la cuenta |
account_update | Cambió el estado de la cuenta de empresa |
account_review_update | Resultado de la revisión de la cuenta |
account_settings_update | Se 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
| Evento | Cuándo se dispara |
|---|---|
phone_number_name_update | Resultado de la solicitud de nombre de visualización |
phone_number_quality_update | Cambió 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
| Evento | Cuándo se dispara |
|---|---|
business_capability_update | Cambiaron los límites y capacidades |
business_status_update | Cambió el estado de verificación del negocio |
Otros
| Evento | Cuándo se dispara |
|---|---|
security | Eventos de seguridad de la cuenta |
flows | Interacciones con WhatsApp Flows |
calls | Eventos de llamada |
user_preferences | El 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.
| Evento | Cuándo se dispara |
|---|---|
billing.past_due | El pago falló y empezó la tolerancia. Todavía envía |
billing.blocked | Acceso cortado: las rutas de envío pasan a responder 402 |
billing.restored | Vuelve 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.
{
"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.
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".
| Evento | Cuándo se dispara |
|---|---|
wallet.credited | Entró saldo: comisión, ajuste del operador o devolución de un retiro fallido |
wallet.debited | Salió saldo: aplicado a una factura o reservado por una solicitud de retiro |
affiliate.approved | La inscripción fue aprobada y el código empezó a valer |
affiliate.rejected | La inscripción fue rechazada o suspendida — reason dice por qué |
payout.paid | El retiro fue pagado, con method (pix/paypal) y payment_ref |
{
"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".
| Evento | Cuándo se dispara |
|---|---|
contact.created | Un contacto nuevo: primer mensaje, API, flujo o panel. Las importaciones no anuncian fila por fila |
contact.opted_in | Consentimiento de marketing registrado — desde el panel, la API o un flujo |
contact.opted_out | Baja: palabra clave, rechazo 131050 de Meta, panel, API o flujo |
broadcast.completed | Una campaña terminó de enviar |
broadcast.paused | Una campaña se detuvo — por alguien o sola |
flow.run.completed | Un contacto llegó al final de un flujo |
flow.answer.captured | Un paso de Pregunta o de Esperar respuesta guardó una respuesta |
conversation.assigned | Una conversación se entregó a alguien del equipo |
conversation.closed | Una 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.
{
"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.
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
{
"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.
reason | Por qué se detuvo la campaña |
|---|---|
manual | Alguien la pausó |
quality | La calidad del número pasó a rojo, o Meta aplicó su límite de spam (131048) |
template | La plantilla se pausó, desactivó, borró o pasó a calidad roja |
account | El número se desconectó, o la cuenta está bloqueada, restringida o sin pago en Meta |
quota | El plan alcanzó su techo mensual de mensajes |
billing | La cuenta perdió el derecho de uso |
errors | Demasiados fallos seguidos, o errores de parámetro justo al inicio — suele ser el mapeo de variables |
throughput | Meta retuvo el caudal durante minutos. Se reanuda sola a los 15 min, 1 h y 4 h; resume_after dice cuándo |
send_window | Fuera 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
{
"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.
{
"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
{
"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.