ログイン無料トライアルを開始
Meta Tech Provider

Supabase Edge Function で WhatsApp の Webhook を受信する

Supabase Edge Function は、番号の Webhook 送信先としてそのまま使えます。Meta の認証 GET には hub.challenge の値で応答し、POST されたメッセージを Postgres のテーブルに挿入し、Cloud API で返信します。デプロイ時は JWT 検証をオフにしてください。Supabase は既定で有効な JWT を要求しますが、Meta は JWT を送れないためです。

verify_jwt = true

Supabase の Edge Function の既定値です。有効な JWT のないリクエストは、ハンドラーが実行される前に拒否されます。

--no-verify-jwt

関数の JWT 検証を無効にする supabase functions deploy のフラグです。

150 s

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 からの配信が始まります。

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

なぜ関数は 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 で実現する

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 の設定は数分で完了し、数か月もかかりません。スマートフォン上のアプリも引き続き使えます。

無料トライアルを始める

確認日

Supabase Edge Functions で WhatsApp Webhook を受信