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ête | Contenu |
|---|---|
X-Fiitsa-Signature-256 | sha256=<hmac> |
X-Fiitsa-Timestamp | Horodatage Unix en secondes |
X-Fiitsa-Delivery | Identifiant unique de l'envoi |
X-Fiitsa-Event | whatsapp.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.