Supabase Edge Function で WhatsApp の Webhook を受信する
Supabase Edge Function は、番号の Webhook 送信先としてそのまま使えます。Meta の認証 GET には hub.challenge の値で応答し、POST されたメッセージを Postgres のテーブルに挿入し、Cloud API で返信します。デプロイ時は JWT 検証をオフにしてください。Supabase は既定で有効な JWT を要求しますが、Meta は JWT を送れないためです。
Supabase の Edge Function の既定値です。有効な JWT のないリクエストは、ハンドラーが実行される前に拒否されます。
関数の JWT 検証を無効にする supabase functions deploy のフラグです。
Edge Function のリクエストのアイドルタイムアウトです。それまでに応答がないと 504 が返ります。
Edge Function は何をする必要がありますか?
2 つのメソッドで、2 つのことを行います。
GET では Meta の認証に応答します。クエリ文字列から hub.mode、hub.verify_token、hub.challenge を読み取り、トークンを送信先に入力した認証用トークンと比較して、challenge をプレーンテキストの本文としてステータス 200 で返します。トークンが一致しない場合は 403 を返してください。
POST ではイベントを受信します。本文を解析し、entry、changes、value からメッセージを取り出してテーブルに挿入し、200 を返します。応答は速くしてください。Meta は応答が遅い、または失敗した場合を配信失敗とみなして再送するため、同じメッセージが 2 回届くことになります。
EasyCoexistence で、関数の URL を同じ認証用トークンとともに番号の Webhook 送信先に設定すると、Meta からの配信が始まります。
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");
});なぜ関数は Meta に 401 を返すのですか?
JWT 検証が有効だからです。Supabase は Edge Function のハンドラーが実行される前に、Authorization ヘッダーの JWT を検証します。これはすべての関数の既定の動作です。Meta はそのようなヘッダーを送らないため、プラットフォームが認証の GET も各配信もすべて拒否し、コードには届きません。
この関数に限って無効にしてください。supabase functions deploy whatsapp --no-verify-jwt でデプロイするか、supabase/config.toml の該当関数のセクションに verify_jwt = false を設定します。Supabase のドキュメントでも、外部の Webhook 提供元から呼び出される公開エンドポイントというまさにこのケースが説明されています。
プラットフォーム側のチェックを外す場合は、関数自身で確認する必要があります。署名ヘッダーは使えません。Meta は購読を所有するアプリのアプリシークレットで署名するためです。送信先 URL のクエリパラメータに長いランダムなシークレットを入れ、それがないリクエストは拒否してください。サンプルでも POST でそのようにしています。
関数はどのように保存し、返信すべきですか?
先に保存し、返信は後にします。WhatsApp のメッセージ ID に一意制約を付けて upsert すれば、Meta の再送が重複を作らず同じ行に入ります。複数の番号がこの関数を指している場合は、受信した phone_number_id を各行に保持してください。
モデルや別の API の呼び出しなど、時間のかかる処理は EdgeRuntime.waitUntil に渡し、すぐにレスポンスを返します。その Promise が完了するまで関数は動き続け、Meta にはすでに 200 が返っています。Supabase はアイドル状態が 150 秒に達すると待機をやめて 504 を返すため、応答前にすべての処理を行うハンドラーは、再送を最も招きやすくなります。
返信は Cloud API のメッセージ用エンドポイントに、関数のシークレットとして保存した番号の phone number id とトークンで送ります。お客様の最後のメッセージから 24 時間以内ならフリーテキストで送れますが、それを過ぎると承認済みのメッセージテンプレートのみ送信できます。
電話から送ったメッセージも関数に届きますか?
はい。Coexistence の番号では WhatsApp Business アプリが引き続き使え、そこから返信すると、Meta は送信したメッセージのエコーを同じ送信先に配信します。このとき messages ではなく、smb_message_echoes フィールドの message_echoes 配列で届きます。
パーサーを拡張して両方を読み取り、direction 列を保存してください。すると、テーブルにはお客様の発言と企業の回答を含む会話全体が残ります。応答時間、未回答のスレッド、連絡先ごとの最後の受信時刻など、多くの人があとで集計したい内容です。
この最後の値は、あらゆる送信経路で必要になる入力でもあります。返信をフリーテキストにできるか、メッセージテンプレートにする必要があるかを決めるからです。テーブルに対するビューを作れば、連絡先ごとの値をアプリの他の部分に公開できます。
よくある間違い
- JWT 検証を有効にしたままデプロイする。ハンドラーが実行される前に Supabase が Meta のリクエストを 401 で拒否し、メッセージは一切届きません。
- challenge を JSON で包んで返す。Meta は本文として生の値を必要とし、認証は何も表示されないまま失敗します。
- 応答の前に時間のかかる処理を行う。Meta は失敗とみなしたものを再送するため、同じメッセージが 2 回処理されます。
- 独自のチェックなしで JWT 検証をオフにする。URL を知った人なら誰でも、偽のメッセージをテーブルに投稿できてしまいます。
easycoexistence.com で番号を接続し、Webhook 送信先を Edge Function の URL と認証用トークンで設定します。番号の phone number id とトークンは、関数のシークレットとして保存してください。料金は接続済みの番号 1 件あたり月額 US$ 9 から、番号が増えるにつれて US$ 1 まで下がり、最初の 3 日間は無料です。
よくある質問
Supabase の無料プランでも動作しますか?
Edge Functions は無料プランでも動作します。ウォールクロックの制限は無料プランで 150 秒、有料プランで 400 秒です。すばやく応答する Webhook ハンドラーなら、どちらでも十分に余裕があります。
関数内で service role キーを使うべきですか?
ここで示したようにサーバー側のみで、関数の環境変数から読み取る場合に限ります。このキーは行レベルセキュリティを回避するため、ブラウザには決して渡さないでください。
1 つの関数で複数の番号を扱えますか?
はい。すべての番号の Webhook 送信先をこの関数に向け、各ペイロードの phone_number_id で処理を分岐させてください。
EasyCoexistence はメッセージを見られますか?
いいえ。Meta が各 Webhook をお客様の関数に直接配信し、EasyCoexistence は会話の内容を一切保存しません。
続きを読む
始める準備はできましたか?
WhatsApp Coexistence の設定は数分で完了し、数か月もかかりません。スマートフォン上のアプリも引き続き使えます。
無料トライアルを始める確認日