Уведомления посетителю
Виджет / Уведомления

Уведомления посетителю

Посетитель написал и ушёл — сообщим ему об ответе оператора: пушем в браузере, сообщением от вашего Telegram-бота или через ваш вебхук.

Как это работает

Включается в НастройкиКонструктор виджетаУведомления посетителю (visitor_notifications.enabled), по умолчанию выключено.

  • Посетитель в чате — ответ приходит в виджет, как обычно, никаких уведомлений.
  • Вкладка с сайтом открыта, но посетитель на другой вкладке — виджет сам показывает уведомление браузера (если посетитель их разрешил) и ставит «(1)» в заголовок вкладки. Второго уведомления с сервера не будет. Если страница уже «заморожена» (телефон свернул браузер), уведомление приходит с сервера, как для закрытой вкладки.
  • Виджет закрыт (ушёл с сайта, закрыл Mini App) — уведомление уходит одним из трёх путей ниже.

Не чаще одного уведомления в 5 минут на посетителя; когда он вернулся в чат, счёт начинается заново. Уведомляют только ответы операторов — внутренние заметки, сообщения бота и системные строки («оператор взял», «обращение закрыто») нет. Текст ответа попадает в уведомление, только если включить «Показывать текст ответа в уведомлении»: иначе на экране блокировки видно лишь, что поддержка ответила.

1. Сайт — Web Push

Push-уведомления сайта обслуживает service worker, а он должен лежать на домене самого сайта. Поэтому нужен один файл: создайте в корне сайта /supporthub-sw.js с единственной строкой:

/supporthub-sw.jsjs
importScripts('https://support.forestsnet.com/supporthub-visitor-sw.js');
  • Другое имя или папку укажите в поле «Путь к файлу на сайте». Если у сайта уже есть свой service worker, строку можно вписать в начало его файла и указать здесь его путь.
  • Файл должен отдаваться как JavaScript, без редиректов. Кнопка «Проверить файл на сайте» в конструкторе скажет, всё ли на месте.
  • Виджет регистрирует файл в своей области …/supporthub-push/: он не управляет страницами сайта, не перехватывает запросы и не заменяет ваш собственный worker. Обрабатывает он только наши уведомления.

Что видит посетитель: после первого сообщения в чате появляется строка «Сообщить, когда ответят? Да / Нет». Браузер спрашивает разрешение только после «Да» — никаких запросов при загрузке страницы. «Нет» запоминается на 30 дней. После подписки в чате видно «Сообщим, когда ответят · Отключить».

Нажатие на уведомление открывает страницу, с которой писал посетитель, сразу на этом диалоге (адрес с ?sh_ticket=…), а если вкладка с сайтом ещё открыта — переключает на неё.

2. Mini App вашего бота поддержки

Страница с виджетом открыта как Mini App бота, который подключён в НастройкиКаналыTelegram. Настраивать ничего не нужно: виджет передаёт данные запуска Mini App (initData), и SupportHub проверяет их подпись токеном этого бота.

  • После первого сообщения Telegram показывает свой диалог «Разрешить боту писать вам?». Если человек уже запускал бота или разрешил сообщения, диалога нет.
  • Ответ оператора приходит от бота: «Поддержка ответила» и кнопка «Открыть чат», которая открывает Mini App сразу на этом диалоге. Кнопка появляется, только если домен Mini App указан в «Разрешённых доменах» (Конструктор виджетаПриватность): адрес страницы сообщает браузер, и кнопку на чужой сайт бот не отправит. Без этого бот пишет «откройте приложение, чтобы прочитать ответ».
  • Если человек заблокировал бота, сообщения больше не отправляются.

Для диалога разрешения на странице должен быть подключён telegram-web-app.js.

3. Mini App другого бота

Виджет встроен в Mini App вашего собственного бота, которого нет в SupportHub. Писать от него мы не можем, поэтому, когда посетителя нет в чате, на ваш вебхук приходит событие visitor.reply_waiting, а сообщение отправляет ваш бот. Разрешение на запись здесь не спрашивается.

  1. Создайте вебхук с событием visitor.reply_waiting (или без фильтра событий) — см. Webhooks. Подпись и повторы — как у остальных событий.
  2. Чтобы знать, кому писать, передайте виджету подписанный visitor_token с полем telegram_user_id: ваш backend уже проверил initData своим токеном и знает пользователя. Без токена в событии не будет telegram_user_id — неподписанным данным из браузера мы не доверяем.
  3. По событию отправьте сообщение ботом (примеры ниже).

Токен с telegram_user_id

Формат и подпись — как у обычного visitor_token; поле telegram_user_id просто добавляется в payload.

token.pypython
import base64, hashlib, hmac, json, time

def visitor_token(user_id: str, telegram_user_id: int, secret: str) -> str:
    """Sign after your backend has validated the Mini App's initData."""
    payload = json.dumps(
        {"user_id": user_id, "telegram_user_id": telegram_user_id, "exp": int(time.time()) + 3600},
        separators=(",", ":"),
    ).encode()
    b64 = base64.urlsafe_b64encode(payload).rstrip(b"=").decode()
    return f"{b64}.{hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()}"
