Actualizado el 25/10/2026
En la API de WhatsApp, el estado de un mensaje enviado llega por el webhook, en el arreglo statuses, y no en la respuesta del envío. Los valores posibles son sent (salió de los servidores de WhatsApp), delivered (llegó al dispositivo), read (se mostró en un chat abierto), played (nota de voz reproducida por primera vez) y failed (no se pudo enviar o entregar, con el motivo en errors). Cada evento trae el wamid del mensaje, y tu sistema debe guardar el estado más avanzado sin dejar que un evento atrasado haga retroceder el registro.
Esta guía muestra qué significa cada estado, los payloads, cómo leer errores y cobro y un modelo de almacenamiento en Node.js y Python.
La respuesta del envío no es el estado
Cuando llamas al endpoint de mensajes, la respuesta 200 trae el wamid:
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "5215512345678", "wa_id": "5215512345678" }],
"messages": [{ "id": "wamid.HBgNNTIxNTUxMjM0NTY3OBUCABEYEjQ..." }]
}
Eso solo significa que Meta aceptó la solicitud. El mensaje todavía puede fallar, por ejemplo por el límite de marketing por usuario o porque el número no tiene WhatsApp. Guarda el wamid al enviar: es la clave que une el mensaje con los estados que llegarán después.
Los cinco estados de la Cloud API
La referencia del webhook de estado define:
| Estado | Significado | En la app del cliente |
|---|---|---|
sent | El mensaje salió de los servidores de WhatsApp | Una palomita |
delivered | El mensaje llegó al dispositivo del destinatario | Dos palomitas |
read | El mensaje se mostró en un chat abierto en el dispositivo | Dos palomitas azules |
played | Primera reproducción de un mensaje de voz | Micrófono azul |
failed | El mensaje no se pudo enviar o entregar | Triángulo rojo de error |
Dos detalles de la documentación cambian la forma de procesarlos:
readpuede llegar sindelivered. Si el cliente tiene el chat abierto cuando llega el mensaje, Meta omite el webhook dedeliveredy envía soloread. Tratareadcomo «entregado y leído».playedsolo existe para notas de voz. Se dispara la primera vez que el cliente reproduce un audio enviado como voz (mira cómo enviar audio y notas de voz).
El payload de estado
Todo estado llega con field: "messages", igual que los mensajes recibidos. Lo que cambia es que value trae statuses en lugar de messages:
{
"object": "whatsapp_business_account",
"entry": [{
"id": "WABA_ID",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "5215500000000", "phone_number_id": "PHONE_NUMBER_ID" },
"statuses": [{
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OBUCABEYEjQ...",
"status": "delivered",
"timestamp": "1761400000",
"recipient_id": "5215512345678",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"type": "regular",
"category": "marketing"
}
}]
}
}]
}]
}
Campos del objeto de estado:
| Campo | Cuándo aparece | Para qué sirve |
|---|---|---|
id | Siempre | El wamid del mensaje enviado |
status | Siempre | Uno de los cinco valores anteriores |
timestamp | Siempre | Momento (Unix, en segundos) en que se generó el evento |
recipient_id | Siempre | Número del destinatario (o ID del grupo, en grupos) |
biz_opaque_callback_data | Si enviaste ese campo en el mensaje | Devuelve un identificador tuyo, como el ID del pedido o de la campaña |
pricing | En sent y en uno de los dos, delivered o read | Categoría y tipo de cobro |
errors | Solo en failed | Código y descripción del error |
En mensajes a grupos, recipient_id trae el ID del grupo, y el número del participante va en recipient_participant_id.
Un mismo POST puede traer varios estados, incluso de mensajes distintos. Recorre siempre el arreglo completo.
El objeto pricing
pricing es la forma más confiable de saber si el mensaje se cobró y cómo:
category: categoría de la tarifa, comomarketing,utility,authenticationoservice.type:regular(cobrado),free_customer_service,free_entry_point(enviado en la ventana gratuita que abre un anuncio de clic a WhatsApp) ofree_group_customer_service.billable: Meta lo marca como obsoleto. Usatypeycategory.
Desde el 1 de octubre de 2026, Meta cambió el significado de type: los mensajes de utilidad enviados dentro de la ventana de atención ahora llegan como regular, y free_customer_service pasó a indicar un mensaje de servicio dentro de la cuota gratuita del número (1.000 al mes). Los mensajes de servicio por encima de esa cuota también llegan como regular. Si tu reporte de costos usa type, revisa la regla. Mira cuánto cuesta una campaña de WhatsApp para sumar los costos.
Desde la versión 24.0 de la Graph API, el objeto conversation deja de venir en los estados, salvo en mensajes enviados dentro de una ventana de punto de entrada gratuito.
Estado failed: cómo leer el error
Cuando el mensaje falla, el estado trae errors:
{
"id": "wamid.HBgNNTIxNTUxMjM0NTY3OBUCABEYEjQ...",
"status": "failed",
"timestamp": "1761400005",
"recipient_id": "5215512345678",
"errors": [{
"code": 131049,
"title": "This message was not delivered to maintain healthy ecosystem engagement.",
"message": "This message was not delivered to maintain healthy ecosystem engagement.",
"error_data": { "details": "In order to maintain a healthy ecosystem engagement, the message failed to be delivered." },
"href": "/documentation/business-messaging/whatsapp/support/error-codes"
}]
}
Guarda el code y el error_data.details. Los códigos que más aparecen en estados de falla, según la lista oficial de errores:
| Código | Causa | Acción |
|---|---|---|
131026 | Destinatario sin WhatsApp, sin aceptar los términos más recientes o con la app desactualizada | Confirma el número; no reintentes en bucle |
131047 | Pasaron más de 24 horas desde el último mensaje del cliente | Envía una plantilla |
131049 | No entregado para cuidar el engagement (límite de marketing por usuario) | Espera al menos 24 horas; mira límite de marketing por usuario |
131050 | El cliente dejó de recibir marketing de la empresa | Márcalo como opt-out; no reintentes |
131053 | Falló la carga del archivo, en general por tipo no compatible | Revisa el MIME del archivo |
131056 | Demasiados mensajes al mismo destinatario en poco tiempo | Espera antes de volver a escribirle a esa persona |
132015 | Plantilla pausada por baja calidad | Edita la plantilla y espera la aprobación |
131000 | Error desconocido | Reintenta; si persiste, abre un ticket |
La lista completa, con más códigos, está en errores de la API de WhatsApp.
Un mensaje failed no se entrega y, por lo tanto, no entra en el cobro por mensaje entregado.
Orden, duplicados y demoras
La documentación de Meta no promete que los estados lleguen en orden, y advierte que los reintentos de entrega del webhook pueden generar notificaciones duplicadas. Los reintentos siguen hasta 7 días. En la práctica:
- Puede llegar
readantes quedelivered, odelivereddespués deread. Nunca sobrescribas un estado más avanzado con uno anterior. - El mismo estado puede llegar dos veces. Usa
id+statuscomo clave de idempotencia. - Un estado puede llegar horas o días después si tu endpoint estuvo caído. Guarda el
timestampdel evento, no la hora en que lo procesaste. deliveredpuede no llegar nunca, por ejemplo si el teléfono del cliente sigue apagado. No trates «sindelivereden 5 minutos» como una falla.
Cómo guardar los estados sin retroceder
El modelo más simple: una fila por mensaje enviado, una columna de hora por etapa y un campo con el estado más avanzado. Ejemplo en Node.js con PostgreSQL (biblioteca pg):
const ORDEN = { sent: 1, delivered: 2, read: 3, played: 4 };
export async function guardarEstado(db, st) {
const cuando = new Date(Number(st.timestamp) * 1000);
if (st.status === 'failed') {
const err = st.errors?.[0] ?? {};
await db.query(
`UPDATE envios SET failed_at = $2, error_codigo = $3, error_detalle = $4,
status = CASE WHEN status_orden = 0 THEN 'failed' ELSE status END
WHERE wamid = $1`,
[st.id, cuando, err.code ?? null, err.error_data?.details ?? err.message ?? null],
);
return;
}
const orden = ORDEN[st.status];
if (!orden) return; // estado desconocido: regístralo en log y sigue
await db.query(
`UPDATE envios SET
${st.status}_at = COALESCE(${st.status}_at, $2),
delivered_at = CASE WHEN $3 >= 3 THEN COALESCE(delivered_at, $2) ELSE delivered_at END,
categoria = COALESCE($4, categoria),
tipo_cobro = COALESCE($5, tipo_cobro),
status = CASE WHEN $3 > status_orden THEN $6 ELSE status END,
status_orden = GREATEST(status_orden, $3)
WHERE wamid = $1`,
[st.id, cuando, orden, st.pricing?.category ?? null, st.pricing?.type ?? null, st.status],
);
}
Lo que garantiza este código:
- No retrocede.
status_ordensolo aumenta; undeliveredatrasado no borra unread. - Idempotente. Guardar el mismo evento dos veces da el mismo resultado, gracias al
COALESCE. readimplica entrega. Si se omitió eldelivered,delivered_atse completa con la hora delread.- Una falla no borra un éxito. Si la fila ya tiene un estado positivo, el
failedse registra en los campos de error sin cambiar el estado principal, para que lo investigues.
La misma lógica en Python, sin base de datos, para pruebas:
ORDEN = {"sent": 1, "delivered": 2, "read": 3, "played": 4}
def aplicar_estado(envio: dict, st: dict) -> dict:
cuando = int(st["timestamp"])
if st["status"] == "failed":
err = (st.get("errors") or [{}])[0]
envio.update(failed_at=cuando, error_codigo=err.get("code"))
if envio.get("status_orden", 0) == 0:
envio["status"] = "failed"
return envio
orden = ORDEN.get(st["status"])
if orden is None:
return envio
envio.setdefault(f"{st['status']}_at", cuando)
if orden >= 3:
envio.setdefault("delivered_at", cuando)
if orden > envio.get("status_orden", 0):
envio.update(status=st["status"], status_orden=orden)
return envio
Para probar con eventos reales en tu computadora, mira cómo probar el webhook de WhatsApp en local.
Vincular el estado a tu negocio con biz_opaque_callback_data
Al enviar, puedes incluir biz_opaque_callback_data con un identificador tuyo (por ejemplo "pedido:48213" o "campana:black-friday"). Vuelve en todos los estados de ese mensaje. Así evitas una consulta extra para saber a qué pedido o campaña pertenece el wamid y facilitas los reportes por campaña.
Métricas que salen de los estados
Con las horas guardadas, las métricas más usadas son directas:
| Métrica | Cálculo |
|---|---|
| Tasa de entrega | mensajes con delivered_at o read_at ÷ mensajes enviados |
| Tasa de lectura | mensajes con read_at ÷ mensajes entregados |
| Tiempo hasta la lectura | read_at − sent_at |
| Tasa de falla por código | conteo de error_codigo ÷ mensajes enviados |
| Costo estimado | suma de tarifas por categoria, solo de mensajes con tipo_cobro = regular |
Para métricas de atención (tiempo de primera respuesta, tiempo medio de atención), mira métricas de atención en WhatsApp.
Cómo hacerlo con Vectalk
Vectalk reenvía a tus URL el mismo webhook de estado de Meta, con statuses, pricing y errors en el formato descrito. Basta con registrar la URL con POST https://apis.vectalk.com.br/api/webhooks y responder 200 en menos de 20 segundos.
Además del webhook, Vectalk guarda el historial de mensajes y permite consultarlo por estado o por wamid, lo que ayuda a conciliar eventos que tu sistema pudo haber perdido:
curl "https://apis.vectalk.com.br/api/messages?status=failed&direction=outbound&from=2026-10-01&to=2026-10-25" \
-H "Authorization: Bearer $VECTALK_TOKEN"
curl "https://apis.vectalk.com.br/api/messages?messageId=wamid.HBgNNTIxNTUxMjM0NTY3OBUCABEYEjQ..." \
-H "Authorization: Bearer $VECTALK_TOKEN"
Los filtros aceptados (status, from, to, messageId, direction, page, limit) están en Listar mensajes, y el formato de los eventos en Webhooks (documentación en portugués). El costo de los mensajes llega en la factura de Vectalk, sin tarjeta en Meta. Crea tu cuenta en apps.vectalk.com.br/register.
Preguntas frecuentes
¿Por qué recibí read sin delivered?
Porque el cliente tenía el chat abierto cuando llegó el mensaje. En ese caso, Meta envía solo read. Considera el mensaje entregado.
El mensaje se quedó en sent. ¿Qué pasó?
Salió de los servidores de WhatsApp, pero todavía no llegó al dispositivo, por ejemplo porque el teléfono está apagado o sin conexión. El delivered llega cuando el dispositivo lo recibe. Si después llega failed, el código explica el motivo.
¿Se cobra un mensaje failed?
No. Meta cobra los mensajes entregados. Un mensaje que falló no se entregó.
¿Puedo consultar el estado de un mensaje por la API de Meta en lugar de esperar el webhook?
La Cloud API entrega los estados por webhook, así que tu endpoint tiene que estar disponible y guardando los eventos. En Vectalk, también puedes consultar el historial con el endpoint de listado de mensajes.
¿Cuánto tiempo después del envío puede llegar un estado?
No hay un plazo fijo. El read depende de cuándo el cliente abre el chat, y los reintentos de entrega del webhook pueden durar hasta 7 días si tu endpoint está caído.
Fuentes oficiales consultadas el 25/10/2026: Referencia del webhook de estado, Webhooks: visión general, Códigos de error, Precios, Mensajes de audio, Documentación de Vectalk: Listar mensajes.
