Recevoir des webhooks WhatsApp dans une Supabase Edge Function
Une Edge Function Supabase peut être directement la destination webhook du numéro. Elle répond à la requête GET de vérification de Meta avec la valeur hub.challenge, insère chaque message reçu en POST dans une table Postgres et répond via la Cloud API. Déployez-la avec la vérification JWT désactivée : Supabase exige par défaut un JWT valide, et Meta ne peut pas en envoyer.
Le comportement par défaut de Supabase pour les Edge Functions : toute requête sans JWT valide est rejetée avant l'exécution de votre handler.
L'option de supabase functions deploy qui désactive la vérification JWT pour une fonction.
Délai d'inactivité d'une requête pour une Edge Function : sans réponse à cette échéance, Supabase renvoie 504.
Que doit faire l'Edge Function ?
Deux choses, sur deux méthodes.
En GET, elle répond à la vérification de Meta. Lisez hub.mode, hub.verify_token et hub.challenge dans la query string, comparez le token avec celui que vous avez saisi comme token de vérification de la destination, puis renvoyez le challenge en corps texte brut avec le statut 200. Renvoyez 403 si le token ne correspond pas.
En POST, elle reçoit les événements. Analysez le corps, extrayez les messages depuis entry, changes et value, insérez-les dans une table et renvoyez 200. Répondez vite : pour Meta, une réponse lente ou en échec est une livraison ratée, et il réessaie, ce qui donne deux fois le même message.
Définissez l'URL de la fonction comme destination webhook du numéro dans EasyCoexistence, avec le même token de vérification, et Meta commence à livrer.
import { createClient } from "npm:@supabase/supabase-js@2";
const db = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
Deno.serve(async (req) => {
const url = new URL(req.url);
if (req.method === "GET") {
const ok = url.searchParams.get("hub.verify_token") === Deno.env.get("VERIFY_TOKEN");
return new Response(ok ? url.searchParams.get("hub.challenge") : "forbidden", { status: ok ? 200 : 403 });
}
if (url.searchParams.get("key") !== Deno.env.get("URL_SECRET")) return new Response("no", { status: 401 });
const body = await req.json();
const rows = (body.entry ?? []).flatMap((e: any) => e.changes ?? [])
.flatMap((c: any) => (c.value?.messages ?? []).map((m: any) => ({
wamid: m.id, sender: m.from, kind: m.type, text: m.text?.body ?? null,
phone_number_id: c.value.metadata?.phone_number_id, raw: m,
})));
if (rows.length) await db.from("whatsapp_messages").upsert(rows, { onConflict: "wamid" });
return new Response("ok");
});Pourquoi la fonction renvoie-t-elle 401 à Meta ?
Parce que la vérification JWT est activée. Supabase valide un JWT dans l'en-tête Authorization avant que le handler d'une Edge Function ne s'exécute, et c'est le comportement par défaut de chaque fonction. Meta n'envoie pas cet en-tête : la plateforme rejette donc à la fois le GET de vérification et chaque livraison, et votre code ne les voit jamais.
Désactivez-la pour cette fonction uniquement. Déployez avec supabase functions deploy whatsapp --no-verify-jwt, ou définissez verify_jwt = false dans la section de la fonction du fichier supabase/config.toml. La documentation de Supabase décrit exactement ce cas : un endpoint public appelé par un fournisseur de webhook externe.
Le contrôle de la plateforme désactivé, la fonction doit avoir le sien. L'en-tête de signature ne vous est pas accessible, car Meta signe avec le secret de l'application propriétaire de l'abonnement. Placez un long secret aléatoire dans l'URL de destination, en paramètre de requête, et rejetez les requêtes qui n'en ont pas, comme le fait l'exemple en POST.
Comment la fonction doit-elle stocker et répondre ?
Stockez d'abord, répondez ensuite. Faites un upsert sur l'identifiant de message WhatsApp avec une contrainte d'unicité : les nouvelles tentatives de Meta tombent alors sur la même ligne au lieu de créer un doublon. Conservez le phone_number_id destinataire dans chaque ligne si plusieurs numéros pointent vers la fonction.
Pour tout traitement lent, comme appeler un modèle ou une autre API avant de répondre, confiez le travail à EdgeRuntime.waitUntil et renvoyez la réponse tout de suite. La fonction continue de tourner jusqu'à la résolution de cette promesse, tandis que Meta a déjà son 200. Supabase cesse d'attendre après 150 secondes d'inactivité et renvoie 504 : un handler qui fait tout son travail avant de répondre est donc le plus susceptible de provoquer des nouvelles tentatives.
Les réponses partent vers l'endpoint messages de la Cloud API, avec le phone number id et le token du numéro, stockés comme secrets de la fonction. Dans les 24 heures suivant le dernier message du client, le texte libre est livré ; au-delà, seul un template approuvé l'est.
Les messages envoyés depuis le téléphone parviennent-ils à la fonction ?
Oui. Sur un numéro Coexistence, l'application WhatsApp Business continue de fonctionner, et lorsque quelqu'un y répond, Meta livre un écho de ce message sortant à la même destination, sous le champ smb_message_echoes et dans un tableau message_echoes plutôt que messages.
Étendez l'analyseur pour lire les deux et stocker une colonne direction. La table contient alors la conversation complète, les lignes du client et les réponses de l'entreprise, ce que la plupart des gens veulent interroger ensuite : délais de réponse, conversations sans réponse, heure du dernier message entrant par contact.
Cette dernière valeur est aussi la donnée dont chaque chemin d'envoi a besoin, puisqu'elle détermine si une réponse peut être du texte libre ou doit être un template. Une vue sur la table peut l'exposer par contact au reste de votre application.
Erreurs courantes
- Déployer avec la vérification JWT laissée activée. Supabase rejette les requêtes de Meta avec un 401 avant l'exécution du handler, et aucun message n'arrive jamais.
- Renvoyer le challenge enveloppé dans du JSON. Meta attend la valeur brute comme corps, sinon la vérification échoue sans bruit.
- Faire un traitement lent avant de répondre. Meta réessaie ce qu'il considère comme des échecs, et le même message est traité deux fois.
- Désactiver la vérification JWT sans aucun contrôle de votre côté. Toute personne qui trouve l'URL peut poster de faux messages dans la table.
Connectez le numéro sur easycoexistence.com, définissez sa destination webhook sur l'URL de l'Edge Function avec votre token de vérification, et stockez le phone number id et le token comme secrets de la fonction. À partir de US$ 9 par numéro et par mois, jusqu'à US$ 1 à mesure que vous ajoutez des numéros, avec les 3 premiers jours gratuits.
Questions fréquemment posées
Cela fonctionne-t-il sur le plan gratuit de Supabase ?
Les Edge Functions tournent sur le plan gratuit, avec une limite de 150 secondes de durée réelle contre 400 secondes sur les plans payants. Un handler de webhook qui répond vite reste très loin de l'une comme de l'autre.
Dois-je utiliser la clé service role dans la fonction ?
Uniquement côté serveur, comme ici, lue depuis l'environnement de la fonction. Elle contourne la sécurité au niveau des lignes : elle ne doit donc jamais atteindre un navigateur.
Une seule fonction peut-elle servir plusieurs numéros ?
Oui. Faites pointer la destination webhook de chaque numéro vers elle et aiguillez selon le phone_number_id de chaque payload.
EasyCoexistence voit-il les messages ?
Non. Meta livre chaque webhook directement à votre fonction, et EasyCoexistence ne stocke jamais le contenu des conversations.
Poursuivre la lecture
Prêt à commencer ?
Configurez WhatsApp Coexistence en quelques minutes, pas en quelques mois. L’application continue de fonctionner sur le téléphone.
Commencer l'essai gratuitVérifié le