Raphael Serafim· Publicado el 10 de septiembre de 2026· 9 min de lectura

Métricas de campaña en WhatsApp: entregado, leído y respondido por la API

Enviado no es entregado y entregado no es leído. Cómo capturar cada estado por el webhook, armar el embudo real de la campaña, medir la métrica que de verdad importa — la respuesta — y reconocer cuándo el número se está quemando.

Ver como Markdown

"¿Cómo fue la campaña?" casi nunca tiene respuesta, porque casi nadie captura los eventos que la responden. El envío devuelve 200, alguien concluye que salió bien, y lo que pasó después se pierde.

Este artículo es sobre medir. La cola entrega los mensajes; la medición dice si valió la pena.

El embudo real

EtapaQué significaDónde aparece
EncoladoDecidiste enviarloTu base de datos
EnviadoLa plataforma lo aceptóRespuesta de la llamada
EntregadoLlegó al aparatoWebhook delivered
LeídoLa persona lo abrióWebhook read
RespondidoLa persona escribió de vueltaWebhook messages
Se dio de bajaPidió no recibir másTu manejo de opt-out

Cada escalón pierde gente. Dónde pierde dice qué arreglar — y sin los escalones intermedios solo tienes el primero y el último.

Capturar los estados

Todo evento de estado llega en value.statuses, y no en value.messages:

javascript
async function procesar(evento) {
  const value = evento?.entry?.[0]?.changes?.[0]?.value;

  for (const s of value?.statuses ?? []) {
    await registrarEstado({
      messageId: s.id,
      estado: s.status,        // sent | delivered | read | failed
      en: new Date(Number(s.timestamp) * 1000),
      error: s.errors?.[0]?.code,
    });
  }

  const msg = value?.messages?.[0];
  if (msg) await registrarRespuesta(msg);
}

Confundir los dos campos es el error número uno de quien escribe el parser por primera vez — está entre las 7 causas de webhook que no funciona.

Unir el estado al envío

Solo funciona si guardaste el messageId al momento de enviar:

javascript
const r = await enviarPlantilla(to, plantilla, params);
await db.query(
  `UPDATE envios SET estado = 'enviado', message_id = $1, enviado_en = NOW()
     WHERE campana_id = $2 AND numero = $3`,
  [r.messageId, campanaId, to],
);

Y al recibir el estado:

javascript
await db.query(
  `UPDATE envios
      SET estado = $1, ${columna(estado)} = $2
    WHERE message_id = $3`,
  [estado, en, messageId],
);

Una columna de timestamp por etapa (entregado_en, leido_en, respondido_en) rinde más que una sola columna de estado: además del embudo, ganas el tiempo entre etapas, y es ahí donde se ve un problema de ritmo.

El reporte

sql
SELECT
  COUNT(*)                                          AS encolados,
  COUNT(*) FILTER (WHERE estado <> 'pendiente')     AS enviados,
  COUNT(*) FILTER (WHERE entregado_en IS NOT NULL)  AS entregados,
  COUNT(*) FILTER (WHERE leido_en     IS NOT NULL)  AS leidos,
  COUNT(*) FILTER (WHERE respondido_en IS NOT NULL) AS respondidos,
  COUNT(*) FILTER (WHERE estado = 'fallo_definitivo') AS fallos,
  ROUND(100.0 * COUNT(*) FILTER (WHERE entregado_en IS NOT NULL)
              / NULLIF(COUNT(*) FILTER (WHERE estado <> 'pendiente'), 0), 1) AS tasa_entrega,
  ROUND(100.0 * COUNT(*) FILTER (WHERE respondido_en IS NOT NULL)
              / NULLIF(COUNT(*) FILTER (WHERE entregado_en IS NOT NULL), 0), 1) AS tasa_respuesta
FROM envios
WHERE campana_id = $1;

La API también expone números agregados de atención, útiles para una vista general sin armar tablas — por el endpoint de estadísticas o, conversando, con la herramienta get_analytics del MCP. Para análisis por campaña, en cambio, el registro en tu base es lo que permite cruzar con pedido, cliente e ingresos.

Cómo leer cada número

Tasa de entrega

RangoLectura
> 95%Base saludable
90–95%Números desactualizados; toca limpiar
< 90%Limpieza antes de la próxima campaña

