> ## Documentation Index
> Fetch the complete documentation index at: https://docs.famulor.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook de confirmaciones de lectura

> Webhook firmado que se envía en cada cambio de estado de entrega (sent, delivered, read, failed) de un mensaje de WhatsApp que envías

<Warning>
  **API de Famulor 1.0 (legado).** Esta página se aplica únicamente a Famulor 1.0 (`app.famulor.de`) y se conserva por compatibilidad. Para la plataforma actual, usa la [referencia de la API de Famulor 2.0](/es/api-reference/introduction).
</Warning>

> Webhook firmado que se envía en cada cambio de estado de entrega (sent, delivered, read, failed) de un mensaje de WhatsApp que envías

El Webhook de confirmaciones de lectura envía un callback HTTP firmado a tu servidor cada vez que cambia el estado de un mensaje de WhatsApp que has enviado — `sent`, `delivered`, `read` o `failed`. Úsalo para el seguimiento de entregas, la confirmación de lectura y los registros de auditoría.

Se configura **por remitente de WhatsApp**, de modo que distintos remitentes pueden apuntar a endpoints diferentes.

## Configuración del webhook

Para activar las confirmaciones de lectura de un remitente:

1. Edita tu [remitente de WhatsApp](/es/channels/whatsapp) y abre la sección **Webhook de confirmaciones de lectura**
2. Introduce tu **URL de webhook** y guarda
3. Se genera automáticamente un **secreto de firma** — úsalo para verificar la firma de cada solicitud

Cada payload incluye el `whatsapp_message_id` devuelto por los endpoints [Enviar mensaje de plantilla](/es/api-v1/whatsapp/send-template) y [Enviar mensaje de texto libre](/es/api-v1/whatsapp/send-freeform), de modo que puedas asociar cada actualización con el mensaje original.

## Formato de la solicitud

El webhook se envía como una solicitud POST a tu URL configurada con un cuerpo JSON y un encabezado `X-Signature-256`.

### Estructura del payload

<ResponseField name="event" type="string">
  El tipo de evento. Valor: `message_status`
</ResponseField>

<ResponseField name="whatsapp_message_id" type="integer">
  Identificador numérico del mensaje — el mismo `whatsapp_message_id` devuelto al enviar el mensaje. Úsalo para correlacionar la actualización de estado con el envío original.
</ResponseField>

<ResponseField name="message_sid" type="string">
  Identificador del mensaje del proveedor para mensajes enviados a través de Twilio, o `null`
</ResponseField>

<ResponseField name="meta_message_id" type="string">
  Identificador del mensaje del proveedor (`wamid` de WhatsApp) para mensajes enviados a través de la Meta Cloud API, o `null`
</ResponseField>

<ResponseField name="conversation_id" type="string">
  Identificador único (UUID) de la conversación a la que pertenece el mensaje, o `null`
</ResponseField>

<ResponseField name="assistant_id" type="string">
  Identificador único (UUID) del asistente conectado al remitente, o `null`
</ResponseField>

<ResponseField name="sender" type="object">
  El remitente de WhatsApp desde el que se envió el mensaje

  <Expandable title="Propiedades del remitente">
    <ResponseField name="id" type="integer">
      Identificador numérico del remitente
    </ResponseField>

    <ResponseField name="phone_number" type="string">
      El número de teléfono de WhatsApp del remitente
    </ResponseField>

    <ResponseField name="display_name" type="string">
      El nombre visible del remitente
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="to" type="string">
  El número de teléfono del destinatario
</ResponseField>

<ResponseField name="from" type="string">
  El número de teléfono del remitente
</ResponseField>

<ResponseField name="direction" type="string">
  Dirección del mensaje. Valor: `outbound`
</ResponseField>

<ResponseField name="status" type="string">
  El nuevo estado de entrega. Valores posibles: `sent`, `delivered`, `read`, `failed`, `undelivered`
</ResponseField>

<ResponseField name="error_code" type="integer">
  Código de error del proveedor cuando `status` es `failed` o `undelivered`; en caso contrario, `null`
</ResponseField>

<ResponseField name="error_message" type="string">
  Mensaje de error sin procesar del proveedor cuando el mensaje falló; en caso contrario, `null`
</ResponseField>

<ResponseField name="error_description" type="string">
  Descripción legible para humanos del error; en caso contrario, `null`
</ResponseField>

<ResponseField name="timestamp" type="string">
  Marca de tiempo ISO 8601 de cuándo la plataforma registró el cambio de estado, en la zona horaria configurada del propietario del número de WhatsApp
</ResponseField>

<ResponseField name="provider_timestamp" type="string">
  Marca de tiempo ISO 8601 de la hora del evento según el propio operador, en la zona horaria configurada del propietario. Presente en los mensajes enviados a través de la Meta Cloud API; `null` en Twilio (el callback de estado de Twilio no incluye una hora de evento). Prefiere este valor cuando esté presente — es la hora autorizada por el operador.
