Виджет для сайта
Как подключить чат поддержки RevBack к сайту. Один тег скрипта — и на сайте появляется кнопка чата; по желанию — своя кнопка, счётчик непрочитанных и связка с пользователем сайта.
Что делает виджет
| Скрипт | https://api.revback.ai/widget.js |
|---|---|
| Технология | Обычный скрипт без зависимостей; интерфейс — в Shadow DOM: стили сайта не протекают в виджет, а стили виджета — на сайт |
| Языки | Русский и английский |
| Что делает | Переписка посетителя с оператором и AI-ассистентом, вложения, счётчик непрочитанных,
«Оператор печатает…», ✓✓ — оператор прочитал, ответ на email, если посетитель его оставил, связка с пользователем
сайта (identify) |
Ключ канала публичный — он виден в коде страницы. Секрет подписи токенов живёт только на вашем бэкенде: в браузер его не кладут.
Шаг 1. Вставить код
Тег — перед </body> на всех страницах, где нужен чат.
<!-- перед </body> -->
<script src="https://api.revback.ai/widget.js" data-widget-key="ваш ключ"></script>Где взять ключ: кабинет RevBack → Каналы → «Чат на сайте» → блок «Установка на сайт» — там же готовый код вставки. Или поле «Ключ канала» в настройках канала. «Чат на сайте» — имя канала по умолчанию, у вас оно может быть другим.
- Адрес API настраивать не нужно: виджет обращается туда же, откуда загружен скрипт.
data-locale="ru|en"— язык интерфейса. Без атрибута — язык браузера; всё, кроме английского, — русский.data-position="left|right"— сторона кнопки. Действует, только пока положение кнопки не задано в настройках канала в кабинете: заданное там главнее.asyncиdeferможно; тег можно вставить и скриптом. Свой код, который обращается кwindow.RevBack, тогда запускайте в обработчикеloadтега скрипта —addEventListener('load', …), примеры ниже. Если тег виджета безasync/defer— свой код можно просто положить в обычный тег после него.- До загрузки
widget.jsвызовыwindow.RevBack?.…ничего не делают, а клик поdata-revback-openтеряется. После загрузки, но до появления виджета на странице,open()и такой клик запоминаются (шаг 3). - Инлайн-копия скрипта и
type="module"не работают: виджет находит свой тег черезdocument.currentScript, а из егоsrcберёт адрес API — у инлайн-скриптаsrcнет, у модуляcurrentScriptпуст. - Обновления виджета приезжают сами: браузер хранит скрипт 5 минут, потом сверяется с сервером. Тег менять не нужно.
<script src="https://api.revback.ai/widget.js" data-widget-key="ваш ключ"
data-locale="en" data-position="left" async></script>// тег виджета в разметке с async или defer
function initRevback() {
// widget.js выполнился — window.RevBack есть
}
// скрипт мог выполниться раньше этого кода (например, из кэша) — тогда load уже не придёт
if (window.RevBack) initRevback()
else document.querySelector('script[data-widget-key]').addEventListener('load', initRevback)// например, в SPA или после согласия посетителя
const s = document.createElement('script')
s.src = 'https://api.revback.ai/widget.js'
s.dataset.widgetKey = 'ваш ключ'
s.addEventListener('load', () => {
// widget.js выполнился — window.RevBack есть: подписки, identify и т. д.
})
document.body.append(s)Шаг 2. Проверить
Проверять — только на домене канала: том, что указан в поле «Адрес сайта». Подходят сам домен, www и
любой поддомен. На localhost, стенде с другим доменом и чужом сайте виджет не заработает: при открытии чата
посетитель увидит «Виджет не активирован для этого домена». Для стенда нужен отдельный канал с доменом стенда.
Что должно получиться. В углу страницы — кнопка чата. Откройте чат и напишите сообщение: беседа появится в кабинете в разделе «Чаты», ответ оператора придёт в виджет.
- Беседу создаёт первое сообщение посетителя: открытие виджета пустых диалогов у оператора не заводит.
- На экране шириной до 480 px чат открывается во весь экран.
Шаг 3. Своя кнопка
Без JavaScript: клик по любому элементу с атрибутом data-revback-open (или внутри него) открывает чат.
Слушатель висит на document, поэтому работают и элементы, добавленные на страницу позже.
<button type="button" data-revback-open>Написать в поддержку</button>Из кода — open(), close(), toggle():
window.RevBack?.open()
window.RevBack?.close()
window.RevBack?.toggle()window.RevBackи слушательdata-revback-openпоявляются, когдаwidget.jsвыполнился. Раньшеwindow.RevBack?.open()ничего не делает, а клик теряется (шаг 1).- После выполнения
widget.js, но до того, как виджет появился на странице, вызовopen(),close(),toggle()и клик поdata-revback-openзапоминаются: открытие применится, когда виджет появится. Если настройки канала не загрузились, виджет не появляется и отложенное открытие теряется. - Штатную кнопку можно скрыть в настройках положения кнопки в кабинете — отдельно для компьютера и телефона.
Чат и
window.RevBackпри этом работают.
Шаг 4. Счётчик и события
Для своей кнопки: число непрочитанных и состояние чата — геттерами и подписками. Код ниже выполняется после
загрузки widget.js (шаг 1).
// счётчик непрочитанных на своей кнопке
const offUnread = RevBack.onUnread((n) => {
badge.textContent = n > 0 ? String(n) : ''
})
// чат открыт / закрыт
const offToggle = RevBack.onToggle((open) => {
menuItem.classList.toggle('active', open)
})
// сессия выдана и первая история на экране
RevBack.onReady(() => { /* … */ })
// отписка
offUnread()
offToggle()getUnread()— число непрочитанных;onUnread(cb)— подписка на него.getOpen()— открыт ли чат;onToggle(cb)— подписка на открытие и закрытие.onUnreadиonToggleзовутcbсразу текущим значением, затем при каждом изменении. Возвращают функцию отписки.onReady(cb)— один раз за загрузку страницы: сессия выдана и первая история на экране. У вернувшегося посетителя с живой сессией (ей осталось больше минуты) — сразу после загрузки, иначе — после первого открытия чата. Если виджет уже готов,cbвызывается сразу. Возвращает функцию отписки.- Исключение в вашем
cbвиджет ловит и пишет вconsole.warn— чат продолжает работать.
Шаг 5. Личность пользователя и выход
RevBack.identify({ token }) связывает посетителя с пользователем вашего сайта: в чате видно, кто
пишет, переписка привязывается к его аккаунту. Код этого шага выполняется после загрузки widget.js (шаг 1).
По умолчанию секрет не выпущен — его нужно выпустить: в настройках канала строка «Секрет для подписи» → кнопка
«Выпустить». Кабинет покажет секрет один раз, и этим же выпуском включится подтверждение личности. Посмотреть секрет
потом нельзя, только «Перевыпустить»; после перевыпуска токены, подписанные старым секретом, получают
401 …/widget-identity-bad-signature. Пока секрет не выпущен, identify отвечает
422 identity/disabled.
Токен — JWT HS256, его выпускает только ваш бэкенд: секрет в браузер не кладут, токен на время жизни равен доступу к переписке пользователя. Ключ подписи — секрет как есть, его байты UTF-8; из base64 его не декодируют.
Имена claims — как в OpenID Connect. Обязательны sub — id пользователя строкой и exp —
срок (unix-секунды; потолка нет, советуем минуты). Один токен — один вход: на каждый вход выпускайте новый токен
со случайным jti — повтор токена из другой сессии получит 410. Остальное — по желанию:
aud— ключ канала: привязывает токен к одному виджету.name,email,phone_number— E.164 (+79001234567; пробелы, дефисы и скобки сервер уберёт сам); отметки подтвержденияemail_verified,phone_number_verified.- Как пользователь вошёл:
amr— массив способов (pwd,otp,sms…,fed), приfed—idp(vk_id,sber_id…);auth_time— время входа, unix-секунды. ids— ваши идентификаторы пользователя,attributes— сведения о нём.
Поле неверного вида вход не роняет: сервер его просто не сохраняет. Отказ бывает только по безопасности токена —
sub, exp, nbf, aud, подпись, структура.
Полная таблица claims, готовые сниппеты для Node, PHP, Python и Go и тест-вектор — в описании операции identify:
https://api.revback.ai/v1/widget/openapi.json.
// токен выпускает ваш бэкенд для вошедшего пользователя; myBackend.revbackToken() — ваш запрос к нему
async function revbackIdentify() {
try {
return await RevBack.identify({ token: await myBackend.revbackToken() })
} catch (e) {
// 410: токен истёк или уже использован в другой сессии — один повтор новым токеном
if (e.status !== 410) throw e
return RevBack.identify({ token: await myBackend.revbackToken() })
}
}
// пользователь вошёл — и при каждой загрузке страницы, пока он в аккаунте
async function onLogin() {
const { identity_state } = await revbackIdentify()
}
// пользователь вышел
async function onLogout() {
try {
await RevBack.reset()
} catch (e) {
// локально гость уже сброшен, новая сессия не выдана (чужой домен, нет сети)
}
}
// смена пользователя: сначала reset, затем identify токеном нового
async function onSwitchUser() {
await onLogout()
await revbackIdentify()
}identify возвращает { identity_state }:
reaffirmed— тот же пользователь, ничего не менялось;promoted— гость этой сессии стал этим пользователем, беседа та же;switched— анонимная переписка слилась в беседу пользователя, лента перезагружена;relogged_in— вошёл другой пользователь: новый посетитель, лента перезагружена.
- Подтверждённая сессия хранится в браузере и переживает перезагрузку. Звать
identifyпри каждой загрузке страницы, где пользователь вошёл, безопасно: повтор даётreaffirmedи ленту не трогает. - Одновременные вызовы
identifyвыполняются по очереди. - Отказ сервера —
Errorс полямиstatus,type(адрес видаhttps://revback.ai/errors/…— сверяйте окончание),invalidParams([{ field, code }], у 422) иretryAfter(секунды изRetry-Afterилиnull). Сбой сети —TypeErrorбез поляtype. Автоповтора нет. reset()— выход: сессия отзывается на сервере (без сети — только локально), заводится новый гость, лента перезагружается; прежняя переписка в этом браузере недоступна. Безreset()виджет в этом браузере остаётся под прежним пользователем, пока жива подтверждённая сессия (до 30 дней); после её истечения виджет сам заводит нового гостя.reset()может отклонить промис: после отзыва он ждёт выдачи новой сессии, а на чужом домене или без сети её не будет. Оборачивайте вtry/catch: при отказе гость локально уже сброшен, новая сессия не выдана.
RevBack.setAttributes(obj) — сведения о посетителе (атрибуты контакта). Работает у любой сессии,
подтверждения личности не требует. Значения — строки, null удаляет ключ; всего не больше 32 ключей.
Токен identify с claim attributes заменяет сохранённый набор целиком ({} —
очистить), токен без этого claim набор не трогает. Если кладёте attributes в токен, записанное через
setAttributes перепишется при каждом identify.
// значения — строки; null удаляет ключ
RevBack.setAttributes({ plan: 'pro', city: 'Казань', promo: null })
.catch((e) => console.warn('setAttributes', e.status, e.type))Ошибки identify
Что пришло в отказе identify и что с этим делать. Для 422 во второй строке — field / code
из invalidParams.
| Результат | Что случилось | Что делать |
|---|---|---|
| TypeErrorбез status и type | запрос не дошёл до сервера — сбой сети | повторить позже: автоповтора нет, его планирует ваш код |
| TypeErrorexpected { token: string } | аргумент — не { token: строка }; запрос не отправлялся |
исправить вызов |
| 401…/widget- |
подпись токена не совпала: не тот секрет, секрет перевыпущен или декодирован из base64. Сессия посетителя жива | подписывать строкой «Секрет для подписи» как есть; после перевыпуска — новым секретом |
| 401другой type | сессия посетителя истекла — виджет пересоберёт её сам | повторить identify |
| 410…/widget- |
exp в прошлом (допуск часов 60 с) или этим токеном уже вошли из другой сессии |
выпустить новый токен и повторить |
| 422identity / disabled | секрет для подписи на канале не выпущен — подтверждение личности не включено | выпустить секрет в настройках канала: строка «Секрет для подписи» → «Выпустить» |
| 422token / unsupported_alg | алгоритм подписи не HS256 (включая none) |
подписывать HS256 |
| 422nbf / not_yet_valid | nbf позже, чем «сейчас + 60 с» |
проверить nbf и часы сервера |
| 422aud / mismatch | aud не содержит ключ канала |
положить в aud ключ канала или убрать claim |
| 422другой field / code | токен собран неправильно — invalidParams называет поле |
исправить названное поле |
| 409…/widget- |
анонимная история слишком велика, чтобы слить её одной операцией | такие случаи разбирает служба поддержки |
| 503…/widget- |
слияние истории занято параллельной операцией | повторить через retryAfter секунд |
| 429 | превышен лимит запросов с адреса или на ключ канала | повторить через retryAfter секунд |
| 404 | канал по этому ключу не найден — удалён или ключ не тот | сверить ключ с кабинетом |
| 5xx | временный сбой на стороне сервера | повторить позже |
Шаг 6. Хранение и CSP
Переписка хранится на сервере и по сроку не удаляется. В браузере виджет держит только своё состояние —
в localStorage, с ключом канала в имени, чтобы два виджета на одном домене не путали сессии:
ccw:<ключ>:visitor— id гостя;ccw:<ключ>:session— сессия и её срок;ccw:<ключ>:prechat_email— посетитель уже оставил email или скрыл вопрос о нём;ccw:<ключ>:read_wm— до какого места посетитель прочитал;ccw:<ключ>:changes_cursor— отметка догона ленты.
Cookie ccw_<ключ>_visitor — копия id гостя: срок 1 год, Path=/,
SameSite=Lax, Secure на https. Очистка одного из хранилищ гостя не меняет — id
восстанавливается из другого. Safari (ITP) может сократить срок cookie. Если недоступны оба хранилища (бывает в
приватном режиме), состояние живёт в памяти вкладки, а после перезагрузки это новый гость.
Content-Security-Policy
Если на сайте включена CSP, добавьте к своим директивам. Если у вас задан только default-src, а
connect-src, img-src, media-src или style-src нет, браузер
применяет к ним default-src — добавьте эти директивы явно, сохранив в них свои значения вроде
'self'.
script-src https://api.revback.ai
connect-src https://api.revback.ai wss://api.revback.ai
style-src 'unsafe-inline'
img-src blob:
media-src blob:script-src— самwidget.js.connect-src— запросы к API и WebSocketwss://api.revback.ai/v1/widget/ws, по которому приходят новые сообщения. Если сокет недоступен, виджет опрашивает историю.style-src 'unsafe-inline'— стили виджет вставляет элементами<style>внутрь Shadow DOM, без nonce.img-src— картинки вложений и их превью показываются черезblob:-адреса; плюс хост вашего логотипа, если он задан в настройках канала: логотип в шапке чата грузится по этой ссылке.media-src blob:— обложку видео, которое прикладывает посетитель, виджет снимает в браузере изblob:-адреса.
Что видит посетитель, если что-то не так
| Что видно | Что случилось | Что делать |
|---|---|---|
| «Виджет не активирован для этого домена» | страница открыта не на домене канала (localhost, стенд, другой сайт) или домен в канале не указан; видно при открытии чата | открыть сайт на домене из поля «Адрес сайта»; для стенда — отдельный канал с доменом стенда |
| «Чат временно недоступен» | сервер отказал в сессии по другой причине, не из-за домена | посмотреть ответ на POST /v1/widget/…/session во вкладке сети браузера |
| Кнопки чата нетскрыта в кабинете | кнопка скрыта в настройках положения для этого экрана — компьютера или телефона | открывать чат своим элементом (шаг 3) или вернуть кнопку в настройках |
| Кнопки чата нетв консоли: не задан data-widget-key | у тега нет атрибута data-widget-key |
добавить ключ канала |
| Кнопки чата нетв консоли: конфиг недоступен | настройки канала не загрузились: неизвестный ключ, выключенный канал или сеть | сверить ключ с кабинетом |
| Кнопки чата нетв консоли пусто | скрипт вставлен инлайн-копией или с type="module": у инлайн-скрипта нет src, а из src виджет берёт адрес API; модуль не видит свой тег — виджет молча выходит |
подключать тегом с src, как в шаге 1 |
| Кнопки чата нетв консоли: Refused to load the script … Content Security Policy | CSP сайта не пускает widget.js |
дополнить CSP — шаг 6 |
Дополнительно
www и поддомены входят в один канал; второй домен — второй канал.