Identify посетителей
Виджет / Identify

Identify посетителей

Привязать визитора к контакту в SupportHub — три варианта в зависимости от того, что и когда вы знаете о пользователе.

Зачем это нужно

По умолчанию визитор анонимен — backend идентифицирует его по случайному vs_* id из cookie / localStorage / IndexedDB (см. multi-tier storage). Если ваш сайт уже знает пользователя (он залогинен в вашу CRM, личный кабинет, мобильное приложение), identify связывает анонимную сессию с реальным контактом — оператор сразу видит email/имя/телефон в карточке, история тикетов склеивается между устройствами.

Три способа

СпособКогда использовать
data-* атрибутыSSR-страница знает пользователя на момент рендера (Blade / EJS / Twig). Просто вписываете email/name в шаблон.
window.SupportHub.identify()SPA — пользователь логинится после загрузки страницы. Вызвать из onLogin-callback.
visitor_tokenCross-device идентификация: посетитель залогинен на телефоне И на ноутбуке — один контакт. Подписанный HMAC-токен от вашей CRM.

Способ 1: data-* атрибуты

Если ваш бекенд рендерит страницу в момент когда уже известен пользователь — впишите данные прямо в <script>-тег. Виджет прочитает их при загрузке и автоматически вызовет POST /widget/identify.

Важно: для data-атрибутов нужен прямой <script async src="…"> тег, а не IIFE-loader из Embed-кнопки. Loader создаёт новый script элемент через document.createElement, который не несёт data-атрибутов.

index.html (SSR)html
<script
  async
  src="https://support.forestsnet.com/widget-bundle?ws=YOUR_WORKSPACE_UUID"
  data-email="{{ user.email }}"
  data-name="{{ user.full_name }}"
  data-phone="{{ user.phone }}"
  data-telegram-id="{{ user.telegram_id }}"
></script>

Доступные data-* поля

data-email
Тип: stringПо умолчанию:
Email посетителя. Главное поле для дедупликации — если контакт с таким email уже существует, identify обновит его, не создаст новый.
data-name
Тип: stringПо умолчанию:
Полное имя посетителя. Идёт в full_name на бекенде.
data-phone
Тип: stringПо умолчанию:
Номер телефона (рекомендуется E.164: +79161234567). Сохраняется в extra_data.phone.
data-telegram-id
Тип: intПо умолчанию:
Numeric Telegram ID. Если у вас бот привязан к виджету — позволяет связать чаты в виджете и в TG в один контакт.
data-visitor-token
Тип: string (HMAC)По умолчанию:
HMAC-подписанный токен от вашей CRM — для cross-device идентификации (см. секцию ниже).

Способ 2: window.SupportHub.identify()

Для SPA — где пользователь логинится после загрузки страницы — вызовите программный API. Метод доступен сразу после загрузки бандла, не нужно ждать init(): вызовы до завершения init буферизуются и отправляются как только controller онлайн.

js
// onLoginSuccess callback
window.SupportHub.identify({
  email: user.email,
  name: user.fullName,
  phone: user.phone,
  telegram_id: user.telegramId,
});

Семантика

  • Контакт создаётся на бэке если его ещё нет (даже если посетитель не отправил ни одного сообщения).
  • Если контакт уже существует по visitor_session — поля обновляются. Email и имя считаются authoritative (последний identify побеждает); phone и telegram_id заполняются только если ещё не были.
  • Можно вызывать многократно — пустые поля не затирают существующие. Безопасно вызывать на каждый route change.
  • Сетевые ошибки не выбрасываются — identify это fire-and-forget с retry на следующем визите. Если бэк временно недоступен — повтор сработает при следующем заходе.

Способ 3: visitor_token (cross-device)

Когда один пользователь заходит с телефона и с ноутбука, стандартный visitor session ID (cookie-based) их не свяжет — это разные браузеры, разные cookies, разные contact rows. Чтобы тикеты с обоих устройств попадали в один контакт — генерируйте на своей стороне HMAC-подписанный токен с user_id и передавайте через data-visitor-token.

Откуда взять секрет

Workspace-уровневый widget_signing_secret создаётся автоматически при первом запросе и забирается через API:

http
GET /api/v1/widget/signing-secret
Authorization: Bearer sk_xxx

200 OK
{
  "secret": "wsig_..."
}

Endpoint workspace-scoped (определяется по API key). Секрет стабилен — повторный GET вернёт то же значение. Если секрет скомпрометирован — обратитесь в саппорт, для ротации требуется инвалидировать активные токены вручную (UI ротации в roadmap).

Как сгенерировать токен (Python)

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

# Получите секрет один раз через GET /api/v1/widget/signing-secret
# и храните в env. Не показывайте на клиенте никогда.
SECRET = "wsig_xxx"

