如何在 WhatsApp 共享收件箱中同步已读与未读状态
WhatsApp 共享收件箱不应在 Webhook 刚到达时就把会话标为已读,而应等团队真正接单或解决后再更新。通过 UnifyPort,可以用 conversation_id 调用会话已读接口;如果要把 WhatsApp 回执精确推进到某条消息,还要同时提交该消息 ID 和发送者 ID。需要再次处理时,则把会话标为未读。
核心结论
- 会话已读和未读动作目前仅支持 WhatsApp;其他渠道不支持该动作时会返回
501 unsupported_by_provider。 POST /v1/accounts/{account_id}/conversations/read必须接收conversation_id,也可以选择推进到某条消息。up_to_message_id与up_to_message_sender_id必须成对出现;只提交一个会返回400 invalid_request。- 标记未读只需要
conversation_id。 - 分配、跟进和解决状态仍应保存在自己的系统中。渠道侧已读状态只是共享收件箱的一种投影,并不是完整工单库。
先区分三种“已读”
可靠的共享收件箱需要把以下三种状态分开:
- 团队队列状态:例如新消息、已分配、等待中、已解决。这些状态由你的应用定义。
- 已连接 WhatsApp 账号的聊天列表状态:已读或未读。通过标记会话已读接口和标记会话未读接口更新。
- 收件人回执:
message.readWebhook 表示收件人读过账号发出的某一条或多条消息,不等于客服打开了一张入站工单。
本地会话设置发生变化时,UnifyPort 还可以映射 conversation.updated 事件。data.conversation.id 用来识别聊天,已读状态变化可以出现在 data.read。应把它作为对账信号;至于谁接单、何时解决、为何重新打开,仍要记录在自己的数据库中。
处理事件之前,要先验证原始请求体签名,并让重试具备幂等性。具体边界可参考Webhook HMAC、重放防护与重试指南。如果接收端只处理收件箱事件,也可以参考Webhook 事件过滤教程,只订阅需要的事件。
决定何时更新渠道状态
不要在每次入站 Webhook 到达时自动标为已读,否则无人处理的队列看起来也会像已清空。建议先制定明确规则:
| 团队动作 | 本地队列状态 | WhatsApp 动作 |
|---|---|---|
| 入站消息已持久化 | new | 不操作 |
| 客服接下会话 | assigned | 如有需要,推进到已接收的消息 |
| 客服解决会话 | resolved | 整个会话标为已读 |
| 客服要求后续跟进 | waiting | 会话标为未读 |
| 自动化在分配前失败 | new | 不操作 |
这样也能避免浏览器刷新、Webhook 重试或后台预览意外清空待办。
标记 WhatsApp 会话已读
conversation_id 放在 JSON 请求体中,而不是 URL 路径中,因为渠道标识可能包含 @、: 等字符。
将整个会话标为已读:
curl -X POST "https://api.unifyport.ai/v1/accounts/$UNIFYPORT_ACCOUNT_ID/conversations/read" \
-H "X-Api-Key: $UNIFYPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "8613912345678@s.whatsapp.net"
}'
如果要把 WhatsApp 回执推进到一条确定的入站消息,应从同一个 message.received 事件复制三个标识:
{
"conversation_id": "120363041234567890@g.us",
"up_to_message_id": "CURRENT-MESSAGE-ID",
"up_to_message_sender_id": "8613912345678@lid"
}
群聊中,up_to_message_sender_id 必须使用匹配的 data.sender.id,不能从会话 ID 推导。如果不需要消息级回执,请同时省略两个 up_to_message_* 字段。
下面的 Node.js helper 会先检查字段是否成对,再发起请求:
const apiBase = 'https://api.unifyport.ai/v1';
async function setWhatsAppReadState({ accountId, conversationId, unread, message }) {
const action = unread ? 'unread' : 'read';
const body = { conversation_id: conversationId };
if (!unread && message) {
if (!message.id || !message.senderId) {
throw new Error('message.id and message.senderId must be supplied together');
}
body.up_to_message_id = message.id;
body.up_to_message_sender_id = message.senderId;
}
const response = await fetch(
`${apiBase}/accounts/${encodeURIComponent(accountId)}/conversations/${action}`,
{
method: 'POST',
headers: {
'X-Api-Key': process.env.UNIFYPORT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}
);
if (!response.ok) {
const failure = await response.json();
throw new Error(`${response.status} ${failure.error?.code ?? 'unknown_error'}`);
}
return response.json();
}
在已完成签名验证的事件处理器中,直接使用文档定义的字段:
await setWhatsAppReadState({
accountId: event.account_id,
conversationId: event.data.conversation.id,
unread: false,
message: {
id: event.data.message.id,
senderId: event.data.sender.id
}
});
为待跟进会话标记未读
未读动作更简单:
await setWhatsAppReadState({
accountId: event.account_id,
conversationId: event.data.conversation.id,
unread: true
});
只应在团队明确重新打开任务时使用。渠道侧未读状态不能替代自己的提醒系统;跟进负责人、到期状态和原因都应存入本地队列。
对账时避免形成循环
应用调用会话动作后,Webhook 可能收到对应的 conversation.updated 事件。为本地发起的操作保存内部操作记录,让后续事件用于确认状态,而不是再次触发同一个动作。
一个稳妥的流程是:
- 幂等保存
message.received。 - 在事务中更新本地工单状态。
- 调用渠道状态动作。
- API 返回
{ "data": { "ok": true } }后再记录成功。 - 把
conversation.updated视为确认,或视为来自已连接账号的外部变化。 - 状态不确定时,通过获取指定会话接口读取该会话,并比较
unread_count。
为其他渠道启用控件前,应查看当前的各渠道统一动作支持矩阵。统一路由并不表示每个渠道都实现了每一种动作。
限制与取舍
如果业务依赖渠道认证的企业能力、官方模板消息或原生治理,官方方案可能更合适。UnifyPort 的非官方接口适合普通账号消息工作流,但不会让所有渠道拥有完全相同的功能。
就本文场景而言,会话已读和未读动作目前仅支持 WhatsApp。其他渠道的共享收件箱应隐藏或禁用这些控件,不要把 501 unsupported_by_provider 当成可不断重试的临时错误。
常见问题
收到 message.received 会自动把聊天标为已读吗?
不会。接收和存储事件不应清空待办,只有到达团队定义的工作流节点时才调用已读接口。
message.read 与标记会话已读有什么区别?
message.read 是收件人阅读消息后的回执事件;会话已读动作改变的是已连接账号本地聊天列表的状态。
可以只提交 up_to_message_id 吗?
不可以。它必须与 up_to_message_sender_id 一起提交;或者两个都省略,直接把整个会话标为已读。
Telegram、LINE、TikTok、Zalo 和 X 能使用同样的已读/未读动作吗?
目前不能。支持矩阵把会话已读和未读列为 WhatsApp 专属动作;不支持的组合会返回 501 unsupported_by_provider。
下一步
先阅读标记会话已读 API Reference,并在定义好本地重新打开规则后,再接入未读动作。
一手来源
截至 2026 年 8 月 21 日核对: