AnmeldenKostenlose Testphase starten
Meta Tech Provider

WhatsApp-Webhooks in einer Supabase Edge Function empfangen

Eine Supabase Edge Function kann direkt das Webhook-Ziel der Nummer sein. Sie beantwortet die Verifizierungs-GET-Anfrage von Meta mit dem Wert hub.challenge, schreibt jede per POST gelieferte Nachricht in eine Postgres-Tabelle und antwortet über die Cloud API. Deployen Sie sie mit deaktivierter JWT-Prüfung, denn Supabase verlangt standardmäßig ein gültiges JWT, und Meta kann keines senden.

verify_jwt = true

Der Standard von Supabase für Edge Functions: Jede Anfrage ohne gültiges JWT wird abgewiesen, bevor Ihr Handler läuft.

--no-verify-jwt

Das Flag von supabase functions deploy, das die JWT-Prüfung für eine Function abschaltet.

150 s

Idle-Timeout für Anfragen an eine Edge Function; ohne Antwort bis dahin folgt 504.

Was muss die Edge Function leisten?

Zwei Dinge, auf zwei Methoden.

Bei GET beantwortet sie die Verifizierung von Meta. Lesen Sie hub.mode, hub.verify_token und hub.challenge aus dem Query-String, vergleichen Sie das Token mit dem Verify-Token, das Sie beim Webhook-Ziel eingetragen haben, und geben Sie die Challenge als reinen Text im Body mit Status 200 zurück. Bei abweichendem Token antworten Sie mit 403.

Bei POST empfängt sie Ereignisse. Parsen Sie den Body, holen Sie die Nachrichten aus entry, changes und value, schreiben Sie sie in eine Tabelle und geben Sie 200 zurück. Antworten Sie schnell: Meta wertet eine langsame oder fehlgeschlagene Antwort als misslungene Zustellung und wiederholt sie, sodass dieselbe Nachricht doppelt eintrifft.

Hinterlegen Sie die URL der Function in EasyCoexistence als Webhook-Ziel der Nummer, mit demselben Verify-Token, und Meta beginnt mit der Zustellung.

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");
});

Warum gibt die Function an Meta einen 401 zurück?

Weil die JWT-Prüfung aktiv ist. Supabase validiert ein JWT im Authorization-Header, bevor der Handler einer Edge Function läuft, und das ist der Standard für jede Function. Meta sendet keinen solchen Header, also weist die Plattform sowohl die Verifizierungs-GET-Anfrage als auch jede Zustellung ab, und Ihr Code bekommt sie nie zu sehen.

Schalten Sie die Prüfung nur für diese eine Function ab. Deployen Sie entweder mit supabase functions deploy whatsapp --no-verify-jwt oder setzen Sie verify_jwt = false im Abschnitt der Function in supabase/config.toml. Die Dokumentation von Supabase beschreibt genau diesen Fall: einen öffentlichen Endpoint, der von einem externen Webhook-Anbieter aufgerufen wird.

Ist die Prüfung der Plattform aus, braucht die Function eine eigene. Der Signatur-Header steht Ihnen nicht zur Verfügung, da Meta mit dem App Secret der App signiert, der das Abonnement gehört. Legen Sie ein langes, zufälliges Secret als Query-Parameter in die Ziel-URL und weisen Sie Anfragen ohne dieses Secret ab, wie es das Beispiel bei POST tut.

Wie sollte die Function speichern und antworten?

Erst speichern, dann antworten. Führen Sie ein Upsert auf der WhatsApp-Nachrichten-ID mit einem Unique Constraint aus, damit die Wiederholungen von Meta in derselben Zeile landen, statt ein Duplikat zu erzeugen. Speichern Sie die empfangende phone_number_id in jeder Zeile, falls mehr als eine Nummer auf die Function zeigt.

Bei allem Langsamen, etwa dem Aufruf eines Modells oder einer anderen API vor der Antwort, übergeben Sie die Arbeit an EdgeRuntime.waitUntil und geben die Antwort sofort zurück. Die Function läuft weiter, bis dieses Promise abgeschlossen ist, während Meta seine 200 längst hat. Supabase wartet höchstens 150 Sekunden Leerlaufzeit und liefert dann 504. Ein Handler, der seine ganze Arbeit vor der Antwort erledigt, löst deshalb am ehesten Wiederholungen aus.