def visitor_token(user_id: str, ttl: int = 86400) -> str:
    payload = {
        "user_id": user_id,
        "exp": int(time.time()) + ttl,
    }
    payload_bytes = json.dumps(payload).encode()
    payload_b64 = base64.urlsafe_b64encode(payload_bytes).rstrip(b"=").decode()
    # ВАЖНО: HMAC считается над raw JSON bytes (не над base64),
    # затем base64-кусок и hex-подпись склеиваются через точку.
    sig = hmac.new(SECRET.encode(), payload_bytes, hashlib.sha256).hexdigest()
    return f"{payload_b64}.{sig}"

Как сгенерировать токен (Node.js)

generate_visitor_token.jsjs
import crypto from "node:crypto";

const SECRET = "wsig_xxx";

export function visitorToken(userId, ttl = 86400) {
  const payload = { user_id: userId, exp: Math.floor(Date.now() / 1000) + ttl };
  const payloadBytes = Buffer.from(JSON.stringify(payload));
  const payloadB64 = payloadBytes.toString("base64url");
  // HMAC over the raw JSON bytes, NOT the base64 string.
  const sig = crypto
    .createHmac("sha256", SECRET)
    .update(payloadBytes)
    .digest("hex");
  return `${payloadB64}.${sig}`;
}

Endpoint на стороне хоста — пример FastAPI/Python

your_backend/api/supporthub.pypython
import base64, hashlib, hmac, json, time, os
from fastapi import APIRouter, Depends, HTTPException

router = APIRouter()

# Получите секрет один раз через GET /api/v1/widget/signing-secret
# на стороне SupportHub и сохраните в env / settings.
SUPPORTHUB_SIGNING_SECRET = os.environ["SUPPORTHUB_SIGNING_SECRET"]

@router.get("/token")
async def supporthub_widget_token(current_user = Depends(get_current_user)):
    """Возвращает HMAC-токен <payload_b64>.<sig> для cross-device виджета."""
    user_id = str(current_user.id)
    if not user_id:
        raise HTTPException(401, "Unauthorized")

    # Компактный JSON: separators=(",", ":") убирает лишние пробелы.
    # HMAC считается над raw JSON bytes (NOT над base64).
    payload = json.dumps(
        {"user_id": user_id, "exp": int(time.time()) + 3600},
        separators=(",", ":"),
    )
    sig = hmac.new(
        SUPPORTHUB_SIGNING_SECRET.encode(),
        payload.encode(),
        hashlib.sha256,
    ).hexdigest()
    payload_b64 = (
        base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=")
    )
    return {
        "token": f"{payload_b64}.{sig}",
        "expires_in": 3600,
    }

One-script установка с auto-token

Один <script> в <body>: сначала fetch'ит токен с вашего /token-endpoint'а, потом подгружает виджет с этим токеном. Если токен не пришёл (гость, 401, сетевая ошибка) — виджет грузится в анонимном режиме без задержки.

index.htmlhtml
<script>
(function(w, d){
  function loadWidget(token){
    var s = d.createElement('script');
    s.async = 1;
    s.src = 'https://support.forestsnet.com/widget-bundle?ws=YOUR_WORKSPACE_UUID';
    if (token) s.setAttribute('data-visitor-token', token);
    d.head.appendChild(s);
  }
  fetch('/api/supporthub/token', { credentials: 'include' })
    .then(function(r){ return r.ok ? r.json() : null; })
    .then(function(data){ loadWidget(data && data.token); })
    .catch(function(){ loadWidget(); });
})(window, document);
</script>

Передать в виджет

html
<script
  async
  src="https://support.forestsnet.com/widget-bundle?ws=YOUR_WORKSPACE_UUID"
  data-visitor-token="<%= visitor_token(user.id) %>"
></script>

Что происходит на бэке

Виджет (auto-boot после load + при каждом SupportHub.identify(...) вызове) отправляет:

http
POST /api/webhooks/widget/{workspace_id}/identify?visitor_id=vs_xxx
Content-Type: application/json

{
  "email": "user@example.com",
  "full_name": "Иван Петров",
  "phone": "+79161234567",
  "telegram_id": 123456789
}

Backend:

  1. Резолвит visitor_id (или visitor_token если он передан) в Contact.internal_id.
  2. Если контакта нет — создаёт его с переданными полями.
  3. Если контакт есть — обновляет email (всегда), full_name / phone / telegram_id (только если ещё не заполнены).
  4. Шлёт два webhook event'а подписчикам: contact.created (только при первом identify) + contact.identified (всегда). См. webhook docs.
  5. Если активный тикет существует и email обновился — пишет system event в timeline тикета (оператор видит «контакт подтвердил email: ivan@…»).

Безопасность

Никогда не передавайте чужие visitor_token или чужие email с frontend. Это приведёт к тому что один посетитель сможет читать тикеты другого. Visitor_token должен генерироваться на бэке вашей системы из текущей серверной сессии.

См. также

  • API-справочник виджета — полный список data-* атрибутов и JS API контроллера.
  • Webhooks — подписаться на contact.created / contact.identified чтобы получать события на свой бекенд.
Была ли страница полезной?