js
// On the Mini App page, once your backend returned the token:
window.SupportHub.identify({ visitor_token: token });

Событие visitor.reply_waiting

json
{
  "id": "5d0c9a…",
  "type": "visitor.reply_waiting",
  "timestamp": "2026-09-25T12:00:05.123456+00:00",
  "workspace_id": "27744f73-…",
  "data": {
    "contact_id": "9b1e0c52-…",
    "external_id": "42",
    "telegram_user_id": 123456789,
    "ticket_id": "0f04c7aa-…",
    "ticket_short_id": "0f04c7aa",
    "message_id": "c3a1b7e0-…",
    "operator_name": "Anna",
    "preview": null,
    "locale": "ru",
    "surface": "telegram_mini_app",
    "open_url": "https://app.example.com/support?sh_ticket=0f04c7aa-…",
    "replied_at": "2026-09-25T12:00:00+00:00"
  },
  "_links": {
    "ticket": "/api/v1/tickets/0f04c7aa-…",
    "messages": "/api/v1/tickets/0f04c7aa-…/messages",
    "contact": "/api/v1/contacts/9b1e0c52-…",
    "tickets": "/api/v1/contacts/9b1e0c52-…/tickets"
  }
}
contact_id
Тип: uuidПо умолчанию: —
Контакт в SupportHub.
external_id
Тип: string | nullПо умолчанию: —
Ваш id человека: user_id из подписанного visitor_token, иначе external_id контакта из API.
telegram_user_id
Тип: int | nullПо умолчанию: —
Telegram id — только проверенный: из подписанного visitor_token или из initData вашего бота поддержки.
ticket_id / ticket_short_id
Тип: uuid / stringПо умолчанию: —
Обращение; короткий номер — первые 8 символов, как в дашборде.
message_id
Тип: uuidПо умолчанию: —
Ответ оператора.
operator_name
Тип: string | nullПо умолчанию: —
Имя оператора, если в виджете включено «Показывать имя оператора».
preview
Тип: string | nullПо умолчанию: —
Начало ответа (до 120 символов) — только с включённым «Показывать текст ответа в уведомлении».
locale
Тип: stringПо умолчанию: —
Язык посетителя: ru / en.
surface
Тип: string | nullПо умолчанию: —
telegram_mini_app или web — где виджет открывался в последний раз.
open_url
Тип: string | nullПо умолчанию: —
Страница посетителя с ?sh_ticket=: виджет откроется на этом диалоге. Для кнопки web_app.
replied_at
Тип: ISO 8601По умолчанию: —
Когда оператор ответил.

Событие приходит и для посетителей сайта (surface: web), если у вас есть свой канал до них — письмо, пуш вашего приложения.

Примеры: бот отправляет уведомление

hook.pypython
# pip install aiogram aiohttp
import hashlib
import hmac
import json
import os

from aiohttp import web
from aiogram import Bot
from aiogram.exceptions import TelegramForbiddenError
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo

bot = Bot(os.environ["BOT_TOKEN"])
SECRET = os.environ["SUPPORTHUB_WEBHOOK_SECRET"].encode()
TEXTS = {"ru": ("Поддержка ответила", "Открыть чат"), "en": ("Support replied", "Open chat")}


async def supporthub_hook(request: web.Request) -> web.Response:
    body = await request.read()
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(request.headers.get("X-Webhook-Signature", ""), expected):
        return web.Response(status=401)

    event = json.loads(body)
    data = event["data"]
    if event["type"] != "visitor.reply_waiting" or not data.get("telegram_user_id"):
        return web.Response(status=204)

    text, button = TEXTS.get(data.get("locale"), TEXTS["en"])
    if data.get("preview"):
        text += "\n\n" + data["preview"]
    markup = None
    if data.get("open_url"):
        markup = InlineKeyboardMarkup(inline_keyboard=[[
            InlineKeyboardButton(text=button, web_app=WebAppInfo(url=data["open_url"])),
        ]])
    try:
        await bot.send_message(data["telegram_user_id"], text, reply_markup=markup)
    except TelegramForbiddenError:
        pass  # the user blocked the bot or never started it
    return web.Response(status=204)


app = web.Application()
app.router.add_post("/hooks/supporthub", supporthub_hook)
web.run_app(app, port=8080)

Где что не работает

  • iPhone и iPad, Safari. Web Push есть только у сайта, добавленного на экран «Домой» (iOS 16.4+). В обычной вкладке Safari виджет ничего не предлагает.
  • Встроенные браузеры (ссылка из Telegram, Instagram, Facebook, WebView приложений) — Web Push нет, предложения нет.
  • Telegram Mini App — Web Push нет: сообщение от вашего бота поддержки или событие вебхука (разделы 2 и 3).
  • Браузер запретил уведомления для сайта — виджет не предлагает; в скрытой вкладке остаётся только «(1)» в заголовке.
  • Приватное окно — подписка пропадает вместе с окном.
  • Выход из аккаунта (SupportHub.logout() или вход другого пользователя) отписывает браузер: ответы прежнему посетителю сюда больше не приходят.
Была ли страница полезной?