InloggenGratis proefperiode starten
Meta Tech Provider

Ontvang WhatsApp-webhooks in een Supabase Edge Function

Een Supabase Edge Function kan rechtstreeks de webhook-bestemming van het nummer zijn. De functie beantwoordt de verificatie-GET van Meta met de waarde van hub.challenge, voegt elk via POST ontvangen bericht toe aan een Postgres-tabel en antwoordt via de Cloud API. Deploy de functie met JWT-verificatie uitgeschakeld, want Supabase eist standaard een geldige JWT en Meta kan er geen sturen.

verify_jwt = true

De standaardinstelling van Supabase voor Edge Functions: elk verzoek zonder geldige JWT wordt geweigerd voordat je handler draait.

--no-verify-jwt

De vlag van supabase functions deploy die JWT-verificatie voor een functie uitschakelt.

150 s

Time-out bij inactiviteit voor een Edge Function; zonder antwoord daarbinnen volgt 504.

Wat moet de Edge Function doen?

Twee dingen, op twee methoden.

Bij GET beantwoordt de functie de verificatie van Meta. Lees hub.mode, hub.verify_token en hub.challenge uit de query string, vergelijk het token met het verify-token dat je als bestemming hebt ingevoerd en geef de challenge terug als platte tekst met status 200. Geef 403 terug als het token niet klopt.

Bij POST ontvangt de functie events. Parse de body, haal de berichten uit entry, changes en value, voeg ze toe aan een tabel en geef 200 terug. Houd het antwoord snel: Meta behandelt een trage of mislukte reactie als een mislukte aflevering en probeert opnieuw, waardoor hetzelfde bericht twee keer binnenkomt.

Stel de URL van de functie in als webhook-bestemming van het nummer in EasyCoexistence, met hetzelfde verify-token, en Meta begint met afleveren.

supabase/functions/whatsapp/index.ts
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");
});

Waarom geeft de functie 401 terug aan Meta?

Omdat JWT-verificatie aan staat. Supabase valideert een JWT in de Authorization-header voordat de handler van een Edge Function draait, en dat is de standaard voor elke functie. Meta stuurt zo'n header niet, dus het platform weigert zowel de verificatie-GET als elke aflevering, en je code ziet ze nooit.

Zet het alleen voor deze functie uit. Deploy met supabase functions deploy whatsapp --no-verify-jwt, of zet verify_jwt = false onder het onderdeel van de functie in supabase/config.toml. De documentatie van Supabase beschrijft precies dit geval: een openbaar endpoint dat een externe webhook-provider aanroept.

Als de controle van het platform uit staat, heeft de functie een eigen controle nodig. De handtekening-header kun je hier niet gebruiken, want Meta tekent met het app secret van de app die eigenaar is van het abonnement. Zet een lang willekeurig geheim als queryparameter in de bestemmings-URL en weiger verzoeken zonder dat geheim, zoals het voorbeeld bij POST doet.

Hoe sla je op en antwoord je?

Eerst opslaan, daarna antwoorden. Doe een upsert op het bericht-id van WhatsApp met een unique constraint, zodat de nieuwe pogingen van Meta op dezelfde rij uitkomen in plaats van een duplicaat te maken. Bewaar de ontvangende phone_number_id in elke rij als meer dan één nummer naar de functie wijst.

Geef traag werk, zoals een model of een andere API aanroepen voordat je antwoordt, door aan EdgeRuntime.waitUntil en geef het antwoord meteen terug. De functie blijft draaien tot die promise klaar is, terwijl Meta al zijn 200 heeft. Supabase wacht maximaal 150 seconden inactiviteit en geeft dan 504 terug, dus een handler die al het werk voor het antwoord doet, veroorzaakt de meeste nieuwe pogingen.

Antwoorden gaan naar het endpoint voor berichten van de Cloud API, met de phone number id en het token van het nummer, opgeslagen als function secrets. Binnen 24 uur na het laatste bericht van de klant wordt vrije tekst bezorgd; daarbuiten alleen een goedgekeurd template.

Komen berichten die vanaf de telefoon worden verstuurd ook bij de functie aan?

Ja. Op een Coexistence-nummer blijft de WhatsApp Business-app werken, en wanneer iemand daarmee antwoordt, levert Meta een echo van dat uitgaande bericht af op dezelfde bestemming, onder het veld smb_message_echoes en een array message_echoes in plaats van messages.

Breid de parser uit om beide te lezen en sla een kolom direction op. De tabel bevat dan het hele gesprek, de regels van de klant en de antwoorden van het bedrijf, en dat wil je achteraf meestal bevragen: reactietijden, onbeantwoorde gesprekken, het laatste inkomende tijdstip per contact.

Die laatste waarde is ook de invoer die elke verzendroute nodig heeft, want ze bepaalt of een antwoord vrije tekst mag zijn of een template moet zijn. Met een view over de tabel kun je haar per contact beschikbaar maken voor de rest van je app.

Veelgemaakte fouten

  • Deployen met JWT-verificatie aan. Supabase weigert de verzoeken van Meta met 401 voordat de handler draait, en er komt nooit een bericht binnen.
  • De challenge in JSON verpakt teruggeven. Meta wil de ruwe waarde als body, anders mislukt de verificatie stilletjes.
  • Trage taken uitvoeren voordat je antwoordt. Meta probeert opnieuw wat het als mislukt ziet, en hetzelfde bericht wordt twee keer verwerkt.
  • JWT-verificatie uitzetten zonder eigen controle. Iedereen die de URL vindt, kan nepberichten in de tabel plaatsen.
Dit doen met EasyCoexistence

Koppel het nummer op easycoexistence.com, stel de webhook-bestemming in op de URL van de Edge Function met je verify-token en bewaar de phone number id en het token als function secrets. Vanaf US$ 9 per nummer per maand, dalend naar US$ 1 naarmate je nummers toevoegt, met de eerste 3 dagen gratis.

Veelgestelde vragen

Werkt dit op het gratis abonnement van Supabase?

Edge Functions draaien op het gratis abonnement, met een wall clock-limiet van 150 seconden tegenover 400 seconden bij betaalde abonnementen. Een webhook-handler die snel antwoordt, blijft daar ruim binnen.

Moet ik de service role key in de functie gebruiken?

Alleen server-side, zoals hier, gelezen uit de omgeving van de functie. Hij omzeilt Row Level Security en mag dus nooit in een browser terechtkomen.

Kan één functie meerdere nummers bedienen?

Ja. Laat de webhook-bestemming van elk nummer naar de functie wijzen en vertak op de phone_number_id in elke payload.

Ziet EasyCoexistence de berichten?

Nee. Meta levert elke webhook rechtstreeks aan je functie af, en EasyCoexistence slaat nooit de inhoud van gesprekken op.

Lees verder

Klaar om te beginnen?

Stel WhatsApp Coexistence in enkele minuten in, niet maanden. De app blijft op je telefoon werken.

Start je gratis proefperiode

Geverifieerd op

WhatsApp-webhooks in Supabase Edge Functions