#JS API виджета
Статус: Доступно — объект
window.Sapport(методыopen,close,toggle,send,setPage,on,off), событияready,open,close,message,handoff,state,errorи очередь вызовов до загрузки скрипта. Недоступен — методidentify: вызов возвращает ошибкуidentify_unavailable. Планируется — подпись личности посетителя (раздел «identify»).
Назначение: управлять виджетом из кода вашей страницы — открыть и закрыть окно, отправить сообщение от имени посетителя, сообщить виджету о смене страницы, подписаться на события.
#Содержание
- Когда использовать
- Подключение и очередь вызовов
- Методы
- События
- Сообщения postMessage и согласие загрузчика
- identify и подпись личности (Планируется)
- Одностраничные приложения
- Ошибки
- Заметки
- Открытые вопросы
#Когда использовать
| Задача | Метод |
|---|---|
| Открыть чат по своей кнопке «Задать вопрос» | Sapport.open() вместе с data-launcher="none" (Встраивание, блок 7) |
| Сообщить, что посетитель перешёл на другую «страницу» без перезагрузки | Sapport.setPage() |
| Реагировать на сообщение ИИ или оператора, передачу оператору, смену состояния диалога | Sapport.on() |
| Отправить заранее заготовленный вопрос от имени посетителя | Sapport.send() |
| Связать посетителя с пользователем вашего сайта | Sapport.identify() (недоступен) |
Доступ: аутентификация не нужна для вызовов в браузере.
#Подключение и очередь вызовов
Объект window.Sapport создаётся скриптом widget.js в момент его выполнения. Повторная вставка скрипта второй виджет не рисует.
Чтобы вызывать методы до загрузки скрипта, поставьте перед тегом widget.js заглушку-очередь. Команды из очереди выполняются сразу после загрузки скрипта, по порядку:
<script>
// Заглушка: вызовы копятся в массиве q и выполняются после загрузки widget.js
window.Sapport = window.Sapport || { q: [] };
window.Sapport.q.push(['setPage', { type: 'course', id: 'hydrolat-basics' }]);
window.Sapport.q.push(['open']);
</script>
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
data-launcher="none"
async></script>
Запись очереди: массив, первый элемент — имя метода, остальные — аргументы. Допустимы только имена публичных методов; остальное игнорируется. После загрузки Sapport.q.push([...]) исполняется сразу.
Подписки Sapport.on(...) в очередь ставить не нужно: подписаться на событие можно в любой момент после загрузки скрипта, а подписавшийся на ready после готовности виджета всё равно получит вызов.
Если скрипт уже загружен, Sapport.open() и остальные методы можно вызывать напрямую. Вызовы open, close, toggle и send до создания окна запоминаются и исполняются, когда виджет готов.
#Методы
Статус: Доступно, кроме
identify.
| Метод | Параметры | Описание |
|---|---|---|
Sapport.open() | — | Открыть окно. Кадр создаётся при первом открытии |
Sapport.close() | — | Закрыть окно |
Sapport.toggle() | — | Переключить |
Sapport.send(text) | text — непустая строка, не более 4000 знаков | Отправить сообщение от имени посетителя и открыть окно. Требует согласия посетителя на сбор данных |
Sapport.setPage(page) | {url, title, type, id, utm} | Обновить контекст страницы |
Sapport.identify(user) | — | Недоступен: возвращает {ok: false, error: 'identify_unavailable'} и подаёт событие error |
Sapport.on(event, fn) | имя события, обработчик | Подписаться. Возвращает функцию отписки |
Sapport.off(event, fn) | имя события, обработчик (необязателен) | Отписаться. Без обработчика снимает все подписки на событие |
open, close, toggle, setPage ничего не возвращают. send возвращает {ok: true} либо {ok: false, error, message}; одновременно подаётся событие error.
#Sapport.setPage
| Поле | Тип | Обязательный | Ограничения | Пример |
|---|---|---|---|---|
url | строка | нет | http(s); относительный адрес разрешается от текущей страницы; до 300 знаков. По умолчанию — адрес страницы | https://example.com/course/hydrolat-basics |
title | строка | нет | до 200 знаков. По умолчанию — document.title | Основы гидролатов |
type | строка | нет | как у window.sapportPage.type: латинские буквы, _, -, до 20 знаков, без цифр (см. ограничение 6) | course |
id | строка | нет | буквы, цифры, _, -, до 40 знаков | hydrolat-basics |
utm | объект | нет | только известные метки: source, medium, campaign, content, term, yclid, ysclid, fbclid, gclid (допустимы и с префиксом utm_); до 200 знаков | {source: 'newsletter'} |
Sapport.setPage({ type: 'course', id: 'hydrolat-basics', title: 'Основы гидролатов' });
Без согласия посетителя (см. Страница, источник и UTM) на платформу уходит только путь страницы, без параметров адреса, заголовка и меток. Согласие читается в момент отправки данных, а не в момент вызова.
#Sapport.send
var r = Sapport.send('Сколько стоит курс?');
if (!r.ok) console.warn(r.error);
Сообщение уходит тем же маршрутом, что и ввод посетителя, — через клиентский Chat API. Если сообщение ушло до готовности кадра, оно ждёт и отправляется сразу после.
#События
Статус: Доступно. Обработчик получает один объект. Ошибка внутри вашего обработчика не прерывает остальные и пишется в консоль.
| Событие | Когда | Данные |
|---|---|---|
ready | Виджет создан (кнопка нарисована или готов к вызову из кода) | {} |
open | Окно открылось | {} |
close | Окно закрылось | {} |
message | В окне появилось сообщение посетителя, ИИ или оператора | {id, sender, content, at, attachments?, cards?} |
state | Сменилось состояние диалога | {state, operator} |
handoff | Диалог ушёл от ИИ: встал в очередь к оператору или принят оператором | {state, operator} |
error | Вызов метода не удался либо кадр сообщил об ошибке | {code, method, message} |
Поля:
| Поле | Значения |
|---|---|
message.sender | user, ai, operator (сообщения system в событие не попадают) |
message.content | текст, до 8000 знаков |
message.attachments | до 5 файлов: {name, type, size, url}; url — только https, ссылка подписана и живёт около часа |
message.cards | до 10 карточек: {code, name, description, image_url, price_text, badge, page_url, buttons}; кнопок до 3, адрес кнопки — https или путь вашего сайта |
state.state | ai (отвечает ИИ), queued (диалог в очереди к оператору), operator (ведёт оператор) |
state.operator | {name, avatar_url} либо null; фото — только https или null |
Sapport.on('message', function (m) {
if (m.sender === 'ai') console.log('Ответил ИИ:', m.content);
if (m.cards) console.log('Карточки:', m.cards.map(function (c) { return c.name; }));
});
Sapport.on('state', function (s) {
if (s.state === 'operator') console.log('Диалог ведёт', s.operator && s.operator.name);
});
var off = Sapport.on('handoff', function (h) {
// например, отправить цель в вашу аналитику
if (window.ym) window.ym(12345678, 'reachGoal', 'chat_handoff');
});
// off() снимает подписку
Событие в браузере — удобство для интерфейса вашей страницы, а не надёжный источник данных: браузер можно закрыть или подделать. Для передачи заявки и диалога на ваш сервер используйте вебхуки (
lead.created,conversation.handoff_requested).
События lead в виджете нет.
#Сообщения postMessage и согласие загрузчика
#Сообщения postMessage из кадра чата
Окно чата — это iframe на домене ячейки. Он сообщает widget.js о событиях сообщениями window.postMessage. Для вашего кода есть Sapport.on(); сообщения кадра — внутренний протокол и контрактом не являются. widget.js принимает сообщения только от своего кадра (проверяет source и origin), чужой скрипт страницы не накрутит счётчик и не закроет чат.
Устаревшие строковые сообщения sapport:new-message (увеличить счётчик на кнопке, пока окно закрыто) и sapport:close (посетитель нажал ✕) по-прежнему работают, но слушать их в своём коде не рекомендуется: используйте события message и close.
#window.sapport.consent (загрузчик)
Если виджет подключён через загрузчик, состояние согласия посетителя передаётся функцией:
window.sapport.consent(true); // согласие дано
window.sapport.consent(false); // согласие не дано / отозвано
или событием:
window.dispatchEvent(new CustomEvent('sapport:consent', { detail: true }));
// допустима и форма { granted: true }
Подробности и готовая разметка баннера — Встраивание виджета, блок 5.
#identify и подпись личности (Планируется)
Статус: Недоступно. Метод
Sapport.identify()в виджете есть, но сегодня всегда возвращает{ok: false, error: 'identify_unavailable'}и подаёт событиеerror. Подпись личности появится вместе с секретом интеграции; описание ниже — целевое, имена и алгоритм могут уточниться.
identify сообщает платформе, какой пользователь вашего сайта сейчас в чате. Без подписи любой посетитель мог бы выдать себя за другого, подставив чужой external_id в консоли браузера. Поэтому external_id принимается только с подписью, созданной вашим сервером.
| Поле | Тип | Обязательный | Ограничения | Пример |
|---|---|---|---|---|
external_id | строка | да | Стабильный идентификатор пользователя в вашей системе | user-4815 |
signature | строка (hex) | да | HMAC-SHA256(secret интеграции, external_id), шестнадцатеричная строка | 9f2c… |
name | строка | нет | Анна | |
email | строка | нет | anna@example.com | |
phone | строка | нет | +79990001122 |
Секрет интеграции (в проекте — «secret интеграции») хранится только на вашем сервере. Никогда не помещайте его в код страницы и не отдавайте браузеру.
#Схема
Браузер Ваш сервер Страница
│ GET /page │
│ ─────────────────────────► │ signature = HMAC(secret, external_id)
│ ◄───────────────────────── │ в HTML кладётся external_id + signature
│ │
│ Sapport.identify({ external_id, signature, … }) ─────────► виджет
#Подпись на Node.js
const crypto = require('node:crypto');
// SECRET — секрет интеграции, из переменной окружения вашего сервера
function signExternalId(externalId) {
return crypto
.createHmac('sha256', process.env.SAPPORT_IDENTIFY_SECRET)
.update(externalId)
.digest('hex');
}
// в обработчике страницы:
// const signature = signExternalId(String(user.id));
#Подпись на PHP
<?php
// $secret — секрет интеграции из конфигурации сервера
function sapport_sign_external_id(string $externalId): string {
$secret = getenv('SAPPORT_IDENTIFY_SECRET');
return hash_hmac('sha256', $externalId, $secret);
}
$signature = sapport_sign_external_id((string)$user['id']);
#Подпись на Python
import hashlib
import hmac
import os
def sign_external_id(external_id: str) -> str:
secret = os.environ["SAPPORT_IDENTIFY_SECRET"].encode()
return hmac.new(secret, external_id.encode(), hashlib.sha256).hexdigest()
signature = sign_external_id(str(user.id))
#Вызов в браузере
<script>
// значения подставляет ваш сервер при формировании страницы
Sapport.identify({
external_id: 'user-4815',
signature: '0000000000000000000000000000000000000000000000000000000000000000',
name: 'Анна',
email: 'anna@example.com'
});
</script>
Значение подписи в примере — заглушка, а не рабочая подпись.
Алгоритм подписи
identifyпока не зафиксирован: проект называет лишь «HMAC отexternal_idсекретом интеграции». Хеш и кодировка выше — рабочее предположение, уточняется при реализации. К подписи вебхуков это не относится: она устроена иначе.
#Одностраничные приложения
Виджет читает адрес и заголовок страницы при первом открытии. В SPA адрес меняется без перезагрузки, и ИИ получил бы устаревший контекст. Вызывайте Sapport.setPage() при смене маршрута: контекст обновляется и после открытия окна.
#React (React Router)
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function PageSync({ type, id, title }) {
const location = useLocation();
useEffect(() => {
if (!window.Sapport) return;
window.Sapport.setPage({ type, id, title: title || document.title });
}, [location.pathname, type, id, title]);
return null;
}
// Страница курса
<PageSync type="course" id={course.slug} title={course.title} />
#Vue (Vue Router)
// router.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({ history: createWebHistory(), routes });
router.afterEach((to) => {
if (!window.Sapport) return;
window.Sapport.setPage({
type: to.meta.pageType,
id: to.params.slug,
title: document.title
});
});
export default router;
#Next.js (App Router)
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export function RouteSync() {
const pathname = usePathname();
useEffect(() => {
(window as any).Sapport?.setPage({ title: document.title });
}, [pathname]);
return null;
}
Тег виджета в Next.js подключайте через next/script с strategy="afterInteractive":
import Script from 'next/script';
<Script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
strategy="afterInteractive"
/>
Вызов setPage до создания кадра запоминается и применяется при его открытии, поэтому можно вызывать его и до загрузки скрипта через очередь (Подключение).
#Ошибки
JS API работает в браузере и не возвращает HTTP-ошибок. Неудачный вызов send или identify возвращает {ok: false, error, message} и подаёт событие error.
Код error | Метод | Когда |
|---|---|---|
invalid_text | send | Пустая строка или не строка |
message_too_long | send | Больше 4000 знаков |
consent_required | send | Согласие посетителя на сбор данных не дано: сообщение не уходит |
identify_unavailable | identify | Метод пока недоступен |
| Ситуация | Поведение |
|---|---|
| Вызов до загрузки скрипта | Sapport is not defined, если нет заглушки-очереди (Подключение) |
| Неизвестное имя метода в очереди | Игнорируется |
| Ошибка в вашем обработчике события | Пишется в консоль, остальные обработчики вызываются |
#Заметки
- Повторные
open()не создают второй кадр. - Каждая вкладка браузера начинает отдельный диалог: сессия хранится в
sessionStorageкадра (Клиентский Chat API).identifyпредназначен, в том числе, для связывания таких диалогов с пользователем. - Изменения JS API смотрите в журнале изменений.
#Открытые вопросы
- Срок выхода
identify: он зависит от секрета интеграции. К владельцу. - Алгоритм подписи
identify: проект называет толькоHMAC(secret интеграции, external_id); хеш-функция и кодировка подписи не зафиксированы. Не указано также, где арендатор получает «secret интеграции» и как его ротировать. Техническое. - Должен ли
identifyобъединять диалоги посетителя из разных вкладок и устройств, и что происходит с уже накопленной анонимной историей, — не описано. К владельцу.