</ResponseField>

<ResponseExample>
  ```json Delivered theme={null} theme={null}
  {
    "event": "message_status",
    "whatsapp_message_id": 890,
    "message_sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "meta_message_id": null,
    "conversation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "assistant_id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
    "sender": {
      "id": 42,
      "phone_number": "+19876543210",
      "display_name": "My Business"
    },
    "to": "+1234567890",
    "from": "+19876543210",
    "direction": "outbound",
    "status": "delivered",
    "error_code": null,
    "error_message": null,
    "error_description": null,
    "timestamp": "2026-06-08T09:30:02+00:00",
    "provider_timestamp": "2026-06-08T09:30:00+00:00"
  }
  ```

  ```json Read theme={null} theme={null}
  {
    "event": "message_status",
    "whatsapp_message_id": 890,
    "message_sid": null,
    "meta_message_id": "wamid.HBgLMTIzNDU2Nzg5MBUCABEYEjk...",
    "conversation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "assistant_id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
    "sender": {
      "id": 42,
      "phone_number": "+19876543210",
      "display_name": "My Business"
    },
    "to": "+1234567890",
    "from": "+19876543210",
    "direction": "outbound",
    "status": "read",
    "error_code": null,
    "error_message": null,
    "error_description": null,
    "timestamp": "2026-06-08T09:31:12+00:00",
    "provider_timestamp": "2026-06-08T09:31:10+00:00"
  }
  ```

  ```json Failed theme={null} theme={null}
  {
    "event": "message_status",
    "whatsapp_message_id": 891,
    "message_sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "meta_message_id": null,
    "conversation_id": null,
    "assistant_id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
    "sender": {
      "id": 42,
      "phone_number": "+19876543210",
      "display_name": "My Business"
    },
    "to": "+1234567890",
    "from": "+19876543210",
    "direction": "outbound",
    "status": "failed",
    "error_code": 63016,
    "error_message": "Template message 24h window expired",
    "error_description": "Template message 24h window expired - customer must reply first",
    "timestamp": "2026-06-08T09:32:00+00:00",
    "provider_timestamp": null
  }
  ```
</ResponseExample>

## Verificación de la firma

Cada solicitud incluye un encabezado `X-Signature-256` que contiene un HMAC-SHA256 del **cuerpo de la solicitud sin procesar**, firmado con el **secreto de firma** de tu remitente:

```
X-Signature-256: sha256=<hmac_sha256(raw_body, signing_secret)>
```

Vuelve a calcular la firma sobre el cuerpo sin procesar y compárala usando una comparación de tiempo constante. Rechaza la solicitud si no coincide.

<CodeGroup>
  ```javascript Node.js theme={null} theme={null}
  import crypto from "crypto";

  function isValid(rawBody, signatureHeader, secret) {
    const expected =
      "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signatureHeader || "")
    );
  }
  ```

  ```php PHP theme={null} theme={null}
  function isValid(string $rawBody, ?string $signatureHeader, string $secret): bool
  {
      $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);

      return hash_equals($expected, (string) $signatureHeader);
  }
  ```
</CodeGroup>

## Comportamiento de los reintentos

Si tu endpoint devuelve un estado que no es 2xx o la solicitud falla, la entrega se reintenta:

| Intento        | Retraso      |
| -------------- | ------------ |
| 1.er reintento | 30 segundos  |
| 2.º reintento  | 60 segundos  |
| 3.er reintento | 120 segundos |

Los errores del servidor (5xx) y los límites de frecuencia (429) se reintentan. Los errores del cliente (4xx) se consideran un endpoint mal configurado y no se reintentan.

## Notas importantes

* El webhook se configura **por remitente** — cada remitente puede tener su propia URL y su propio secreto.
* Los eventos activados por el botón **Realizar solicitud de prueba** en los ajustes del remitente incluyen un campo adicional `test: true` y usan valores de ejemplo. Las actualizaciones de estado reales nunca incluyen `test`.
* `read` solo se dispara si el destinatario tiene activadas las confirmaciones de lectura en su configuración de privacidad de WhatsApp. `delivered` siempre se dispara.
* Los estados pueden llegar fuera de orden o ser reenviados por el proveedor. Solo reenviamos un avance genuino, así que no recibirás un `delivered` después de un `read` para el mismo mensaje — pero aun así deberías tratar el webhook como la fuente de verdad y deduplicar por `whatsapp_message_id` + `status`.
* `timestamp` es siempre la hora en que la plataforma registró el cambio (en la zona horaria del propietario del número). `provider_timestamp` es la hora de evento autorizada por el operador cuando está disponible — prefiérela por su precisión, y recurre a `timestamp` cuando sea `null`.
* Usa **Regenerar** en los ajustes del remitente para rotar el secreto de firma si alguna vez queda expuesto.
