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

#JS API виджета

Статус: Планируется — объект Sapport и его методы не существуют в текущей версии widget.js. Доступно — только два сообщения postMessage из кадра чата и ключ window.sapport.consent загрузчика (раздел «Что есть сегодня»). Не используйте планируемые методы в боевой разметке: вызов Sapport.open() сегодня завершится ошибкой Sapport is not defined.

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

#Содержание

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

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

ЗадачаМетод
Открыть чат по своей кнопке «Задать вопрос»Sapport.open() вместе с data-launcher="none"
Сообщить, что посетитель перешёл на другую «страницу» без перезагрузкиSapport.setPage()
Связать посетителя с пользователем вашего сайтаSapport.identify() с подписью
Реагировать на сообщение ИИ, заявку или передачу операторуSapport.on()
Отправить заранее заготовленный вопрос от имени посетителяSapport.send()

Доступ: аутентификация не нужна для вызовов в браузере. Подпись для identify создаёт ваш сервер секретом интеграции; секрет в браузер не попадает.

#Что есть сегодня (Доступно)

Статус: Доступно.

#Сообщения postMessage из кадра чата

Окно чата — это iframe на домене ячейки. Он сообщает странице-хозяину о двух событиях сообщениями window.postMessage. Их принимает сам widget.js; ваша страница тоже может слушать их.

Сообщение (строка)КогдаЧто делает widget.js
sapport:new-messageПришло новое сообщение ИИ или оператораЕсли окно закрыто, увеличивает счётчик на кнопке
sapport:closeПосетитель нажал ✕ в шапке окнаЗакрывает окно

Свойства:

Слушать на своей странице:

<script>
  window.addEventListener('message', function (e) {
    // origin ячейки, откуда загружен widget.js
    if (e.origin !== 'https://formula-cream.pro') return;
    if (typeof e.data !== 'string') return;

    if (e.data === 'sapport:new-message') {
      console.log('В чате новое сообщение');
    }
    if (e.data === 'sapport:close') {
      console.log('Посетитель закрыл окно чата');
    }
  });
</script>

Эти сообщения — внутренний протокол между кадром и widget.js, а не обещанный контракт: состав сообщений может измениться. Для устойчивой интеграции дождитесь Sapport.on().

#window.sapport.consent (загрузчик)

Если виджет подключён через загрузчик, состояние согласия посетителя передаётся функцией:

window.sapport.consent(true);   // согласие дано
window.sapport.consent(false);  // согласие не дано / отозвано

или событием:

window.dispatchEvent(new CustomEvent('sapport:consent', { detail: true }));
// допустима и форма { granted: true }

Подробности и готовая разметка баннера — Встраивание виджета, блок 5.

#Что НЕ существует сегодня

#Подключение (Планируется)

Статус: Планируется. Целевой вид; имена и сигнатуры могут уточниться при публикации.

Объект window.Sapport создаётся скриптом widget.js. Вызывайте методы после его загрузки — например, из обработчика onload тега:

<script
  src="https://formula-cream.pro/widget.js"
  data-channel="00000000-0000-4000-8000-000000000000"
  data-launcher="none"
  async
  onload="Sapport.on('ready', function () { Sapport.open(); })"></script>

Очередь вызовов до загрузки скрипта проект не описывает; не рассчитывайте на неё. Гарантия порядка — событие ready.

#Методы (Планируется)

Статус: Планируется. Все методы ниже — проект. Возвращаемые значения, кроме указанных, не фиксируются.

МетодПараметрыОписание
Sapport.open()—Открыть окно. Создаёт кадр при первом вызове
Sapport.close()—Закрыть окно
Sapport.toggle()—Переключить
Sapport.send(text)text — строка, не более 4000 знаковОтправить сообщение от имени посетителя, как будто он набрал его сам. Требует согласия, если арендатор его включил
Sapport.setPage(page){type, id, title}Обновить контекст страницы: тип и идентификатор сущности и заголовок
Sapport.identify(user){external_id, name, email, phone, signature}Связать посетителя с пользователем вашего сайта (ниже)
Sapport.on(event, fn)event — имя события, fn — обработчикПодписаться на событие

#Sapport.setPage

ПолеТипОбязательныйОграниченияПример
typeстроканеткак у window.sapportPage.type: латинские буквы, _, -, до 20 знаков, без цифр (см. ограничение 6)course
idстроканетбуквы, цифры, _, -, до 40 знаковhydrolat-basics
titleстроканетдо 150 знаков; в промпт попадает очищеннымОсновы гидролатов
Sapport.setPage({ type: 'course', id: 'hydrolat-basics', title: 'Основы гидролатов' });

Адрес страницы берётся из location; без согласия (см. Страница, источник и UTM) в промпт попадает только путь.

#Sapport.send

Sapport.send('Сколько стоит курс?');

Сообщение уходит тем же маршрутом, что и ввод посетителя, — через клиентский Chat API. Ответ ИИ появится в окне; для получения ответа в коде подпишитесь на событие message.

#События (Планируется)

Статус: Планируется. Содержимое detail (поля событий) проект не фиксирует; состав ниже — предложение документа и подлежит проверке при реализации.

СобытиеКогдаПредполагаемые данные
readyВиджет загружен, настройки канала получены—
openОкно открылось—
closeОкно закрылось—
messageВ окне появилось сообщение: посетителя, ИИ или оператораsender (user, ai, operator), content
leadПо диалогу создан лидидентификатор лида не передаётся посетителю — вероятно, только факт
handoffДиалог передан оператору (оператор подключился или диалог встал в очередь)—
Sapport.on('message', function (m) {
  if (m.sender === 'ai') console.log('Ответил ИИ:', m.content);
});

Sapport.on('lead', function () {
  // Например, отправить цель в вашу аналитику
  if (window.ym) window.ym(12345678, 'reachGoal', 'chat_lead');
});

Sapport.on('handoff', function () {
  console.log('Подключается оператор');
});

Событие lead показывает, что заявка создана на платформе. Для передачи заявки на ваш сервер используйте вебхуки (lead.created, а для передачи оператору — conversation.handoff_requested), а не обработчик в браузере: браузер можно закрыть или подделать.

#identify и подпись личности (Планируется)

Статус: Планируется.

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>

Значение подписи в примере — заглушка, а не рабочая подпись.

Алгоритм хеша (SHA-256) и кодировка (hex) выбраны документом по аналогии с подписью вебхуков; проект фиксирует лишь «HMAC от external_id секретом интеграции». Уточняется при реализации.

#Одностраничные приложения (Планируется)

Виджет читает адрес и заголовок страницы при первом открытии. В SPA адрес меняется без перезагрузки, и ИИ получил бы устаревший контекст. Вызывайте Sapport.setPage() при смене маршрута.

#React (React Router)

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export function SapportPageSync({ 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;
}
// Страница курса
<SapportPageSync 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 SapportRouteSync() {
  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) обходной путь для SPA: не полагаться на автоматический контекст и задавать window.sapportPage до первого открытия; после открытия окна контекст обновить нельзя (блок 4).

#Ошибки

JS API работает в браузере и не возвращает HTTP-ошибок. Условия, при которых вызов не имеет эффекта, проект не описывает. Предполагается:

СитуацияПредполагаемое поведение
Вызов до загрузки скриптаSapport is not defined: очередь проектом не описана
send без согласия, когда арендатор его требуетСообщение не уходит; окно показывает плашку согласия
identify с неверной подписьюЛичность не принимается; посетитель остаётся анонимным

Это не контракт; точное поведение будет описано при выпуске.

#Заметки

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