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

# Webhook des accusés de lecture

> Webhook signé envoyé à chaque changement de statut de livraison (envoyé, livré, lu, échoué) d'un message WhatsApp que vous envoyez

<Warning>
  **API Famulor 1.0 (héritée).** Cette page concerne uniquement Famulor 1.0 (`app.famulor.de`) et est conservée pour la compatibilité. Pour la plateforme actuelle, consultez la [référence API Famulor 2.0](/fr/api-reference/introduction).
</Warning>

> Webhook signé envoyé à chaque changement de statut de livraison (envoyé, livré, lu, échoué) d'un message WhatsApp que vous envoyez

Le Webhook des accusés de lecture délivre un rappel HTTP signé à votre serveur chaque fois qu'un message WhatsApp que vous envoyez change de statut — `sent`, `delivered`, `read` ou `failed`. Utilisez-le pour le suivi de livraison, la confirmation de lecture et les pistes d'audit.

Il est configuré **par expéditeur WhatsApp** : différents expéditeurs peuvent donc pointer vers des points de terminaison différents.

## Configuration du webhook

Pour activer les accusés de lecture pour un expéditeur :

1. Modifiez votre [expéditeur WhatsApp](/fr/channels/whatsapp) et ouvrez la section **Read Receipts Webhook**
2. Saisissez votre **Webhook URL** et enregistrez
3. Un **Signing Secret** est généré automatiquement — utilisez-le pour vérifier la signature de chaque requête

Chaque charge utile porte le `whatsapp_message_id` renvoyé par les points de terminaison [Envoyer un modèle de message](/fr/api-v1/whatsapp/send-template) et [Envoyer un message libre](/fr/api-v1/whatsapp/send-freeform), afin que vous puissiez rattacher chaque mise à jour au message d'origine.

## Format de la requête

Le webhook est envoyé sous forme de requête POST vers l'URL que vous avez configurée, avec un corps JSON et un en-tête `X-Signature-256`.

### Structure de la charge utile

<ResponseField name="event" type="string">
  Le type d'événement. Valeur : `message_status`
</ResponseField>

<ResponseField name="whatsapp_message_id" type="integer">
  Identifiant numérique du message — le même `whatsapp_message_id` que celui renvoyé lors de l'envoi du message. Utilisez-le pour corréler la mise à jour de statut avec l'envoi d'origine.
</ResponseField>

<ResponseField name="message_sid" type="string">
  Identifiant de message du fournisseur pour les messages envoyés via Twilio, ou `null`
</ResponseField>

<ResponseField name="meta_message_id" type="string">
  Identifiant de message du fournisseur (`wamid` WhatsApp) pour les messages envoyés via l'API Meta Cloud, ou `null`
</ResponseField>

<ResponseField name="conversation_id" type="string">
  Identifiant unique (UUID) de la conversation à laquelle appartient le message, ou `null`
</ResponseField>

<ResponseField name="assistant_id" type="string">
  Identifiant unique (UUID) de l'assistant connecté à l'expéditeur, ou `null`
</ResponseField>

<ResponseField name="sender" type="object">
  L'expéditeur WhatsApp depuis lequel le message a été envoyé

  <Expandable title="Propriétés de l'expéditeur">
    <ResponseField name="id" type="integer">
      Identifiant numérique de l'expéditeur
    </ResponseField>

    <ResponseField name="phone_number" type="string">
      Le numéro de téléphone WhatsApp de l'expéditeur
    </ResponseField>

    <ResponseField name="display_name" type="string">
      Le nom d'affichage de l'expéditeur
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="to" type="string">
  Le numéro de téléphone du destinataire
</ResponseField>

<ResponseField name="from" type="string">
  Le numéro de téléphone de l'expéditeur
</ResponseField>

<ResponseField name="direction" type="string">
  Sens du message. Valeur : `outbound`
</ResponseField>

<ResponseField name="status" type="string">
  Le nouveau statut de livraison. Valeurs possibles : `sent`, `delivered`, `read`, `failed`, `undelivered`
</ResponseField>

<ResponseField name="error_code" type="integer">
  Code d'erreur du fournisseur lorsque `status` vaut `failed` ou `undelivered`, sinon `null`
</ResponseField>

<ResponseField name="error_message" type="string">
  Message d'erreur brut du fournisseur en cas d'échec du message, sinon `null`
</ResponseField>

<ResponseField name="error_description" type="string">
  Description lisible de l'erreur, sinon `null`
</ResponseField>

<ResponseField name="timestamp" type="string">
  Horodatage ISO 8601 du moment où la plateforme a enregistré le changement de statut, dans le fuseau horaire configuré par le propriétaire du numéro WhatsApp
</ResponseField>

<ResponseField name="provider_timestamp" type="string">
  Horodatage ISO 8601 de l'heure d'événement propre à l'opérateur, dans le fuseau horaire configuré par le propriétaire. Présent pour les messages envoyés via l'API Meta Cloud ; `null` via Twilio (le rappel de statut de Twilio n'inclut pas d'heure d'événement). Préférez ce champ lorsqu'il est présent — c'est l'heure de référence de l'opérateur.
</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>

## Vérification de la signature

Chaque requête inclut un en-tête `X-Signature-256` contenant un HMAC-SHA256 du **corps brut de la requête**, signé avec le **Signing Secret** de votre expéditeur :

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

Recalculez la signature à partir du corps brut et comparez-la à l'aide d'une comparaison à temps constant. Rejetez la requête si elle ne correspond pas.

<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>

## Comportement des nouvelles tentatives

Si votre point de terminaison renvoie un statut non-2xx ou si la requête échoue, la livraison est retentée :

| Tentative     | Délai        |
| ------------- | ------------ |
| 1re tentative | 30 secondes  |
| 2e tentative  | 60 secondes  |
| 3e tentative  | 120 secondes |

Les erreurs serveur (5xx) et les limitations de débit (429) déclenchent une nouvelle tentative. Les erreurs client (4xx) sont considérées comme un point de terminaison mal configuré et ne déclenchent pas de nouvelle tentative.

## Remarques importantes

* Le webhook est configuré **par expéditeur** — chaque expéditeur peut avoir sa propre URL et son propre secret.
* Les événements déclenchés par le bouton **Make test request** dans les paramètres de l'expéditeur incluent un champ `test: true` supplémentaire et utilisent des valeurs d'exemple. Les vraies mises à jour de statut n'incluent jamais `test`.
* `read` ne se déclenche que si le destinataire a activé les accusés de lecture dans ses paramètres de confidentialité WhatsApp. `delivered` se déclenche toujours.
* Les statuts peuvent arriver dans le désordre ou être renvoyés par le fournisseur. Nous ne transmettons que la véritable progression, de sorte que vous ne recevrez pas de `delivered` après un `read` pour le même message — mais vous devriez tout de même traiter le webhook comme la source de vérité et dédupliquer par `whatsapp_message_id` + `status`.
* `timestamp` est toujours l'heure à laquelle la plateforme a enregistré le changement (dans le fuseau horaire du propriétaire du numéro). `provider_timestamp` est l'heure de référence de l'opérateur lorsqu'elle est disponible — préférez-la pour plus de précision, et repliez-vous sur `timestamp` lorsqu'elle vaut `null`.
* Utilisez **Regenerate** dans les paramètres de l'expéditeur pour faire tourner le secret de signature s'il venait à être exposé.
