Формула ИИ для разработчиков Справочник API Каталог ошибок OpenAPI
Разделы

#JS API виджета

Статус: Доступно — объект window.Sapport (методы open, close, toggle, send, setPage, on, off), события ready, open, close, message, handoff, state, error и очередь вызовов до загрузки скрипта. Недоступен — метод identify: вызов возвращает ошибку identify_unavailable. Планируется — подпись личности посетителя (раздел «identify»).

Назначение: управлять виджетом из кода вашей страницы — открыть и закрыть окно, отправить сообщение от имени посетителя, сообщить виджету о смене страницы, подписаться на события.

#Содержание

  1. Когда использовать
  2. Подключение и очередь вызовов
  3. Методы
  4. События
  5. Сообщения postMessage и согласие загрузчика
  6. identify и подпись личности (Планируется)
  7. Одностраничные приложения
  8. Ошибки
  9. Заметки
  10. Открытые вопросы

#Когда использовать

ЗадачаМетод
Открыть чат по своей кнопке «Задать вопрос»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.senderuser, 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.stateai (отвечает ИИ), 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_textsendПустая строка или не строка
message_too_longsendБольше 4000 знаков
consent_requiredsendСогласие посетителя на сбор данных не дано: сообщение не уходит
identify_unavailableidentifyМетод пока недоступен
СитуацияПоведение
Вызов до загрузки скриптаSapport is not defined, если нет заглушки-очереди (Подключение)
Неизвестное имя метода в очередиИгнорируется
Ошибка в вашем обработчике событияПишется в консоль, остальные обработчики вызываются

#Заметки

#Открытые вопросы