要在本地主机上测试 WhatsApp Cloud API Webhook,请通过 HTTPS 公开您的本地端点,实施 Meta 的 GET 验证质询,然后根据原始正文验证每个 POST 请求。X-Hub-Signature-256。在您的 Meta 应用程序中注册隧道 URL,订阅 WhatsApp Business 帐户messages,然后发送测试消息以接收真实的负载,而无需进行部署。
WhatsApp Webhooks 使用两种不同的验证流程
最重要的区别是 Webhook 设置和 Webhook 传递的身份验证方式不同。在设置过程中,Meta 会发送 GET 请求,其中包含 hub.mode、hub.verify_token 和 hub.challenge。您的端点比较验证令牌并以纯文本形式返回质询。之后,事件传递是POST请求;这些应该通过验证使用您的Meta 应用程序密钥创建的 HMAC 签名来进行身份验证。
验证令牌是您选择的随机字符串;它不是 WhatsApp 访问令牌,也不是应用程序密钥。返回质询证明了对回调端点的控制。它不会验证未来的 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 签名指南解释了跨框架的原始正文处理。
启动隧道并配置回调
- 在本地运行 Next.js 应用程序,通常使用
npm run dev在端口 3000 上运行。 - 运行
npx portpreview 3000在单独的终端中。 - 将
META_VERIFY_TOKEN设置为随机值,META_APP_SECRET设置为 Meta 应用程序设置中的应用程序密钥。 - 在 Meta 开发者仪表板中,打开 WhatsApp 产品的配置页面。
- 将回调 URL 设置为
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/whatsapp并输入相同的验证令牌。 - 验证成功后,订阅 WhatsApp Business 帐户的
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"
正确的响应是带有正文的状态 200 123456,而不是 JSON,也不是 "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>。使用 Meta 应用程序密钥在原始请求字节上计算 HMAC-SHA256。永久或临时 WhatsApp 访问令牌用于 Graph API 调用;它不是 HMAC 密钥。使用恒定时间比较并拒绝丢失的签名。
保持启用本地验证。任何知道隧道 URL 的人都可以向其中 POST 任意 JSON。如果没有验证,伪造的事件可能会触发自动回复、改变 CRM 记录或暴露客户状态。如果意外提交、打印或共享应用程序密钥,则轮换应用程序密钥。
快速确认并删除重复消息
在对事件进行身份验证和持久排队后返回 200。下载媒体、致电法学硕士或更新多项服务时不要等待。当确认失败时,提供商会重试传送,并且网络模糊性意味着重复是正常的。
使用 WhatsApp 消息id作为入站消息和状态对象的幂等键。对已处理的 ID 施加唯一约束。状态转换可以合法地从已发送到已交付再到已读取,因此可以对每个相关转换进行重复数据删除,而不会丢弃后续状态。
对 WhatsApp Webhook 设置进行故障排除
无法验证回调 URL
通过公共 URL 测试 GET 路由。确保它接受 GET,比较确切的验证令牌,并仅使用质询进行响应。重定向、身份验证中间件、区域设置重写或 JSON 包装器可能会破坏验证。确认正在运行的开发进程已加载环境变量。
验证成功,但没有消息到达
仅回调验证不会将 WhatsApp Business 帐户订阅到字段。在仪表板中确认 messages 订阅以及电话号码属于预期的应用和帐户。如果应用仍处于开发模式,则从允许的收件人发送消息。
每个 POST 都无法签名验证
常见原因是使用访问令牌而不是应用程序密钥、对解析的 JSON 进行哈希处理、省略 sha256= 前缀或比较不同的编码。记录正文长度以及标头是否存在,但绝不打印秘密或完整的客户负载。
文本消息有效,但媒体处理失败
媒体通知包含 ID,不一定是文件字节。使用有效的访问令牌通过 Graph API 获取媒体,然后下载。将较慢的工作流程保留在 Webhook 确认路径之外。
本地端点会看到重复事件
检查响应状态和延迟,添加持久幂等性,并在每次修复后重播一个捕获的事件。 webhook 重播指南展示了如何避免为每次代码更改发送新的真实消息。
在本地测试期间保护客户数据
- 尽可能使用测试电话号码和合成对话。
- 编辑电话号码、消息正文、媒体 URL、联系人和个人资料日志中的名称。
- 仅在忽略的环境文件或秘密管理器中存储应用程序密钥、访问令牌和验证令牌。
- 限制谁可以查看隧道捕获并在调试会话后将其删除。
- 在执行业务操作之前验证对象、字段和帐户标识符。
隧道使迭代速度更快,但它也带来了生产型个人信息数据到开发者机器。在与真实用户进行测试之前,应用隧道安全检查表中的控制。
