ローカルホストで WhatsApp Cloud API Webhook をテストするには、HTTPS 経由でローカル エンドポイントを公開し、メタの GET 検証チャレンジを実装してから、生の本文に対して各 POST リクエストを検証します。X-Hub-Signature-256 メタ アプリにトンネル URL を登録し、messages WhatsApp ビジネス アカウントをサブスクライブし、デプロイせずに実際のペイロードを受信するテスト メッセージを送信します。
WhatsApp Webhook は 2 つの異なる検証フローを使用します
最も重要な違いは、Webhook のセットアップと Webhook 配信が異なる方法で認証されることです。セットアップ中に、Meta は、GET リクエストを送信します。エンドポイントは検証トークンを比較し、チャレンジをプレーン テキストとして返します。その後、イベント配信は hub.mode、hub.verify_token、および hub.challenge を含む POST リクエストになります。これらは、メタ アプリ シークレットを使用して作成された HMAC 署名を検証することによって認証される必要があります。
検証トークンは、選択したランダムな文字列です。これは WhatsApp アクセス トークンでも、App Secret でもありません。チャレンジを返すことは、コールバック エンドポイントの制御を証明します。今後の POST リクエストは認証されません。 Meta の公式 WhatsApp Webhook ガイドコールバック設定、サブスクリプション、Webhook フィールドについて説明します。
Next.js App Router エンドポイントを作成する
以下のルートは両方のフェーズを処理します。 request.text() を使用して POST データを読み取ると、署名検証に必要な正確なバイトが保持されます。
// app/api/webhooks/whatsapp/route.ts
import crypto from 'node:crypto';
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
const mode = request.nextUrl.searchParams.get('hub.mode');
const token = request.nextUrl.searchParams.get('hub.verify_token');
const challenge = request.nextUrl.searchParams.get('hub.challenge');
if (mode === 'subscribe' && token === process.env.META_VERIFY_TOKEN) {
return new Response(challenge ?? '', { status: 200 });
}
return new Response('Forbidden', { status: 403 });
}
export async function POST(request: Request) {
const rawBody = await request.text();
const supplied = request.headers.get('x-hub-signature-256') ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.META_APP_SECRET!)
.update(rawBody)
.digest('hex');
const a = Buffer.from(supplied);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
await enqueueWhatsAppPayload(payload);
return new Response('EVENT_RECEIVED', { status: 200 });
}
request.json() を呼び出してから HMAC 用に JSON を再構築しないでください。空白、エスケープ、またはキーの順序が変更され、異なるダイジェストが生成される場合があります。 Express を使用する場合は、グローバル JSON パーサーの前に Buffer をキャプチャします。一般的なWebhook 署名ガイドでは、フレームワーク全体での raw-body の処理について説明しています。
トンネルを開始し、コールバックを構成します。
- Next.js アプリをローカルで実行します。通常は、
npm run devポート 3000 で実行します。 - 実行
npx portpreview 3000別のターミナルで。 META_VERIFY_TOKENをランダムな値に設定し、META_APP_SECRETを Meta のアプリ設定から App Secret に設定します。- Meta 開発者ダッシュボードで、WhatsApp 製品の設定ページを開きます。
- コールバック URL を
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/whatsappに設定し、同じ検証トークンを入力します。 - 検証が成功したら、WhatsApp ビジネス アカウントの
messagesフィールドをサブスクライブします。
トンネルは、GET チャレンジと後続の POST 配信の両方でアクティブのままにする必要があります。古いセッションからコピーされた URL は解決される可能性がありますが、マシンに転送されなくなるため、ローカル トンネルが変更されるたびに正確なコールバックを確認してください。
GET チャレンジを個別にテストする
ダッシュボードを使用する前に、リクエストをローカルで再現します。
curl -i \
"http://localhost:3000/api/webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=${META_VERIFY_TOKEN}&hub.challenge=123456"
正しい応答は、JSON や JSON ではなく、本文付きのステータス 200 123456です。"123456" 引用符付き。トークンが間違っている場合は、403 が適切です。検証トークンがそこに表示されるため、クエリ パラメータをログに記録しないでください。
ビジネス ロジックを作成する前にメッセージ ペイロードを理解する
WhatsApp はデータを複数のレベルでラップします。一般的な通知には、object: "whatsapp_business_account"、entry 配列、changes 配列、および field が messages である変更があります。 value 内では、受信したユーザー コンテンツが messages に表示されます。送信したメッセージの配信、既読、および失敗の更新は statuses に表示されます。
for (const entry of payload.entry ?? []) {
for (const change of entry.changes ?? []) {
if (change.field !== 'messages') continue;
for (const message of change.value.messages ?? []) {
await handleInboundMessage({
id: message.id,
from: message.from,
type: message.type,
text: message.text?.body,
});
}
for (const status of change.value.statuses ?? []) {
await updateDeliveryStatus(status.id, status.status);
}
}
}
すべての通知にテキスト メッセージが含まれていると想定しないでください。画像、音声、ドキュメント、場所、対話型応答、システム メッセージ、ステータスのみのペイロードにはさまざまな形状があります。ディスパッチャーを message.type キーで保持し、オプションのフィールドを検証し、不明なイベント タイプをクラッシュせずにレビュー用に保持します。
POST 署名を正しく検証します。
X-Hub-Signature-256 値は sha256=<hex digest> の形式を使用します。 メタ アプリ シークレットを使用して、生のリクエスト バイトに対して HMAC-SHA256 を計算します。永続的または一時的な WhatsApp アクセス トークンは、Graph API 呼び出しに使用されます。 HMAC キーではありません。定数時間比較を使用し、不足している署名を拒否します。
ローカル検証を有効にしておきます。トンネル URL を学習した人は誰でも、それに任意の JSON を POST できます。検証を行わないと、偽造イベントによって自動応答がトリガーされたり、CRM レコードが変更されたり、顧客の状態が暴露されたりする可能性があります。 App Secret が誤ってコミット、印刷、または共有された場合は、ローテーションします。
迅速に確認し、メッセージの重複を排除します。
認証してイベントを永続的にキューに追加した後、200 を返します。メディアのダウンロード中、LLM の呼び出し中、またはいくつかのサービスの更新中は待たないでください。プロバイダーは、確認応答が失敗した場合に配信を再試行します。ネットワークのあいまいさは、重複が正常であることを意味します。
WhatsApp メッセージを受信メッセージおよびステータス オブジェクトの冪等キーとして使用します。id処理された ID に一意の制約を設定します。ステータスの遷移は、送信済みから配信済み、読み取りへと正当に進行する可能性があるため、後の状態を破棄せずに、関連する各遷移の重複を排除します。
WhatsApp Webhook 設定のトラブルシューティング
コールバック URL を検証できませんでした
パブリック URL を介して GET ルートをテストします。 GET を受け入れ、正確な検証トークンを比較し、チャレンジのみで応答することを確認します。リダイレクト、認証ミドルウェア、ロケールの書き換え、または JSON ラッパーによって検証が中断される可能性があります。実行中の開発プロセスによって環境変数が読み込まれていることを確認します。
検証は成功しましたが、メッセージが到着しません
コールバック検証だけでは、WhatsApp ビジネス アカウントをフィールドに登録しません。ダッシュボードでmessages サブスクリプションと、電話番号が予想されるアプリとアカウントに属していることを確認します。アプリがまだ開発モードにある場合は、許可された受信者からメッセージを送信します。
すべての POST が署名の検証に失敗します
通常の原因は、App Secret の代わりにアクセス トークンを使用すること、解析された JSON をハッシュすること、sha256= プレフィックスを省略すること、または異なるエンコーディングを比較することです。本文の長さとヘッダーが存在するかどうかをログに記録しますが、シークレットや完全な顧客ペイロードは出力しません。
テキスト メッセージは機能しますが、メディアの処理が失敗します。
メディア通知には ID が含まれますが、必ずしもファイル バイトが含まれるとは限りません。有効なアクセス トークンを使用して Graph API を通じてメディアを取得し、ダウンロードします。遅いワークフローを Webhook 確認パスの外側に保ちます。
ローカル エンドポイントは重複したイベントを認識します。
応答ステータスと遅延を検査し、永続的な冪等性を追加し、各修正後にキャプチャされた 1 つのイベントを再生します。 Webhook リプレイ ガイドでは、コード変更ごとに新しい実際のメッセージの送信を避ける方法が示されています。
ローカル テスト中に顧客データを保護する
- 可能な場合は、テスト用の電話番号と合成会話を使用します。
- 電話番号、メッセージ本文、メディア URL、連絡先、プロフィール名を秘匿化します。
- アプリ シークレットを保存し、トークンにアクセスし、無視された環境ファイルまたはシークレット マネージャーにのみトークンを検証します。
- トンネル キャプチャを表示できるユーザーを制限し、デバッグ セッション後に削除します。
- ビジネス アクションを実行する前にオブジェクト、フィールド、アカウント ID を検証します。
トンネルを使用すると反復が高速になりますが、また、実稼働環境に基づいた個人データを開発者マシンにもたらします。実際のユーザーでテストする前に、トンネル セキュリティ チェックリストのコントロールを適用してください。
