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_token | Cross-device идентификация: посетитель залогинен на телефоне И на ноутбуке — один контакт. Подписанный HMAC-токен от вашей CRM. |
Способ 1: data-* атрибуты
Если ваш бекенд рендерит страницу в момент когда уже известен пользователь — впишите данные прямо в <script>-тег. Виджет прочитает их при загрузке и автоматически вызовет POST /widget/identify.
Важно: для data-атрибутов нужен прямой <script async src="…"> тег, а не IIFE-loader из Embed-кнопки. Loader создаёт новый script элемент через document.createElement, который не несёт data-атрибутов.
<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-* поля
full_name на бекенде.+79161234567). Сохраняется в extra_data.phone.| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
| 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 онлайн.
// 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:
GET /api/v1/widget/signing-secret
Authorization: Bearer sk_xxx
200 OK
{
"secret": "wsig_..."
}Endpoint workspace-scoped (определяется по API key). Секрет стабилен — повторный GET вернёт то же значение. Если секрет скомпрометирован — обратитесь в саппорт, для ротации требуется инвалидировать активные токены вручную (UI ротации в roadmap).
Как сгенерировать токен (Python)
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)
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
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, сетевая ошибка) — виджет грузится в анонимном режиме без задержки.
<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>Передать в виджет
<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(...) вызове) отправляет:
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:
- Резолвит
visitor_id(илиvisitor_tokenесли он передан) вContact.internal_id. - Если контакта нет — создаёт его с переданными полями.
- Если контакт есть — обновляет email (всегда), full_name / phone / telegram_id (только если ещё не заполнены).
- Шлёт два webhook event'а подписчикам:
contact.created(только при первом identify) +contact.identified(всегда). См. webhook docs. - Если активный тикет существует и email обновился — пишет system event в timeline тикета (оператор видит «контакт подтвердил email: ivan@…»).
Безопасность
Никогда не передавайте чужие visitor_token или чужие email с frontend. Это приведёт к тому что один посетитель сможет читать тикеты другого. Visitor_token должен генерироваться на бэке вашей системы из текущей серверной сессии.
См. также
- API-справочник виджета — полный список
data-*атрибутов и JS API контроллера. - Webhooks — подписаться на
contact.created/contact.identifiedчтобы получать события на свой бекенд.