Antworten gehen an den Messages-Endpoint der Cloud API, mit der Telefonnummern-ID und dem Token der Nummer, die als Function Secrets gespeichert sind. Innerhalb von 24 Stunden nach der letzten Nachricht des Kunden wird Freitext zugestellt; danach nur noch ein genehmigtes Template.

Erreichen vom Telefon gesendete Nachrichten die Function?

Ja. Bei einer Coexistence-Nummer funktioniert die WhatsApp Business App weiter, und wenn jemand darüber antwortet, liefert Meta ein Echo dieser ausgehenden Nachricht an dasselbe Ziel, und zwar im Feld smb_message_echoes und in einem Array message_echoes statt in messages.

Erweitern Sie den Parser, damit er beides liest, und speichern Sie eine Spalte direction. Die Tabelle enthält dann die vollständige Unterhaltung, die Zeilen des Kunden und die Antworten des Unternehmens. Genau das wollen die meisten später abfragen: Antwortzeiten, unbeantwortete Verläufe, den Zeitpunkt der letzten eingehenden Nachricht je Kontakt.

Dieser letzte Wert ist zugleich die Eingabe, die jeder Sendeweg braucht, denn er entscheidet, ob eine Antwort Freitext sein darf oder ein Template sein muss. Eine View über die Tabelle kann ihn pro Kontakt für den Rest Ihrer App bereitstellen.

Häufige Fehler

  • Deployen mit aktiver JWT-Prüfung. Supabase weist die Anfragen von Meta mit 401 ab, bevor der Handler läuft, und keine Nachricht kommt je an.
  • Die Challenge in JSON verpackt zurückgeben. Meta braucht den rohen Wert als Body, sonst schlägt die Verifizierung ohne Hinweis fehl.
  • Langsame Arbeit vor der Antwort erledigen. Meta wiederholt, was es als Fehlschlag sieht, und dieselbe Nachricht wird zweimal verarbeitet.
  • Die JWT-Prüfung abschalten, ohne eine eigene Prüfung einzubauen. Jeder, der die URL findet, kann gefälschte Nachrichten in die Tabelle schreiben.
Mit EasyCoexistence

Verbinden Sie die Nummer auf easycoexistence.com, setzen Sie ihr Webhook-Ziel auf die URL der Edge Function mit Ihrem Verify-Token und hinterlegen Sie die Telefonnummern-ID und das Token als Function Secrets. Ab US$ 9 pro Nummer und Monat, mit wachsender Nummernzahl bis US$ 1, die ersten 3 Tage kostenlos.

Häufig gestellte Fragen

Funktioniert das im kostenlosen Supabase-Tarif?

Edge Functions laufen im kostenlosen Tarif, mit einer Wall-Clock-Grenze von 150 Sekunden gegenüber 400 Sekunden in kostenpflichtigen Tarifen. Ein Webhook-Handler, der schnell antwortet, bleibt weit innerhalb beider Grenzen.

Soll ich den Service-Role-Key in der Function verwenden?

Nur serverseitig, wie hier, gelesen aus der Umgebung der Function. Er umgeht Row Level Security und darf deshalb nie in einen Browser gelangen.

Kann eine Function mehrere Nummern bedienen?

Ja. Richten Sie das Webhook-Ziel jeder Nummer auf sie aus und verzweigen Sie anhand der phone_number_id in jeder Payload.

Sieht EasyCoexistence die Nachrichten?

Nein. Meta liefert jeden Webhook direkt an Ihre Function, und EasyCoexistence speichert keine Inhalte von Unterhaltungen.

Weiterlesen

Bereit für den Start?

Richten Sie WhatsApp Coexistence in wenigen Minuten statt Monaten ein. Die App funktioniert weiterhin auf dem Telefon.

Kostenlose Testphase starten

Verifiziert am

WhatsApp-Webhooks in Supabase Edge Functions