← 所有文章
教程

如何在 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_idup_to_message_sender_id 必须成对出现;只提交一个会返回 400 invalid_request
  • 标记未读只需要 conversation_id
  • 分配、跟进和解决状态仍应保存在自己的系统中。渠道侧已读状态只是共享收件箱的一种投影,并不是完整工单库。

先区分三种“已读”

可靠的共享收件箱需要把以下三种状态分开:

  1. 团队队列状态:例如新消息、已分配、等待中、已解决。这些状态由你的应用定义。
  2. 已连接 WhatsApp 账号的聊天列表状态:已读或未读。通过标记会话已读接口标记会话未读接口更新。
  3. 收件人回执message.read Webhook 表示收件人读过账号发出的某一条或多条消息,不等于客服打开了一张入站工单。

本地会话设置发生变化时,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 事件。为本地发起的操作保存内部操作记录,让后续事件用于确认状态,而不是再次触发同一个动作。

一个稳妥的流程是:

  1. 幂等保存 message.received
  2. 在事务中更新本地工单状态。
  3. 调用渠道状态动作。
  4. API 返回 { "data": { "ok": true } } 后再记录成功。
  5. conversation.updated 视为确认,或视为来自已连接账号的外部变化。
  6. 状态不确定时,通过获取指定会话接口读取该会话,并比较 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 日核对: