← Все статьи
Руководство

Как синхронизировать статусы «прочитано» и «не прочитано» в общем ящике WhatsApp

В общем ящике WhatsApp не следует помечать диалог прочитанным сразу после получения Webhook. Меняйте статус, когда сотрудник действительно принял обращение или завершил работу. В UnifyPort для этого достаточно вызвать действие read с conversation_id. Чтобы отправить квитанцию до конкретного сообщения WhatsApp, передайте вместе идентификатор сообщения и идентификатор его отправителя. Если обращение требует повторной обработки, пометьте диалог непрочитанным.

Ключевые выводы

  • Действия read и unread для диалогов сейчас поддерживаются только в 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 — это проекция состояния, а не полная база обращений.

Разделите три значения слова «прочитано»

Надёжный общий ящик должен различать три независимых состояния:

  1. Состояние внутренней очереди: новое, назначено, ожидает, решено или другие этапы вашего приложения.
  2. Состояние списка чатов подключённого аккаунта WhatsApp: прочитано или не прочитано. Его меняют endpoint пометки диалога прочитанным и endpoint пометки непрочитанным.
  3. Квитанция получателя: событие message.read означает, что адресат прочитал одно или несколько исходящих сообщений. Оно не означает, что оператор открыл входящее обращение.

При изменении локальной настройки диалога UnifyPort может сопоставить событие conversation.updated. Чат определяется по data.conversation.id, а изменение состояния чтения может находиться в data.read. Используйте это событие для сверки. Кто принял обращение, когда оно было закрыто и почему открыто повторно, по-прежнему должно храниться в вашей базе.

До применения события проверяйте подпись исходного тела запроса и обрабатывайте повторы идемпотентно. Граница приёма описана в руководстве по HMAC, защите от повторов и доставке Webhook. Если endpoint обслуживает только входящую очередь, подписывайтесь на нужные события явно — это показано в руководстве по фильтрам Webhook.

Выберите момент изменения статуса WhatsApp

Не помечайте прочитанным каждый входящий 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 этого сообщения. Не выводите его из идентификатора диалога. Если точная граница не нужна, не передавайте оба поля up_to_message_*.

Небольшая функция Node.js проверяет пару до вызова API:

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
  }
});

Как пометить диалог непрочитанным

Для unread нужен более короткий вызов:

await setWhatsAppReadState({
  accountId: event.account_id,
  conversationId: event.data.conversation.id,
  unread: true
});

Используйте его, когда команда сознательно возвращает обращение в работу. Не полагайтесь на непрочитанное состояние провайдера как на единственное напоминание: сохраняйте владельца, срок и причину во внутренней очереди.

Сверяйте состояние без цикла

После вызова действия приложение может получить соответствующее событие conversation.updated. Записывайте инициированные вами операции во внутренний журнал, чтобы последующее событие подтверждало состояние, а не запускало то же действие ещё раз.

Безопасная последовательность:

  1. Идемпотентно сохранить message.received.
  2. Обновить локальное состояние обращения в транзакции.
  3. Вызвать действие состояния провайдера.
  4. Зафиксировать успех после ответа { "data": { "ok": true } }.
  5. Обработать conversation.updated как подтверждение или внешнее изменение со стороны подключённого аккаунта.
  6. При сомнении запросить конкретный диалог через Get conversation и сравнить unread_count.

Перед включением элемента управления для другого канала проверьте актуальную матрицу действий по провайдерам. Единый маршрут не означает, что каждый провайдер реализует каждое действие.

Ограничения и компромиссы

Официальное подключение лучше подходит, если процесс зависит от сертифицированных бизнес-функций, официальных шаблонов или встроенного управления. Неофициальный интерфейс UnifyPort предназначен для работы с сообщениями обычных аккаунтов, но не делает набор функций всех каналов одинаковым.

В описанном процессе read и unread сейчас доступны только для WhatsApp. Для остальных провайдеров скрывайте или отключайте эти элементы управления. Не считайте 501 unsupported_by_provider временной ошибкой, которую нужно постоянно повторять.

Частые вопросы

Событие message.received автоматически помечает чат прочитанным?

Нет. Приём и сохранение события не должны очищать очередь. Вызывайте read только в выбранной бизнес-точке.

Чем message.read отличается от пометки диалога прочитанным?

message.read — квитанция о том, что получатель прочитал отправленные сообщения. Действие над диалогом меняет локальное состояние списка чатов подключённого аккаунта.

Можно передать только up_to_message_id?

Нет. Передавайте его вместе с up_to_message_sender_id либо не передавайте оба поля, чтобы пометить весь диалог.

Можно использовать эти действия для Telegram, LINE, TikTok, Zalo и X?

Сейчас нет. Матрица указывает read и unread для диалогов только у WhatsApp. Неподдерживаемые комбинации возвращают 501 unsupported_by_provider.

Следующий шаг

Начните с API Reference для пометки диалога прочитанным, а действие unread добавляйте после определения локальных правил повторного открытия.

Первичные источники

Проверено 21 августа 2026 года: