Voir en Markdown

Webhooks de statut

Déclare une URL dans ton tableau de bord, rubrique Développeurs. Chaque changement d'état d'un message que tu as envoyé y est poussé.

Une URL par clé : ton tunnel local sur la clé de test, ton serveur sur la clé de production. Tu n'as jamais à basculer un réglage entre deux essais.

La charge utile

{
  "event": "whatsapp.message.status",
  "id": "evt_9f2c1a3b4d5e6f708192",
  "created_at": "2026-09-02T10:11:12.000Z",
  "data": {
    "message_id": "wamid.HBgMMjI1MDcwMDAwMDAw",
    "status": "delivered",
    "recipient": "+2250700000000",
    "client_ref": "facture-2026-0912",
    "error": null,
    "error_code": null
  }
}

status vaut sent, delivered, read ou failed. Sur un échec, error_code porte le code Meta.

client_ref est la référence que tu as passée à l'envoi, renvoyée telle quelle.

En-têtes

En-têteContenu
X-Fiitsa-Signature-256sha256=<hmac>
X-Fiitsa-TimestampHorodatage Unix en secondes
X-Fiitsa-DeliveryIdentifiant unique de l'envoi
X-Fiitsa-Eventwhatsapp.message.status

Vérifier la signature

La signature est le HMAC-SHA256 de "{horodatage}.{corps brut}", avec le secret affiché dans ton tableau de bord.

L'horodatage fait partie de la chaîne signée. C'est ce qui rend un rejeu détectable : sans lui, un envoi capturé resterait indéfiniment rejouable. Rejette tout événement de plus de cinq minutes.

const crypto = require("crypto");

app.post("/webhooks/fiitsa", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.get("X-Fiitsa-Signature-256");
  const horodatage = req.get("X-Fiitsa-Timestamp");

  if (Math.abs(Date.now() / 1000 - Number(horodatage)) > 300) return res.sendStatus(401);

  const attendu = "sha256=" + crypto
    .createHmac("sha256", process.env.FIITSA_WEBHOOK_SECRET)
    .update(horodatage + "." + req.body.toString())
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(attendu))) {
    return res.sendStatus(401);
  }

  const evenement = JSON.parse(req.body.toString());
  res.sendStatus(200);
});

Utilise une comparaison à temps constant, pas === : une comparaison naïve fuit la signature attendue, caractère par caractère, par le temps de réponse.

Livraison

Une seule tentative, sans reprise. Réponds vite avec un code 2xx et fais ton traitement ensuite : un serveur lent est traité comme un serveur en échec.

Si tu manques un événement, l'état du message reste interrogeable par l'API.

Tester

Le bouton Envoyer un test du tableau de bord poste un événement réel et signé sur ton URL, puis affiche ce que ton serveur a répondu : code HTTP, latence, corps de la réponse. La charge utile est identique à celle de production, à un champ "test": true près.

Contraintes sur l'URL

HTTPS obligatoire, sur un domaine public. Les adresses IP littérales, localhost et les suffixes internes sont refusés à l'enregistrement. Les redirections ne sont pas suivies.