Fondamentaux du webhook
L'API Cloud WhatsApp livre des événements à votre application via un système de webhook basé sur le push. Plutôt que d'interroger un endpoint pour de nouveaux événements, les serveurs de Meta envoient via POST un payload JSON à une URL que vous enregistrez — votre endpoint webhook. Votre serveur traite le payload et renvoie HTTP 200 pour accuser réception.
Deux méthodes HTTP opèrent sur la même URL :
- GET — défi de vérification unique lors de l'enregistrement. Votre endpoint doit renvoyer le paramètre de requête
hub.challengeen texte brut. - POST — notifications d'événements en direct. Chaque message entrant, mise à jour de statut et événement de compte arrive ici.
Les contraintes les plus importantes à internaliser avant d'écrire du code :
- Répondre dans les 20 secondes — tout ce qui est plus lent déclenche un réessai. Accusez réception immédiatement, traitez de façon asynchrone.
- Livraison at-least-once — le même événement peut arriver plusieurs fois. Votre handler doit être idempotent.
- Aucune garantie d'ordre — les événements peuvent arriver hors séquence. Ne supposez jamais un ordre chronologique.
- Limite de taille de payload de 3 Mo — les payloads webhook individuels ne dépasseront pas 3 Mo. Les fichiers médias ne sont pas inclus en ligne.
- HTTPS requis — Meta rejette les endpoints HTTP simples. Certificat SSL valide requis.
Référence complète des propriétés du webhook
| Propriété | Valeur / Spécification | Notes |
|---|---|---|
| Protocole | HTTPS uniquement | Certificat SSL valide requis. HTTP rejeté catégoriquement. |
| Direction | Fournisseur → Consommateur (push) | Meta pousse vers vous. Aucun polling requis. |
| Méthode de vérification | GET + hub.verify_token | Unique lors de l'enregistrement. Renvoyer hub.challenge en texte brut. |
| Authentification sur les événements | X-Hub-Signature-256 HMAC-SHA256 | Signé avec App Secret. Vérifier chaque POST. |
| Méthode HTTP pour les événements | POST | Toujours POST. Jamais GET pour les événements en direct. |
| Format des données | JSON | Content-Type: application/json |
| Délai de réponse | 20 secondes | Dépassement déclenche un réessai. Renvoyer 200 immédiatement. |
| Critère de succès | HTTP 200 | Tout non-200 déclenche le mécanisme de réessai. |
| Limite de taille du payload | 3 Mo | Médias non en ligne — référencés par ID. |
| Durée de réessai | Jusqu'à 7 jours | Backoff exponentiel. Voir le programme complet ci-dessous. |
| Garantie de livraison | At-least-once | Doublons possibles. Rendre le handler idempotent. |
| Garantie d'ordre | Aucune | Les événements peuvent arriver hors ordre chronologique. |
| Niveaux de webhook | Numéro de téléphone + WABA | Le numéro de téléphone a priorité sur le fallback WABA. |
| Relecture manuelle | Non prise en charge | Aucune relecture native. Événements perdus après 7 jours. |
| Algorithme de signature | HMAC-SHA256 | Clé = App Secret. Entrée = octets bruts du body. |
| En-tête de signature | X-Hub-Signature-256 | Format : sha256=<hex> |
| Livraisons concurrentes | Plusieurs par seconde | Les numéros à haut volume reçoivent de nombreux événements/seconde. |
Tous les champs d'abonnement du webhook
Vous vous abonnez à des champs individuels dans le Meta Developer Dashboard (WhatsApp → Configuration → Webhooks → Manage). L'abonnement à messages est obligatoire pour la plupart des intégrations. Chaque champ représente une catégorie d'événements — abonnez-vous uniquement à ce dont vous avez besoin pour réduire le volume de payload.
| Champ | Couverture | Priorité |
|---|---|---|
| messages | Tous les messages clients entrants + toutes les mises à jour de statut sortantes (sent / delivered / read / failed). Le champ principal pour toute intégration de messagerie. | S'abonner toujours |
| account_update | Violations de politique (ACCOUNT_VIOLATION) et restrictions actives (ACCOUNT_RESTRICTION) sur vos numéros de téléphone. Essentiel pour la surveillance en production. |
Recommandé |
| message_template_status_update | Approbation, rejet, mise en pause et désactivation des templates. Se déclenche lorsque Meta change le statut d'un template que vous avez soumis. | Recommandé |
| phone_number_quality_update | Changements de notation de qualité (GREEN / YELLOW / RED) pour vos numéros de téléphone. La qualité affecte votre niveau de messagerie. Abonnez-vous pour détecter la dégradation tôt. | Recommandé |
| phone_number_name_update | Événements d'approbation ou de rejet du nom d'affichage. Se déclenche lorsque Meta examine un nom que vous avez soumis pour votre numéro WhatsApp Business. | Situationnel |
| business_capability_update | Changements de niveau de limite de messagerie — lorsque Meta améliore ou rétrograde votre niveau de conversations quotidiennes (1K / 10K / 100K / Unlimited). | Situationnel |
| flows | Interactions WhatsApp Flows — événements de soumission de données lorsqu'un utilisateur complète ou interagit avec un Flow attaché à votre numéro. | Si vous utilisez Flows |
| security | Événements de PIN de vérification en deux étapes. Se déclenche lorsque le PIN d'un numéro de téléphone est modifié ou désactivé. | Optionnel |
| message_template_components_update | Se déclenche lorsque Meta modifie les composants d'un template approuvé (rare — Meta peut ajuster les templates pour se conformer à la politique). | Optionnel |
| account_alerts | Alertes de facturation, avertissements de capacité et autres avis opérationnels au niveau du compte de Meta. | Optionnel |
Événements livrés par champ d'abonnement
Champ messages — types de messages entrants
Le champ messages livre deux catégories : les messages entrants des clients (identifiés par un tableau messages dans l'objet value) et les mises à jour de statut de livraison sortantes (identifiées par un tableau statuses). Vérifiez quel tableau est présent avant de traiter.
| msg.type | Emplacement du contenu | Notes |
|---|---|---|
| text | msg.text.body | Corps de message en texte brut. Peut inclure des URLs. |
| image | msg.image.id, .mime_type, .caption | ID → résoudre l'URL → télécharger. Légende optionnelle. |
| audio | msg.audio.id, .voice | voice: true si enregistré dans l'application. Toujours télécharger immédiatement. |
| video | msg.video.id, .caption | Légende optionnelle. |
| document | msg.document.id, .filename, .mime_type | Nom de fichier inclus — stockez-le. |
| sticker | msg.sticker.id, .animated | Le drapeau animated distingue WebP vs sticker animé. |
| location | msg.location.latitude, .longitude, .name, .address | Nom et adresse sont optionnels. |
| contacts | msg.contacts[n].name, .phones | Tableau — le client peut partager plusieurs contacts. |
| reaction | msg.reaction.emoji, .message_id | Référence l'ID du message auquel le client a réagi. |
| interactive | msg.interactive.type, puis .button_reply ou .list_reply | Vérifier type avant de lire le sous-objet. |
| order | msg.order.catalog_id, .product_items | WhatsApp Commerce — commande de produit depuis le catalogue. |
| system | msg.system.body, .type | Événements système : le client a changé de numéro, etc. |
| button | msg.button.text, .payload | Clic sur un bouton de réponse rapide depuis un message template. |
| referral | msg.referral.source_url, .source_type, .source_id | Données de référence d'annonce Click-to-WhatsApp accompagnant le message. |
Champ messages — mises à jour de statut sortantes
| valeur de statut | Signification | Champs supplémentaires |
|---|---|---|
| sent | Message accepté par Meta et transféré à WhatsApp. Pas encore livré à l'appareil. | timestamp, recipient_id, conversation, pricing |
| delivered | Le message a atteint l'appareil du destinataire (double coche grise). | timestamp, recipient_id, conversation, pricing |
| read | Le destinataire a ouvert la conversation (double coche bleue). Ne se déclenche que si les accusés de lecture sont activés. | timestamp, recipient_id |
| failed | Échec permanent de la livraison. Vérifiez status.errors[0].code pour l'erreur spécifique. | tableau errors avec code et title |
Programme exact de réessai avec temporisations de backoff
Meta réessaie les livraisons webhook échouées pendant jusqu'à 7 jours en utilisant un backoff exponentiel. « Échoué » signifie que votre endpoint a renvoyé un non-200, a atteint le délai d'attente (a pris plus de 20 secondes) ou était inaccessible. Après 7 jours, l'événement est définitivement rejeté — aucun chemin de récupération n'existe nativement.
Comportement des codes de réponse HTTP
Ce que vous renvoyez à Meta détermine si l'événement est considéré comme livré, réessayé, ou s'il déclenche une alerte. L'arbre de décision est plus simple que la plupart des développeurs ne s'y attendent — tout est binaire : 200 signifie succès, tout le reste signifie réessai.
Spécification complète de la signature HMAC-SHA256
Chaque requête POST de Meta inclut une signature cryptographique qui vous permet de vérifier que la requête provient réellement de Meta et n'a pas été altérée durant le transfert. Ignorer cette vérification signifie que n'importe quel attaquant découvrant l'URL de votre webhook peut envoyer des données arbitraires à votre application.
Format de l'en-tête
Vérification en Node.js
Vérification en Python
JSON.stringify(JSON.parse(body)) produit des octets différents de l'original — les espaces, l'ordre des clés et la précision des nombres peuvent changer. Calculez toujours le HMAC sur les octets exacts reçus dans la requête HTTP, avant toute transformation.Webhooks au niveau WABA vs au niveau Numéro de téléphone
L'API WhatsApp Cloud prend en charge deux niveaux de configuration de webhook qui interagissent selon un ordre de priorité spécifique. Comprendre cela permet d'éviter la perte silencieuse d'événements lors de la gestion de plusieurs numéros.
| Aspect | Webhook Numéro de téléphone | Webhook WABA |
|---|---|---|
| Emplacement config | Meta Developer Dashboard → WhatsApp → Configuration | Meta Business Settings → WhatsApp Accounts → [WABA] → Webhook |
| Portée | Un numéro de téléphone spécifique | Tous les numéros du WABA |
| Priorité | Plus haute — prioritaire | Plus basse — repli uniquement |
| Comportement de repli | Si défini, le webhook WABA ne reçoit pas les événements pour ce numéro | Reçoit les événements pour les numéros sans webhook spécifique |
| Idéal pour | Intégrations à numéro unique, logique de routage par numéro | Opérations multi-numéros, agence gérant de nombreux clients |
| Routage d'événements | Les événements vont uniquement à cette URL | Les événements de tous les numéros non configurés arrivent ici |
| Champs d'abonnement | Configurés séparément | Configurés séparément |
metadata.phone_number_id. N'ajoutez un webhook au niveau Numéro de téléphone que pour les numéros nécessitant un routage différent. C'est plus propre que de maintenir des webhooks séparés par client, et SocialHook prend en charge plusieurs numéros par compte sous un flux d'événements unique et normalisé.Livraison "At-least-once" — implications et déduplication
Le système de webhooks de Meta garantit qu'un événement donné sera livré au moins une fois. Il ne garantit pas une livraison exactement une fois. Scénario de doublon : votre serveur traite un événement, renvoie un code 200, mais l'accusé de réception est perdu durant le transport avant que le système de Meta ne l'enregistre. Meta réessaie. Votre gestionnaire traite à nouveau le même événement.
Pour un simple bot d'écho, c'est sans conséquence. Pour un agent IA qui déclenche une action CRM, un paiement ou un message sortant, les doublons causent de réels problèmes. Votre gestionnaire doit être idempotent.
Modèle de déduplication
La valeur msg.id (la chaîne wamid.HBgL...) est unique par message. Les mises à jour de statut utilisent l'ID du message sortant. Les réactions et autres types d'événements ont leurs propres identifiants uniques. Utilisez toujours l'ID spécifique à l'événement — jamais une valeur dérivée — comme clé de déduplication.
Schémas de payload pour les types d'événements clés
Enveloppe de haut niveau (tous les événements)
Message texte entrant (objet value)
Mise à jour du statut de message sortant (objet value)
Checklist de sécurité
X-Hub-Signature-256 est manquant, malformé ou non correspondant. Sans cela, votre point de terminaison accepte des événements falsifiés par n'importe qui.crypto.timingSafeEqual() sous Node.js, hmac.compare_digest() sous Python. Une simple égalité de chaînes (=== / ==) fuit des informations temporelles que les attaquants exploitent pour falsifier les signatures caractère par caractère.express.raw() sur la route du webhook. Dans FastAPI : await request.body() avant toute désérialisation JSON.process.env.WHATSAPP_APP_SECRET (Node.js) ou os.environ["WHATSAPP_APP_SECRET"] (Python). Changez-le immédiatement en cas de fuite.msg.id traités dans Redis avec un TTL de 24h+. Vérifiez avant de traiter. Ignorez les doublons silencieusement — ne renvoyez jamais d'erreur.crypto.randomBytes(32).toString('hex').Common errors and fixes
| Error / Symptom | Root cause | Fix |
|---|---|---|
| Verification fails at registration | Returning JSON instead of plain text, wrong verify token, or returning hub.challenge wrapped in a JSON object |
Return hub.challenge as plain text with Content-Type: text/plain. Exact string — no JSON wrapping. |
| HMAC always fails | Computing HMAC after JSON parsing, using access token instead of App Secret, double-encoding the body | Use express.raw() in Express. Call await request.body() in FastAPI before parsing. Key = App Secret from App Settings → Basic. |
| Events not arriving at all | messages field not subscribed, or webhook not saved/verified |
Dashboard → WhatsApp → Configuration → Webhooks → Manage → enable messages field. Confirm webhook shows as verified. |
| Receiving same event multiple times | At-least-once delivery — expected behavior, not a bug | Implement deduplication using msg.id as Redis key with 24h TTL. Check before processing. |
| Events arriving out of order | No ordering guarantee — by design | Use timestamp field to sort when building conversation threads. Never assume sequential delivery order. |
| Meta retries keep coming | Handler returning non-200, timing out (>20s), or crashing | Return 200 immediately. Move all processing to async queue. Wrap handler in try-catch. Monitor server stability. |
| Media download returns 401 | Using wrong token or token expired | Media downloads require the same access token used for the API. Ensure it's a permanent System User token, not a temporary user token. Check token hasn't been revoked. |
messages array missing from payload |
It's a status update — the array is statuses, not messages |
Check for value.messages AND value.statuses separately. Both arrive under the messages field subscription. |
| Sender number has no + prefix | Cloud API delivers from without the + prefix (e.g. 15550001234 not +15550001234) |
Normalize on receipt: '+' + msg.from to get E.164 format. SocialHook normalizes this automatically. |
| Timestamp is a string, not an integer | Cloud API delivers timestamp as a Unix timestamp string |
Always parseInt(msg.timestamp, 10) (Node.js) or int(msg["timestamp"]) (Python) before date operations. |
SocialHook: the managed webhook layer
Everything in this reference — HMAC verification, payload extraction from the nested envelope, timestamp parsing, sender normalization, retry handling, deduplication infrastructure, WABA vs phone-level routing — is infrastructure work your product doesn't differentiate on.
SocialHook handles the entire Cloud API webhook layer and delivers a normalized event to your endpoint, so your application code only ever sees clean, consistent JSON — never the raw nested Meta payload with its quirks.
| Concern | Raw Cloud API | SocialHook normalized |
|---|---|---|
| Message extraction | entry[0].changes[0].value.messages[0] | Flat message object at top level |
| Sender format | "15550001234" — no + prefix | "+15550001234" — E.164 normalized |
| Timestamp | "1747231892" — string | 1747231892 — integer |
| HMAC verification | You implement it | Done — signature_verified: true |
| Retry on your downtime | Meta retries (7 days) | SocialHook retries (3x exponential) |
| Multi-channel format | Different schema per platform | Same schema: WhatsApp + FB + Instagram |
| Delivery logs | Not available | Full log per event |
| Monthly cost | Your server infra costs | $50 flat |
SocialHook covers WhatsApp, Facebook Messenger, and Instagram DMs — all three Meta messaging channels — under one account, one webhook URL, one normalized payload format. See the full payload reference or start with the 5-minute quickstart.
Common questions
msg.id in Redis with a 24h TTL using SET ... NX. Before processing any event, check if the ID exists. If it does, skip. If not, add it and process. This prevents duplicate CRM records, double AI responses, and duplicate payments.sha256=<64-character lowercase hex>. The hex value is HMAC-SHA256 of the raw request body bytes, using your App Secret as the key (find it in App Settings → Basic — this is different from your access token). Strip the sha256= prefix before comparing. Always use timing-safe comparison.messages array, status updates contain a statuses array. Always check which array is present before processing — attempting to read value.messages[0] on a status update payload returns undefined.metadata.phone_number_id, and only add Phone Number-level webhooks where different routing is needed.Référence lue.
Maintenant, recevez votre premier webhook.
Vous connaissez la spécification. SocialHook gère la vérification HMAC, la normalisation du payload, la logique de tentative et les logs de livraison — votre application ne voit que du JSON propre. Connectez votre numéro en moins de 5 minutes.