Fonctionnement du pipeline de messages WhatsApp entrants
La plupart des tutoriels commencent par du code avant d'expliquer l'architecture. C'est pourquoi la plupart des implémentations contiennent des bugs. Voici le pipeline complet — chaque saut effectué par un message avant d'atteindre votre logique applicative :
Quatre conditions doivent être remplies pour que ce pipeline fonctionne de manière fiable :
- Votre point de terminaison est accessible publiquement via HTTPS. Pas de localhost, pas de HTTP. Meta rejette les URL non-HTTPS et ne peut pas atteindre les IP privées.
- Votre point de terminaison répond avec 200 en moins de 20 secondes. Toute réponse plus lente entraîne une nouvelle tentative de livraison de la part de Meta. Si les tentatives se répètent suffisamment, Meta cesse entièrement d'envoyer vers votre point de terminaison.
- Vous vérifiez la signature HMAC-SHA256 sur chaque requête. Sans cela, tout attaquant qui découvre l'URL de votre webhook peut envoyer de faux messages à votre système.
- Vous acquittez d'abord, vous traitez ensuite. Renvoyez 200 immédiatement, poussez la charge utile vers une file d'attente, traitez de façon asynchrone. Ne faites jamais de travail lourd de manière synchrone dans le gestionnaire de webhook.
Étape 1 : Construire le point de terminaison du webhook
Votre point de terminaison doit gérer deux méthodes HTTP sur la même URL : GET (défi de vérification unique de Meta) et POST (événements de messages en direct). Voici l'implémentation minimale fonctionnelle en Node.js et Python.
Étape 2 : Gérer correctement le défi de vérification
Lorsque vous enregistrez l'URL de votre webhook dans le tableau de bord des développeurs Meta, Meta envoie une requête GET unique pour vérifier que vous contrôlez le point de terminaison. C'est l'étape qui fait échouer presque chaque première implémentation — généralement parce que les développeurs renvoient du JSON au lieu de texte brut, ou renvoient l'objet params complet au lieu de la seule valeur du défi.
La requête GET de Meta inclut trois paramètres de requête :
hub.mode— toujours la chaînesubscribehub.verify_token— la chaîne que vous définissez dans le tableau de bord Meta. Vous choisissez cette valeur — faites-en un secret aléatoire, pas un mot prévisiblehub.challenge— un nombre aléatoire que Meta veut recevoir en retour sous forme de texte brut
Votre point de terminaison doit : (1) vérifier que hub.mode === 'subscribe', (2) vérifier que hub.verify_token correspond à votre valeur attendue, (3) renvoyer hub.challenge comme corps de réponse avec Content-Type: text/plain et un statut 200. Renvoyez du JSON, renvoyez la mauvaise valeur, ou renvoyez un statut non-200 — la vérification échoue et Meta marque votre webhook comme non vérifié.
res.json({ challenge }) dans Express au lieu de res.send(challenge). Meta attend du texte brut. Envelopper le défi dans du JSON fait échouer la vérification même si votre serveur renvoie 200.Étape 3 : Vérification de signature HMAC-SHA256 — l'étape que personne ne saute en production
L'URL de votre webhook est publique. Quiconque la trouve peut envoyer de fausses charges utiles via POST à votre serveur. Sans vérification de signature, votre agent IA pourrait recevoir des conversations fabriquées, votre CRM pourrait être empoisonné avec des contacts inventés, et votre logique pourrait être déclenchée par un attaquant à volonté.
Meta signe chaque requête POST en utilisant votre App Secret (trouvé dans Meta Developer Dashboard → App Settings → Basic). La signature se trouve dans l'en-tête X-Hub-Signature-256, formatée comme sha256=<hex_digest>. Le digest est un HMAC-SHA256 des octets bruts du corps de la requête, en utilisant votre App Secret comme clé.
JSON.parse() avant de calculer le HMAC, le digest ne correspondra jamais — la sérialisation JSON peut modifier les espaces et l'ordre des clés. Vous devez calculer le HMAC sur les octets exacts qui sont arrivés dans le corps de la requête. Dans Express, cela signifie configurer express.raw() sur votre route webhook avant tout autre analyseur de corps. Dans FastAPI, appelez await request.body() avant toute désérialisation JSON.Étape 4 : Développement local — exposer localhost à Meta
Meta ne peut pas atteindre http://localhost:3000. Pendant le développement, vous avez besoin d'une URL HTTPS publique qui tunnelise le trafic vers votre serveur local. Deux options fiables :
Enregistrez l'URL du tunnel dans le tableau de bord des développeurs Meta (WhatsApp → Configuration → Webhooks). Lorsque vous redémarrez ngrok, il génère une nouvelle URL — mettez à jour le tableau de bord à chaque fois. Cloudflare Tunnel persiste entre les redémarrages pour les tunnels nommés. Basculez vers l'URL de votre domaine de production avant la mise en ligne.
Étape 5 : Enregistrer le webhook dans le tableau de bord des développeurs Meta
Avec votre point de terminaison en cours d'exécution et accessible publiquement :
- Ouvrez developers.facebook.com → votre application → WhatsApp → Configuration
- Sous Webhooks, cliquez sur Edit
- Collez l'URL HTTPS de votre webhook (par ex.
https://yourdomain.com/webhook) - Entrez votre Verify Token — la chaîne exacte que vous avez définie dans
VERIFY_TOKEN - Cliquez sur Verify and Save — Meta lance immédiatement le défi GET. Votre serveur doit répondre en ~5 secondes.
- Après l'enregistrement, cliquez sur Manage à côté du webhook et activez l'abonnement au champ messages. Sans cela, Meta ne poussera pas les événements de messages entrants vers votre point de terminaison.
Étape 6 : Analyser la charge utile du webhook Cloud API
Une fois la vérification terminée, chaque message client entrant déclenche un POST vers votre point de terminaison. Voici la structure complète d'un événement de message texte Cloud API — et comment le parcourir :
Le message qui vous intéresse est enfoui à body.entry[0].changes[0].value.messages[0]. Cette imbrication prend tout le monde au dépourvu. Un événement de mise à jour de statut (accusé de réception) arrive dans la même enveloppe mais contient un tableau statuses au lieu de messages. Voici le motif d'extraction :
Étape 7 : Gérer tous les types de messages WhatsApp
Le champ type vous indique quelle propriété lire. Chaque type a une structure différente. Voici la carte complète :
msg.text.bodymsg.image.id → téléchargervoice: true si enregistré dans l'application.msg.audio.id → téléchargermsg.document.id, .filenamemsg.video.id → téléchargermsg.location.latitude, .longitudemsg.contacts[0].namemsg.reaction.emoji, .message_idmsg.interactive.button_reply.idÉtape 8 : Télécharger les médias entrants — la récupération en deux étapes
Lorsqu'un client envoie une image, une note vocale ou un document, la charge utile du webhook ne contient pas le fichier — elle contient un ID de média. Vous devez effectuer un appel API séparé pour résoudre l'ID en une URL de téléchargement temporaire, puis récupérer le fichier réel. L'URL de téléchargement expire après 5 minutes — téléchargez immédiatement et stockez sur votre propre infrastructure.
Étape 9 : Architecture de file d'attente — ne jamais traiter dans le gestionnaire de webhook
C'est le modèle de production qui sépare une implémentation jouet d'une implémentation qui survit au trafic réel. La règle est absolue : votre gestionnaire de webhook doit renvoyer 200 en moins de 20 secondes. Si vous appelez un LLM, interrogez un CRM ou téléchargez des médias de manière synchrone dans le gestionnaire, vous finirez par atteindre le délai d'attente sous charge, Meta réessaiera, et vous traiterez des événements en double.
Le modèle correct : acquitter immédiatement, pousser la charge utile brute vers une file d'attente, traiter de façon asynchrone dans un worker. Voici un modèle BullMQ minimal (supporté par Redis) :
msg.id traitées dans Redis avec un TTL court (par ex. 24h). Avant de traiter une tâche, vérifiez si msg.id est déjà dans votre ensemble de déduplication. Si oui, ignorez. Si non, ajoutez-le et traitez. Cela évite les réponses doubles de votre agent IA et les enregistrements en double dans le CRM.Alternative : évitez tout ce qui précède avec SocialHook
Tout ce qui précède — gestion du défi de vérification, signature HMAC-SHA256, extraction de la charge utile depuis entry[0].changes[0].value imbriqué, résolution d'ID de média, logique de réessai, déduplication — est un travail d'infrastructure. Ce n'est pas votre produit. C'est l'échafaudage sur lequel votre produit fonctionne.
SocialHook remplace cette couche entière. Vous connectez votre numéro WhatsApp Business à SocialHook, collez l'URL du webhook de votre serveur dans le tableau de bord SocialHook, et SocialHook :
- Gère le défi de vérification de Meta automatiquement
- Vérifie la signature HMAC-SHA256 sur chaque événement entrant
- Extrait le message de l'enveloppe Cloud API imbriquée
- Normalise la charge utile vers un format cohérent sur WhatsApp, Facebook Messenger et Messages directs Instagram
- Réessaie la livraison vers votre point de terminaison jusqu'à 3 fois avec backoff exponentiel si votre serveur renvoie un statut non-200
- Journalise chaque tentative de livraison avec horodatage, code de statut et latence
- Livre à votre point de terminaison en moins de 50 ms
Votre point de terminaison reçoit ceci au lieu de la charge utile brute imbriquée de Meta :
Pas de parcours imbriqué. Pas de parseInt(timestamp). Pas de préfixe + manquant sur le numéro de l'expéditeur. Pas d'instructions switch spécifiques à une plateforme — la même forme de charge utile arrive que le client vous ait envoyé un message sur WhatsApp, Facebook ou Instagram. Un seul gestionnaire de webhook, trois canaux. Le coût total est un forfait de 50 $/mois — moins d'une heure de temps d'ingénierie pour construire et maintenir l'équivalent from scratch.
Questions fréquentes
express.raw() sur votre route webhook avant tout autre analyseur de corps. Dans FastAPI, appelez await request.body() avant la désérialisation. Vérifiez également que vous utilisez votre App Secret (depuis App Settings → Basic) et non votre jeton d'accès comme clé HMAC — ce sont des valeurs différentes.ngrok http 3000) est le plus courant — il génère une URL HTTPS publique qui redirige vers votre port local. Cloudflare Tunnel (cloudflared tunnel --url http://localhost:3000) est une alternative gratuite sans limite de temps de session. Collez l'URL générée dans le champ webhook du tableau de bord des développeurs Meta. N'oubliez pas de la mettre à jour lorsque vous redémarrez ngrok car l'URL change.body.entry[0].changes[0].value.messages[0]. L'horodatage est une chaîne, l'expéditeur n'a pas de préfixe +, et le format diffère des webhooks Facebook Messenger et Instagram. SocialHook extrait le message, normalise l'expéditeur au format E.164, convertit l'horodatage en entier, et livre la même structure JSON plate sur les trois canaux Meta. Vous écrivez un seul analyseur et cela fonctionne pour WhatsApp, Facebook et Instagram.GET https://graph.facebook.com/v21.0/{media_id} avec votre jeton d'accès pour obtenir une URL de téléchargement temporaire, (2) récupérer le fichier depuis cette URL (également avec votre jeton d'accès dans l'en-tête Authorization), (3) stocker le fichier sur votre propre serveur ou stockage cloud. L'URL temporaire expire en environ 5 minutes — téléchargez immédiatement. Consultez le code Node.js complet dans la section Médias ci-dessus.Connectez votre numéro.
Premier webhook en 5 minutes.
Vous avez lu l'implémentation complète. Si vous préférez ne pas maintenir vous-même la vérification HMAC, la logique de réessai et la normalisation de charge utile — SocialHook gère tout cela. Collez l'URL de votre point de terminaison. Recevez du JSON propre. 50 $/mois forfait.