Guides

Webhooks

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

Mis à jour le 12 novembre 2026Voir en Markdown

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#

bash
curl -X POST "$INBOX_API/webhook-endpoints" \
  -H "Authorization: Bearer $INBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.exemple.com/webhooks/inbox",
    "events": [
      "message.received",
      "message.delivered",
      "message.failed"
    ]
  }'
Réponse · 201 Created
{
  "id": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
  "url": "https://api.exemple.com/webhooks/inbox",
  "events": [
    "message.received",
    "message.delivered",
    "message.failed"
  ],
  "status": "active",
  "consecutiveFailures": 0,
  "disabledReason": null,
  "createdAt": "2026-11-12T08:33:45.704Z",
  "secret": "whsec_b4ac869893324714f88bfd25026b3e2e2891bb8c087"
}

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#

bash
curl -X POST "$INBOX_API/webhook-endpoints/ID/test" \
  -H "Authorization: Bearer $INBOX_KEY"
Réponse · 200 OK
{
  "ok": true,
  "statusCode": 200,
  "durationMs": 3,
  "error": null
}

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#

TypeQuandContenu de data
message.receivedUn client vous écritmessageId, waMessageId, phoneNumberId, from, type, content, contactName, occurredAt
message.sentLe message est remis à MetamessageId, waMessageId, phoneNumberId, to, status, occurredAt
message.deliveredRemis à l'appareil du destinataireidem, plus pricing si Meta le fournit
message.readLu par le destinataireidem
message.failedRefusé ou non remismessageId, phoneNumberId, to, status, occurredAt, error (code, message)
template.status_updatedMeta approuve, rejette ou met en pause un templatetemplateId, 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#

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êteValeur
BrefPoint-Signaturet=<secondes>,v1=<signature> : voir ci-dessous
BrefPoint-Event-TypeLe type de l'événement
BrefPoint-Delivery-IdIdentifiant de cette livraison
Content-Typeapplication/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.

verify-webhook.mjs
// Vérification d'un webhook Bref Point Inbox — à copier dans votre serveur (Node.js ≥ 18, sans dépendance).
//
// L'en-tête `BrefPoint-Signature` a la forme : t=<secondes>,v1=<hmac-sha256-hex>
// La signature porte sur la chaîne  "<t>.<corps brut>"  avec le secret `whsec_…` de la destination.
import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * @param {string|Buffer} rawBody  le corps EXACT reçu (avant tout JSON.parse)
 * @param {string} header          valeur de l'en-tête BrefPoint-Signature
 * @param {string} secret          secret whsec_… de la destination
 * @param {number} toleranceSeconds âge maximal accepté (protège du rejeu), 300 s par défaut
 */
export function verifyWebhook(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1, 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}

// Exemple avec Express :
//   app.post('/webhooks/inbox', express.raw({ type: 'application/json' }), (req, res) => {
//     if (!verifyWebhook(req.body, req.get('BrefPoint-Signature'), process.env.INBOX_WEBHOOK_SECRET)) return res.sendStatus(401);
//     const event = JSON.parse(req.body);      // { id, type, createdAt, data }
//     // `event.id` est stable : s'il revient (nouvel essai, rejeu), ignorez-le s'il est déjà traité.
//     res.sendStatus(200);                     // répondre vite (2xx), traiter ensuite
//   });

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

bash
curl "$INBOX_API/webhook-deliveries/ID" \
  -H "Authorization: Bearer $INBOX_KEY"
Réponse · 200 OK
{
  "id": "473dfe39-7555-4942-8a5b-d2ec99026b44",
  "endpointId": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
  "eventId": "2cbc3e2b-fba4-4246-868d-76ffe9fcb829",
  "eventType": "message.sent",
  "status": "succeeded",
  "attempts": 1,
  "lastStatusCode": 200,
  "lastError": null,
  "replayOf": null,
  "createdAt": "2026-11-12T08:33:46.267Z",
  "deliveredAt": "2026-11-12T08:33:46.324Z",
  "payload": {
    "data": {
      "to": "237611111111",
      "status": "sent",
      "messageId": "a9691d7a-ee51-4da9-87b2-24cce79a9313",
      "occurredAt": "2026-11-12T08:33:46.251Z",
      "waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87"
    },
    "createdAt": "2026-11-12T08:33:46.247Z"
  },
  "attemptsDetail": [
    {
      "attempt": 1,
      "statusCode": 200,
      "error": null,
      "durationMs": 0,
      "responseSnippet": "ok",
      "at": "2026-11-12T08:33:46.322Z"
    }
  ]
}

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 :

bash
curl -X POST "$INBOX_API/webhook-deliveries/ID/replay" \
  -H "Authorization: Bearer $INBOX_KEY"
Réponse · 202 Accepted
{
  "id": "13980898-c6ac-4b7c-8e78-f68fd741cfb9",
  "endpointId": "bc06ae32-7564-46d5-89fb-a9869019aa8e",
  "eventId": "2cbc3e2b-fba4-4246-868d-76ffe9fcb829",
  "eventType": "message.sent",
  "status": "pending",
  "attempts": 0,
  "lastStatusCode": null,
  "lastError": null,
  "replayOf": "473dfe39-7555-4942-8a5b-d2ec99026b44",
  "createdAt": "2026-11-12T08:33:46.391Z",
  "deliveredAt": null
}

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.