Je suis complètement bloqué en essayant de vérifier mon webhook Meta/WhatsApp Business API. Meta le rejette constamment avec l’erreur standard : « The callback URL or verify token couldn’t be validated ».
J’ai isolé le problème à la façon dont n8n retourne la réponse, mais je ne comprends pas pourquoi ça échoue.
Voici ma configuration :
Hébergement : n8n auto-hébergé via Docker (Windows).
Tunnel : Cloudflare Tunnels (.trycloudflare.com).
Nœud de déclenchement : Nœud Webhook, Méthode GET, Chemin whatsapp-incoming. L’option Respond est strictement définie sur « Using ‘Respond to Webhook’ Node ».
Nœud de réponse : Nœud ‘Respond to Webhook’. Respond With est défini sur Text. L’expression Response Body est exactement : {{ $json.query["hub.challenge"] }}.
Voici le comportement étrange que j’observe :
Le réseau fonctionne bien : La requête de Meta atteint correctement mon instance n8n.
L’exécution réussit : Dans l’onglet Executions, le workflow se termine parfaitement et affiche un statut vert « Succeeded ».
L’expression s’analyse correctement : Lorsque je vérife les détails d’exécution du nœud ‘Respond to Webhook’, il capture et prévisualise avec succès le numéro de défi exact envoyé par Meta.
LE PROBLÈME (Échec silencieux) : Lorsque je teste l’URL manuellement via mon navigateur (par exemple, https://my-cloudflare-url/webhook/whatsapp-incoming?hub.challenge=11223344), n8n enregistre une exécution réussie, mais le navigateur affiche un écran complètement blanc/vide. Il ne retourne pas le texte, pas même un message d’erreur. Il retourne un corps vide.
Parce que le corps de la réponse est vide au lieu de retourner le numéro de défi brut, Meta rejette la vérification.
J’ai déjà essayé :
Supprimer tous les en-têtes de réponse supplémentaires dans le nœud ‘Respond to Webhook’.
M’assurer que l’expression est évaluée comme une Expression (fx) et non comme une chaîne statique.
Recréer le nœud webhook.
Quelqu’un a-t-il rencontré ce problème où le nœud ‘Respond to Webhook’ s’exécute avec succès mais retourne un corps vide au client ? Me manque-t-il une configuration ?
Je pense que ta configuration est correcte, donc pas besoin de passer à JSON ou de modifier les URLs. J’ai testé ta configuration exacte et elle fonctionne parfaitement.
La confusion vient du panneau de sortie. Par défaut, le nœud « Respond to Webhook » affiche son entrée, pas la réponse réelle qu’il envoie. Si ton expression aboutit à vide, n8n enregistre quand même une exécution verte réussie (HTTP 200) mais envoie un corps vide. Cela correspond exactement à ton problème.
Alors ouvre le nœud Respond, va dans l’onglet Settings et active « Enable Response Output Branch » pour voir ce qui a été envoyé. Si c’est vide, l’expression ne se résout pas (souvent à cause de guillemets courbes copiés), donc retype les crochets à la main. Si ça affiche 11223344 et que le navigateur ne reçoit toujours rien, alors c’est le tunnel trycloudflare qui supprime le corps et c’est une chasse différente.
Fais-moi savoir si cela t’aide d’une façon ou d’une autre.
Salut @Permadi Bienvenue !
Répondre avec : Text envoie Content-Type: text/html par défaut, et la poignée de main de Meta veut le challenge en retour en texte brut sans rien autour. Supprimer les en-têtes de réponse l’a laissé à cette valeur par défaut, alors définissez-le explicitement sur le nœud Respond to Webhook, Nom Content-Type et Valeur text/plain.
Voir ceci :
L’indice dans votre message que les réponses ci-dessus ignorent est celui-ci : votre aperçu d’exécution affiche le numéro du défi, mais le corps arrive vide. Cela écarte la théorie des guillemets typographiques, car si l’expression ne se résolvait pas, vous ne verriez pas du tout le numéro dans l’aperçu. Et ce n’est pas entièrement expliqué par le type de contenu non plus, puisque text/html enverrait toujours le numéro, juste avec le mauvais en-tête, pas un corps vide.
Ce qui correspond à toutes vos preuves est le type. hub.challenge arrive en tant que numéro, et Respond to Webhook en mode texte veut une chaîne. Quand il reçoit un numéro nu, certaines compilations n’envoient rien, ce qui correspond exactement à votre exécution verte avec un corps vide.
Convertissez-le en chaîne et testez à nouveau :
{{ $json.query[“hub.challenge”].toString() }}
ou
{{ String($json.query[“hub.challenge”]) }}
Si le navigateur affiche maintenant le numéro, c’était ça, et Meta acceptera la poignée de main. Définissez l’en-tête Content-Type sur text/plain comme l’a dit Anshul pendant que vous y êtes, puisque Meta veut du texte brut une fois que le corps n’est pas vide.
S’il est toujours vide après la conversion en chaîne, alors le tunnel consomme le corps plutôt que n8n. Les tunnels rapides trycloudflare supprimaient intermittently les petits corps, alors frappez l’URL avec curl -i directement contre votre conteneur, en contournant Cloudflare. Si curl affiche le numéro et le navigateur à travers le tunnel ne l’affiche pas, vous avez votre réponse et ce n’est pas n8n du tout.