Виджет на сайте
Чат на сайте организации подключается одним скриптом. Скрипт, окно чата и Web SDK раздаёт сама установка, внешний CDN не нужен. В примерах адрес установки для клиентов — https://support.example.com, ключ размещения — pk_… (его показывает Интеграции → Виджет на сайт организации).
Подключение
<script src="https://support.example.com/widget/loader.js" data-placement="pk_…" async></script>
Скрипт (около 3 КБ) ставит кнопку чата и открывает окно в изолированном iframe. С сайтом окно общается только сообщениями с проверкой origin и источника. CSS и скрипты сайта в окно не проникают.
Тот же виджет программно:
const chat = window.ShtabHelpdesk.init({
placement: 'pk_…',
widgetOrigin: 'https://support.example.com',
})
chat.open()
init возвращает { open, close, identify, logout, setTheme, navigated, destroy }.
Домен сайта
Размещение работает только на подтверждённых доменах: origin сайта доказывается TXT-записью DNS, запись и её значение показывает настройка размещения. Запросы с неподтверждённого origin API отклоняет.
Вошедший клиент
Гость пишет без входа: сессия держится на cookie браузера. Клиент, вошедший в личный кабинет сайта, получает свою историю обращений по chat token: его подписывает бэкенд сайта, а виджет передаёт identify(token).
| Поле | Значение |
|---|---|
iss | издатель chat token, которого заводит администратор организации: адрес, вид подписи, секрет или ключи |
aud | ключ размещения |
sub | постоянный id клиента в системе сайта: по нему находятся его обращения |
name | имя, которое увидит оператор (необязательно) |
iat, exp | время выпуска и окончания, exp − iat не больше 600 секунд |
jti | уникальная строка: токен действует один раз |
Подпись — HS256 общим секретом или RS256/ES256 ключом издателя. Токен выпускает только бэкенд: секрет никогда не попадает в браузер. Выход из кабинета — logout().
Тот же sub с пространством token:<издатель> — клиент интеграционного API: обращение, начатое системой через API, клиент увидит в виджете.
Точка входа
Скрипт сообщает окну страницу сайта (origin и путь, без query и fragment) при подключении, открытии окна и повторном init. Новое обращение берёт её в «Точку входа» для оператора. Одностраничное приложение, которое не вызывает init при переходах, вызывает chat.navigated() само.
Свои стили
Вид окна меняют два слоя:
- CSS размещения (Интеграции → Виджет на сайт → Свой CSS) применяется внутри окна поверх его стилей. Разметки и
@importнет, загрузить что-то извне не даст политика безопасности окна. - Тема сайта —
init({ theme })иsetTheme(theme): значения переменных--shtab-*иscheme: 'light' | 'dark'поверх значений CSS размещения.
| Переменная | Что красит |
|---|---|
--shtab-accent | основной цвет: кнопки, ссылки, кнопка чата на сайте |
--shtab-background, --shtab-text | фон и текст окна |
--shtab-font-family | шрифт окна |
--shtab-radius | скругление окна |
--shtab-header-background, --shtab-header-text | шапка |
--shtab-bubble-customer, --shtab-bubble-customer-text | сообщения клиента |
--shtab-bubble-operator, --shtab-bubble-operator-text | сообщения сотрудников и бота |
Классы для CSS размещения: .shtab-widget (окно), .shtab-widget__header, __title, __subtitle, __bubble (с --customer и --operator), __composer. Переменные и классы — контракт: они меняются с предупреждением за 30 дней. Тему сайта проверяет мост: только эти переменные, без url(), правил и разметки.
ShtabHelpdesk.init({
placement: 'pk_…',
widgetOrigin: 'https://support.example.com',
theme: { vars: { '--shtab-accent': '#7c3aed', '--shtab-radius': '24px' }, scheme: 'dark' },
})
Свой интерфейс: headless Web SDK
/widget/sdk/helpdesk-sdk.js (ESM) — тот же клиент, на котором написан виджет. Сайт рисует чат сам:
import { HelpdeskClient } from 'https://support.example.com/widget/sdk/helpdesk-sdk.js'
const client = new HelpdeskClient({
apiBaseUrl: 'https://support.example.com',
realtimeUrl: 'wss://support.example.com/connection/websocket',
placementKey: 'pk_…',
})
await client.config() // окно размещения: язык, тексты, согласие
await client.refresh().catch(() => client.startGuest()) // гость: сессия по cookie или новая
// await client.identify(chatTokenFromYourBackend) // вошедший клиент
client.connect() // realtime
const { conversation } = await client.startConversation('Тема', { text: 'Здравствуйте!' })
let older = null
const stream = client.openConversation(conversation.id, {
onMessages: (messages) => render(messages), // история и realtime без потерь и дублей
onSnapshot: ({ nextCursor }) => {
older = nextCursor // курсор страницы перед загруженной
},
onTyping: ({ authorName }) => showTyping(authorName), // «печатает…» сотрудника
})
// Прокрутили к началу: страница по 50 перед загруженными, null — начало истории.
if (older) {
;({ nextCursor: older } = await client.olderHistory(conversation.id, older))
}
composer.addEventListener('input', () => stream.typing()) // «печатает…» клиента, не чаще раза в 3 с
await client.sendMessage(conversation.id, { text: 'Ещё вопрос' }) // офлайн дождётся сети
stream.close()
await client.logout()
Требования те же, что у виджета: подтверждённый домен сайта и chat token от бэкенда для вошедшего клиента.
Мобильные приложения
В приложениях iOS и Android тот же чат открывают мобильные SDK.