Pour tester un webhook WhatsApp Cloud API sur localhost, exposez votre endpoint local en HTTPS, implémentez le challenge GET de Meta, puis vérifiez le X-Hub-Signature-256 de chaque requête POST à partir du corps brut. Enregistrez l’URL du tunnel dans votre application Meta, abonnez le compte WhatsApp Business à messages et envoyez un message de test pour recevoir une vraie charge utile sans déploiement.
Les webhooks WhatsApp utilisent deux modes de vérification distincts
La configuration et la livraison des webhooks ne sont pas authentifiées de la même façon. Pendant la configuration, Meta envoie une requête GET contenant hub.mode, hub.verify_token et hub.challenge. Votre endpoint compare le jeton de vérification et renvoie le challenge en texte brut. Les événements arrivent ensuite par requêtes POST, à authentifier en validant la signature HMAC créée avec le secret de l’application Meta.
Le jeton de vérification est une chaîne aléatoire que vous choisissez ; ce n’est ni le jeton d’accès WhatsApp ni le secret de l’application. Renvoyer le challenge prouve que vous contrôlez l’URL de rappel, mais n’authentifie pas les futures requêtes POST. Le guide officiel des webhooks WhatsApp décrit la configuration du rappel, les abonnements et les champs.
Créer un endpoint avec l’App Router de Next.js
La route suivante gère les deux phases. Lire les données POST avec request.text() conserve exactement les octets nécessaires à la vérification de signature.
// app/api/webhooks/whatsapp/route.ts
import crypto from 'node:crypto';
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
const mode = request.nextUrl.searchParams.get('hub.mode');
const token = request.nextUrl.searchParams.get('hub.verify_token');
const challenge = request.nextUrl.searchParams.get('hub.challenge');
if (mode === 'subscribe' && token === process.env.META_VERIFY_TOKEN) {
return new Response(challenge ?? '', { status: 200 });
}
return new Response('Forbidden', { status: 403 });
}
export async function POST(request: Request) {
const rawBody = await request.text();
const supplied = request.headers.get('x-hub-signature-256') ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.META_APP_SECRET!)
.update(rawBody)
.digest('hex');
const a = Buffer.from(supplied);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
await enqueueWhatsAppPayload(payload);
return new Response('EVENT_RECEIVED', { status: 200 });
}
N’appelez pas request.json() avant de reconstruire le JSON pour le HMAC : espaces, échappement ou ordre des clés peuvent changer et produire un condensé différent. Avec Express, capturez un Buffer avant l’analyseur JSON global. Le guide de vérification des signatures de webhook détaille le traitement du corps brut selon les frameworks.
Démarrer un tunnel et configurer l’URL de rappel
- Lancez l’application Next.js localement, généralement avec
npm run devsur le port 3000. - Exécutez
npx portpreview 3000dans un autre terminal. - Attribuez une valeur aléatoire à
META_VERIFY_TOKENet le secret des paramètres Meta àMETA_APP_SECRET. - Dans le tableau de bord développeur Meta, ouvrez la page Configuration du produit WhatsApp.
- Définissez l’URL de rappel sur
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/whatsappet saisissez le même jeton de vérification. - Après validation, abonnez le compte WhatsApp Business au champ
messages.
Le tunnel doit rester actif pendant le challenge GET et les livraisons POST suivantes. Une URL d’une ancienne session peut encore être résolue sans rediriger vers votre machine ; vérifiez donc l’URL exacte à chaque changement de tunnel.
Tester séparément le challenge GET
Avant d’utiliser le tableau de bord, reproduisez la requête localement :
curl -i \
"http://localhost:3000/api/webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=${META_VERIFY_TOKEN}&hub.challenge=123456"
La bonne réponse est un statut 200 avec le corps 123456, ni du JSON ni "123456" entre guillemets. Un jeton erroné doit produire 403. Ne journalisez pas les paramètres de requête, car le jeton de vérification y figure.
Comprendre une charge utile messages avant d’écrire la logique métier
WhatsApp imbrique les données sur plusieurs niveaux. Une notification type contient object: "whatsapp_business_account", un tableau entry, un tableau changes et une modification dont le field vaut messages. Dans value, le contenu entrant se trouve dans messages ; les états de livraison, lecture et échec de vos envois se trouvent dans statuses.
for (const entry of payload.entry ?? []) {
for (const change of entry.changes ?? []) {
if (change.field !== 'messages') continue;
for (const message of change.value.messages ?? []) {
await handleInboundMessage({
id: message.id,
from: message.from,
type: message.type,
text: message.text?.body,
});
}
for (const status of change.value.statuses ?? []) {
await updateDeliveryStatus(status.id, status.status);
}
}
}
Ne supposez pas que chaque notification contient du texte. Images, audio, documents, positions, réponses interactives, messages système et charges ne contenant qu’un état ont des formes différentes. Utilisez un répartiteur fondé sur message.type, validez les champs facultatifs et conservez les événements inconnus pour examen au lieu de provoquer une erreur.
Vérifier correctement la signature POST
La valeur X-Hub-Signature-256 a la forme sha256=<hex digest>. Calculez le HMAC-SHA256 des octets bruts avec le secret de l’application Meta. Le jeton d’accès WhatsApp, permanent ou temporaire, sert aux appels Graph API et non de clé HMAC. Comparez en temps constant et refusez toute signature absente.
Gardez la vérification active en local. Quiconque connaît l’URL du tunnel peut y envoyer du JSON. Sans contrôle, un faux événement pourrait déclencher des réponses, modifier le CRM ou révéler des données client. Renouvelez le secret s’il a été validé dans Git, affiché ou partagé par erreur.
Accuser réception rapidement et dédupliquer les messages
Renvoyez 200 après authentification et mise en file durable. N’attendez pas le téléchargement de médias, un appel à un LLM ou plusieurs mises à jour de services. Les fournisseurs réessaient si l’accusé échoue et les doublons sont normaux en cas d’incertitude réseau.
Utilisez l’id du message WhatsApp comme clé d’idempotence pour messages entrants et états, avec une contrainte d’unicité sur les ID traités. Un état peut légitimement évoluer d’envoyé à livré puis lu ; dédupliquez chaque transition utile sans supprimer un état ultérieur.
Résoudre les problèmes de configuration du webhook WhatsApp
L’URL de rappel n’a pas pu être validée
Testez la route GET via l’URL publique. Elle doit accepter GET, comparer exactement le jeton et renvoyer uniquement le challenge. Redirections, middleware d’authentification, réécritures de langue ou enveloppe JSON peuvent échouer. Vérifiez aussi que le processus de développement a chargé la variable d’environnement.
La vérification réussit, mais aucun message n’arrive
La vérification du rappel n’abonne pas automatiquement le compte aux champs. Confirmez l’abonnement à messages et que le numéro appartient à la bonne application et au bon compte. En mode développement, utilisez un destinataire autorisé.
Toutes les requêtes POST échouent à la vérification de signature
Les causes habituelles sont l’emploi du jeton d’accès au lieu du secret, le hachage du JSON analysé, l’oubli du préfixe sha256= ou des encodages différents. Journalisez la longueur du corps et la présence de l’en-tête, jamais le secret ni toute la charge client.
Les messages texte fonctionnent, mais pas les médias
Une notification média contient un ID, pas nécessairement les octets du fichier. Récupérez d’abord le média par Graph API avec un jeton valide, puis téléchargez-le. Gardez ce traitement lent hors du chemin d’accusé de réception.
Le endpoint local reçoit des événements en double
Inspectez le statut et la latence de réponse, ajoutez une idempotence durable et rejouez un événement capturé après chaque correction. Le guide de rejeu des webhooks évite l’envoi d’un nouveau message réel à chaque modification.
Protéger les données client pendant les tests locaux
- Utilisez si possible des numéros de test et des conversations synthétiques.
- Masquez dans les journaux numéros, corps des messages, URL de médias, contacts et noms de profil.
- Conservez secret, jetons d’accès et jeton de vérification uniquement dans des fichiers d’environnement ignorés ou un gestionnaire de secrets.
- Limitez l’accès aux captures du tunnel et supprimez-les après le débogage.
- Validez les identifiants d’objet, de champ et de compte avant toute action métier.
Un tunnel accélère les itérations, mais apporte aussi des données personnelles proches de la production sur la machine du développeur. Appliquez la liste de contrôle de sécurité des tunnels avant tout test avec de vrais utilisateurs.
