# Webhooks

> Recevoir les événements en temps réel, vérifier leur signature, comprendre les reprises et rejouer une livraison.

Les webhooks vous notifient **dès qu'il se passe quelque chose** : message reçu, envoyé, délivré, lu, échoué, template approuvé ou rejeté. Bref Point Inbox envoie une requête `POST` JSON à l'adresse que vous déclarez.

## Déclarer une destination

<ApiExample method="POST" path="/webhook-endpoints" />

<Callout type="warning" title="Le secret n'est affiché qu'une seule fois">
Le champ `secret` (`whsec_…`) sert à **vérifier la signature** de chaque événement. Conservez-le dans vos variables d'environnement. Il se renouvelle avec `POST /webhook-endpoints/{id}/rotate-secret` ; l'ancien cesse alors de fonctionner.
</Callout>

L'URL doit être en **`https://`** et joignable publiquement. Les adresses `http://`, `localhost`, les réseaux privés et les noms internes sont refusés. Pour développer en local, exposez votre serveur par un tunnel HTTPS public.

Les destinations sont propres à un **environnement** : celles du sandbox ne reçoivent jamais d'événements de production.

### Vérifier la destination

<ApiExample method="POST" path="/webhook-endpoints/{id}/test" />

Nous envoyons un événement `webhook.test` signé comme les autres et vous renvoyons la réponse de votre serveur (`ok`, code HTTP, durée, ou l'erreur de connexion).

## Les événements

| Type | Quand | Contenu de `data` |
|---|---|---|
| `message.received` | Un client vous écrit | `messageId`, `waMessageId`, `phoneNumberId`, `from`, `type`, `content`, `contactName`, `occurredAt` |
| `message.sent` | Le message est remis à Meta | `messageId`, `waMessageId`, `phoneNumberId`, `to`, `status`, `occurredAt` |
| `message.delivered` | Remis à l'appareil du destinataire | idem, plus `pricing` si Meta le fournit |
| `message.read` | Lu par le destinataire | idem |
| `message.failed` | Refusé ou non remis | `messageId`, `phoneNumberId`, `to`, `status`, `occurredAt`, `error` (`code`, `message`) |
| `template.status_updated` | Meta approuve, rejette ou met en pause un template | `templateId`, `wabaConnectionId`, `name`, `language`, `status`, `previousStatus`, `reason` |

Abonnez une destination à `*` pour tout recevoir, ou à une liste précise. `PATCH /webhook-endpoints/{id}` modifie l'abonnement.

### Le corps reçu

```json title="POST sur votre URL"
{
  "id": "8dc9eca4-2eff-46de-80aa-0ac9674dd1ae",
  "type": "message.delivered",
  "createdAt": "2026-11-12T06:09:17.388Z",
  "data": { "messageId": "7a6d4b4c-…", "status": "delivered", "occurredAt": "2026-11-12T06:09:19.002Z" }
}
```

Chaque requête porte aussi ces en-têtes :

| En-tête | Valeur |
|---|---|
| `BrefPoint-Signature` | `t=<secondes>,v1=<signature>` : voir ci-dessous |
| `BrefPoint-Event-Type` | Le type de l'événement |
| `BrefPoint-Delivery-Id` | Identifiant de cette livraison |
| `Content-Type` | `application/json` |

## Vérifier la signature

**Vérifiez toujours la signature** : sans elle, n'importe qui pourrait appeler votre adresse en se faisant passer pour Bref Point Inbox.

La signature `v1` est le **HMAC-SHA256**, en hexadécimal, de la chaîne `"<t>.<corps brut>"`, avec votre secret `whsec_…`. `t` est l'horodatage (en secondes) de l'envoi : refusez les envois trop anciens (5 minutes par défaut) pour vous protéger du rejeu.

<Tabs items={["Node.js", "Python", "PHP"]}>
<Tab>
<CodeFile src="verify-webhook.mjs" lang="javascript" />
</Tab>
<Tab>
<CodeFile src="verify-webhook.py" lang="python" />
</Tab>
<Tab>
<CodeFile src="verify-webhook.php" lang="php" />
</Tab>
</Tabs>

<Callout type="warning" title="Le corps brut, pas le JSON ré-encodé">
La signature porte sur les **octets exacts reçus**. Calculez-la **avant** tout `JSON.parse` / `json_decode` : un JSON ré-encodé (espaces, ordre des clés, accents) donne une autre signature. Avec Express, utilisez `express.raw({ type: "application/json" })` sur cette route.
</Callout>

Ces trois exemples sont **exécutés par les tests de Bref Point Inbox** contre la vraie signature : ce que vous copiez ici fonctionne.

## Fiabilité des livraisons

- **Répondez vite** par un code `2xx` (délai maximal : **10 secondes**), puis traitez. Une redirection (`3xx`) est un échec.
- Un échec est **réessayé jusqu'à 8 fois** au total, avec une attente qui double : environ 30 s, 1 min, 2 min, 4 min, 8 min, 16 min, puis 32 min (soit à peu près **une heure**).
- Les événements sont livrés **au moins une fois** : l'`id` est stable d'une tentative à l'autre, **dédoublonnez dessus**.
- L'**ordre n'est pas garanti** : un `message.read` peut précéder un `message.delivered`. Fiez-vous à `occurredAt`, et ne revenez jamais en arrière sur un statut.
- Après **15 livraisons définitivement échouées d'affilée**, la destination est **désactivée** (`status: "disabled"`, avec la raison). Réactivez-la avec `PATCH … { "enabled": true }` une fois votre récepteur réparé.

## Historique et rejeu

Chaque livraison et chaque tentative sont conservées (selon la rétention de votre plan).

<ApiExample method="GET" path="/webhook-deliveries/{id}" />

`attemptsDetail` donne, pour chaque tentative, le code de réponse, la durée, l'erreur éventuelle et un extrait de la réponse de votre serveur : de quoi comprendre un échec sans deviner.

Votre récepteur était en panne ? Une fois réparé, **rejouez** :

<ApiExample method="POST" path="/webhook-deliveries/{id}/replay" />

Le rejeu crée une **nouvelle livraison** du même événement (même `id` d'événement) vers la même destination ; `replayOf` pointe vers l'originale. Il est refusé (`409`) si la livraison est encore en cours ou si la destination est désactivée.