En la API oficial esto no es solo desperdicio: un fallo repetido perjudica la reputación del número.

Tasa de lectura. Siempre subestima — solo aparece para quien tiene activa la confirmación de lectura. Sirve para comparar campañas entre sí, no como número absoluto.

Tasa de respuesta. La que importa. Es la única que exige una acción deliberada, y en WhatsApp además abre la ventana de 24 horas — o sea, la atención siguiente no genera cobro nuevo. Una campaña con buena respuesta es más barata por conversión de lo que sugieren los números brutos. Vale leerlo junto con la ventana de 24 horas y las plantillas.

Lectura alta con respuesta cero casi siempre significa un mensaje que no pide nada. Una plantilla con botón de respuesta rápida suele resolverlo.

Los dos números que avisan antes

La entrega y la lectura cuentan el pasado. El bloqueo y la denuncia cuentan el futuro.

sql
SELECT
  c.id, c.nombre, c.enviada_en,
  ROUND(100.0 * COUNT(*) FILTER (WHERE e.entregado_en IS NOT NULL)
              / NULLIF(COUNT(*), 0), 1) AS entrega,
  COUNT(*) FILTER (WHERE e.error = 'contacto_bloqueo') AS bloqueos,
  COUNT(*) FILTER (WHERE e.estado = 'optout')          AS bajas
FROM campanas c
JOIN envios e ON e.campana_id = c.id
GROUP BY c.id
ORDER BY c.enviada_en DESC
LIMIT 10;

Mira la columna entrega de arriba hacia abajo. Si cae campaña tras campaña con la misma base, la reputación se está deteriorando — y el paso siguiente es una restricción, no un aviso.

Cuando aparece ese patrón, la respuesta es contraintuitiva: enviar menos. Bajar la frecuencia, revisar el contenido, reconfirmar a los contactos antiguos. Subir el volumen para compensar la caída de entrega es lo que convierte un problema recuperable en un número perdido.

Comparar de verdad

Para que una comparación valga, cambia una variable por vez:

  • mismo público, textos distintos → el texto;
  • mismo texto, horarios distintos → el horario;
  • mismo texto, con y sin botón → el botón.

Cambiar texto, horario y público a la vez y concluir que "la versión nueva anduvo mejor" no informa nada sobre qué repetir.

Conclusión

Medir una campaña de WhatsApp es guardar el messageId, capturar value.statuses y tener una columna de timestamp por etapa. Media hora de trabajo, una sola vez.

El retorno es dejar de decidir por impresión: pasas a saber si el problema es la lista, el texto, el horario o el número — y cada uno tiene una corrección distinta.

¿Listo para automatizar tu WhatsApp?

Crea tu cuenta gratis y empieza a enviar mensajes por la API en minutos.

Empezar gratis

Preguntas frecuentes

¿Cómo sé si el mensaje fue entregado por la API de WhatsApp?+

Por el webhook de estado. Después del envío llegan eventos en entry[0].changes[0].value.statuses con el id del mensaje y el estado: sent, delivered, read o failed. Los unes al envío original por el messageId que devolvió la llamada de envío.

¿Cuál es la diferencia entre enviado, entregado y leído?+

Enviado significa que la plataforma aceptó el mensaje. Entregado significa que llegó al aparato. Leído significa que la persona abrió la conversación, y solo aparece si mantiene activada la confirmación de lectura, así que la tasa de lectura siempre subestima la realidad.

¿Qué tasa de entrega se considera buena?+

Por encima del 95% es saludable. Entre 90% y 95% indica una base con números desactualizados. Por debajo del 90% es señal de que la lista necesita limpieza — y en la API oficial, un intento fallido repetido también perjudica la reputación del número.

¿Cómo mido si la campaña dio resultado?+

Por la tasa de respuesta, no por la de lectura. La respuesta es la única métrica que exige una acción deliberada de la persona, y en WhatsApp además abre la ventana de 24 horas, lo que reduce el costo de la atención siguiente.

¿Cómo me doy cuenta de que el número se está quemando?+

Sigue los bloqueos y las denuncias por campaña y la tasa de entrega a lo largo del tiempo. Una entrega que cae campaña tras campaña, con la misma base, es la señal más confiable de que la reputación se está deteriorando. Cuando eso aparece, baja el volumen antes de perder el número.

Sigue leyendo