UnifyPort 入站集成:先建 Webhook 的检查清单
做跨境客服或东南亚运营的消息集成时,第一步应该是先建 webhook,而不是先接账号。UnifyPort 没有通用的 REST 消息历史读取 API,也不保证错过的 payload 一定可重放;因此 webhook 就是你的入站记录层。先注册 POST /v1/webhook-endpoints,开启 signing_secret,订阅 message.received 或 [*],把事件入库,再交给客服系统、CRM、AI 或自动化流程。
关键结论
- 先注册 webhook,再连接生产消息账号。
- 设置
signing_secret,让投递带上X-Device-Timestamp和X-Device-Signature。 - 慢任务开始前,先保存标准事件 envelope。
- 只做入站 inbox 时,订阅
message.received,并检查data.message.direction === "inbound"。 - 事件过滤、签名验证、重试处理和业务路由要分层实现。
为什么 webhook 要放在最前面
客户消息可能在 CRM、AI agent 或共享 inbox 准备好之前就已经到达。如果接收端还没注册,就不能假设之后一定能通过消息历史 API 把它补回来。UnifyPort 的 Quickstart 也把 webhook 注册放在账号授权之前。
这篇清单可以和两篇已有教程一起看:Webhook HMAC replay protection 讲时间戳、签名和幂等,UnifyPort webhook event filters 讲 subscribed_events 和通配符该怎么选。这里把它们串成一个落地顺序。
步骤 1:创建一个 signed endpoint
真实 API 路由是 POST /v1/webhook-endpoints。入站 inbox 可以只订阅 message.received;完整事件收集器可以用 [*]。
curl -X POST https://api.unifyport.ai/v1/webhook-endpoints \
-H "X-Api-Key: $UNIFYPORT_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"url\": \"$PUBLIC_WEBHOOK_URL\",
\"status\": \"active\",
\"subscribed_events\": [\"message.received\"],
\"signing_secret\": \"$WEBHOOK_SIGNING_SECRET\",
\"retry_policy\": { \"max_attempts\": 3 }
}"
retry_policy.max_attempts 表示初次投递之后还能重试几次。文档中的默认值是 3,允许范围是 0 到 5。接入时请以 Create webhook endpoint 为准。
步骤 2:验证原始请求体
开启签名后,UnifyPort 会发送 X-Device-Signature。它是下面这个字符串的 HMAC-SHA256 十六进制摘要:
<X-Device-Timestamp>.<raw request body>
接收端必须在 JSON 解析或重新序列化之前验证原始 bytes。Node.js 的 crypto.createHmac() 和 crypto.timingSafeEqual() 适合这个模式;官方文档也要求 timingSafeEqual() 比较的 buffer 长度一致。
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;
app.post('/webhooks/unifyport', express.raw({ type: 'application/json' }), async (req, res) => {
const timestamp = req.get('X-Device-Timestamp') ?? '';
const signature = req.get('X-Device-Signature') ?? '';
const expected = crypto
.createHmac('sha256', signingSecret)
.update(timestamp + '.')
.update(req.body)
.digest('hex');
const valid =
/^[0-9a-f]{64}$/i.test(signature) &&
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
await inbox.insertIfAbsent(event.id, event);
return res.sendStatus(202);
});
生产环境还要补上时间戳新鲜度、持久化幂等和重试确认策略,细节见 Webhook delivery and signature verification。
步骤 3:先保存标准事件 envelope
message.received 在不同 provider 上保持相同顶层结构:
{
"id": "evt_2f9c1a4b7e",
"type": "message.received",
"provider": "whatsapp",
"account_id": "acc_8c21d0",
"occurred_at": "2026-06-08T12:34:56Z",
"data": {
"conversation": { "id": "8613912345678", "type": "user", "title": "Jordan Lee" },
"sender": { "id": "8613912345678", "name": "Jordan Lee", "type": "user" },
"message": {
"id": "wamid.HBgM",
"text": "Hi - is my order shipped yet?",
"direction": "inbound",
"sent_at": "2026-06-08T12:34:55Z"
}
}
}
至少保存 id、type、provider、account_id、occurred_at、data.conversation.id、data.sender.id 和 data.message.id。如果后面接 n8n,可以参考 n8n WhatsApp AI agent tutorial:n8n 适合处理可信事件,不适合承担第一道安全边界。
限制与取舍
非官方接口适合需要从普通或既有消息账号接收入站消息的团队;如果你需要平台认证、官方商业功能或 provider 级政策保证,应选择对应官方 API。另一个常见误区是把 message.received 当成只代表入站:它可能包含入站或出站,inbox 流程必须检查 data.message.direction。
FAQ
应该先订阅哪个事件?
只做入站 inbox,就先订阅 message.received。只有当端点是通用事件收集器时,才使用 [*]。
签名验证就足够防重放吗?
不够。HMAC 验证完整性和共享密钥,仍然需要时间戳新鲜度和按事件 ID 的持久化去重。
CRM 写入完成前可以返回 200 吗?
可以,但前提是事件已经进入你的持久化 inbox 或队列。CRM、AI 和通知可以异步处理。
以后还能加 LINE、Zalo 或 X 吗?
可以。统一 envelope 里有 provider 和 account_id,下游按平台分支即可,入口仍然是同一个 signed webhook。
下一步
打开 Create webhook endpoint,先注册 receiver,再按 webhook delivery guide 完成验证和确认策略。
Sources
官方来源核对日期:2026-08-26。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。