← Blog

·

Estados de mensaje en la API de WhatsApp: sent, delivered, read y failed por webhook

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:

EstadoSignificadoEn la app del cliente
sentEl mensaje salió de los servidores de WhatsAppUna palomita
deliveredEl mensaje llegó al dispositivo del destinatarioDos palomitas
readEl mensaje se mostró en un chat abierto en el dispositivoDos palomitas azules
playedPrimera reproducción de un mensaje de vozMicrófono azul
failedEl mensaje no se pudo enviar o entregarTriángulo rojo de error

Dos detalles de la documentación cambian la forma de procesarlos:

  • read puede llegar sin delivered. Si el cliente tiene el chat abierto cuando llega el mensaje, Meta omite el webhook de delivered y envía solo read. Trata read como «entregado y leído».
  • played solo 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:

CampoCuándo aparecePara qué sirve
idSiempreEl wamid del mensaje enviado
statusSiempreUno de los cinco valores anteriores
timestampSiempreMomento (Unix, en segundos) en que se generó el evento
recipient_idSiempreNúmero del destinatario (o ID del grupo, en grupos)
biz_opaque_callback_dataSi enviaste ese campo en el mensajeDevuelve un identificador tuyo, como el ID del pedido o de la campaña
pricingEn sent y en uno de los dos, delivered o readCategoría y tipo de cobro
errorsSolo en failedCó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, como marketing, utility, authentication o service.
  • type: regular (cobrado), free_customer_service, free_entry_point (enviado en la ventana gratuita que abre un anuncio de clic a WhatsApp) o free_group_customer_service.
  • billable: Meta lo marca como obsoleto. Usa type y category.

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ódigoCausaAcción
131026Destinatario sin WhatsApp, sin aceptar los términos más recientes o con la app desactualizadaConfirma el número; no reintentes en bucle
131047Pasaron más de 24 horas desde el último mensaje del clienteEnvía una plantilla
131049No entregado para cuidar el engagement (límite de marketing por usuario)Espera al menos 24 horas; mira límite de marketing por usuario
131050El cliente dejó de recibir marketing de la empresaMárcalo como opt-out; no reintentes
131053Falló la carga del archivo, en general por tipo no compatibleRevisa el MIME del archivo
131056Demasiados mensajes al mismo destinatario en poco tiempoEspera antes de volver a escribirle a esa persona
132015Plantilla pausada por baja calidadEdita la plantilla y espera la aprobación
131000Error desconocidoReintenta; 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 read antes que delivered, o delivered después de read. Nunca sobrescribas un estado más avanzado con uno anterior.
  • El mismo estado puede llegar dos veces. Usa id + status como clave de idempotencia.
  • Un estado puede llegar horas o días después si tu endpoint estuvo caído. Guarda el timestamp del evento, no la hora en que lo procesaste.
  • delivered puede no llegar nunca, por ejemplo si el teléfono del cliente sigue apagado. No trates «sin delivered en 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_orden solo aumenta; un delivered atrasado no borra un read.
  • Idempotente. Guardar el mismo evento dos veces da el mismo resultado, gracias al COALESCE.
  • read implica entrega. Si se omitió el delivered, delivered_at se completa con la hora del read.
  • Una falla no borra un éxito. Si la fila ya tiene un estado positivo, el failed se 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étricaCálculo
Tasa de entregamensajes con delivered_at o read_at ÷ mensajes enviados
Tasa de lecturamensajes con read_at ÷ mensajes entregados
Tiempo hasta la lecturaread_at − sent_at
Tasa de falla por códigoconteo de error_codigo ÷ mensajes enviados
Costo estimadosuma 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.

Integre o WhatsApp oficial ao seu sistema

A Vectalk entrega a Cloud API da Meta com os mesmos payloads e webhooks, um token para todas as contas e documentação completa.