К основному содержимому

Документация для интеграторов

Здесь всё, что нужно для подключения: адреса, контракт API, сущности, порядок интеграции, серверная и клиентская части. Каждый пример ниже проверен на живом стенде, а не составлен по исходникам.

Адреса

НазначениеАдрес
APIhttps://api.starthey.com
Виджетhttps://widget.starthey.com/v1/widget.js
Справочник APIapi.starthey.com/api/sdk/docs
Спецификация OpenAPIapi.starthey.com/api/sdk/openapi.json

Инфраструктура и база данных размещены в России. Для тестовой интеграции выдаётся отдельное приложение (app_id) на том же контуре: данные разных приложений изолированы, но отдельной тестовой инсталляции нет — считайте тестовые данные боевыми и не загружайте в них персональные данные реальных клиентов.

Контракт API

Спецификация OpenAPI 3.1 генерируется из обработчиков запросов, поэтому не может разойтись с тем, что сервер действительно принимает. Её можно импортировать в Postman, Insomnia или Bruno и сгенерировать клиент под свой язык.

Сущности

СущностьЧто это
Приложение (app)Ваша площадка, идентификатор вида sapp_…. Комнаты, сообщения и участники принадлежат ровно одному приложению, и граница между приложениями не пересекается.
Ключ (key)Серверные учётные данные: key_id и секрет. Бэкенд использует их для выпуска пользовательских токенов. В браузер не попадают никогда.
Токен пользователяКороткоживущий JWT, который ваш бэкенд выпускает под конкретного пользователя и набор прав. Это единственное, что уходит во фронтенд.
Комната (room)Диалог — обычно вокруг товара или заказа. Режим шифрования фиксируется при создании.
Участник (member)Пользователь в комнате с ролью. Членство и права токена — разные вещи; см. раздел ниже.
Сообщение (message)Принадлежит комнате, имеет msg_id (UUID) и сквозной номер seq. Поддерживает правку, удаление, реакции, треды, закрепление и вложения.

Порядок интеграции

  1. Получаете app_id, key_id и секрет ключа. Секрет показывается один раз и не восстанавливается — храните его в секрет-хранилище или переменной окружения.
  2. Сообщаете домены, на которых будет стоять виджет, и мы регистрируем их для вашего приложения. Это делается до выпуска первых токенов — см. раздел ниже, порядок здесь важен.
  3. Ваш бэкенд создаёт комнату под товар или заказ.
  4. Ваш бэкенд добавляет участников в комнату.
  5. Ваш бэкенд выпускает пользовательский токен под конкретную комнату.
  6. Фронтенд получает токен и подключает виджет или SDK.

Серверная часть: выпуск токена

KEY_ID="sak_…"
KEY_SECRET="sak_secret_…"          # сохранён при выпуске ключа
BASE="https://api.starthey.com"

BODY='{"user_id":"buyer-42","scopes":["chat:read:tovar-12345","chat:write:tovar-12345","chat:subscribe:tovar-12345"],"ttl_secs":3600}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$KEY_SECRET" -hex | awk '{print $2}')

curl -sS -X POST "$BASE/api/sdk/tokens" \
  -H "X-SDK-Key-Id: $KEY_ID" \
  -H "X-SDK-Secret: $KEY_SECRET" \
  -H "X-SDK-Signature: $SIG" \
  -H "Content-Type: application/json" \
  -d "$BODY"
# -> {"token":"eyJ…","expires_at":1786310400}

Подписывайте ровно те байты, которые отправляете. HMAC считается по сырому телу запроса: сериализуйте один раз, подпишите эту строку и отправьте именно её. Подпись по повторной сериализации — другой порядок ключей, другие пробелы, автоформатирование в HTTP-клиенте — даёт 401, неотличимый от неверного секрета. Это самая частая причина потерянного дня на интеграции.

Время жизни токена — ttl_secs, по умолчанию 3600 секунд, максимум 86400. Поле exp проверяется без допуска: обновляйте токен заранее, а не по факту ошибки. Для iat допуск есть — токен отклоняется, только если выпущен более чем на 30 секунд в будущем, так что небольшое расхождение часов на вашем сервере не мешает.

Во фронтенд уходит только token. Ключ и секрет — никогда.

Права (scopes)

Формат — область:действие:ресурс.

Покомнатные права, их безопасно отдавать в браузер:

chat:read:<room>
chat:write:<room>
chat:subscribe:<room>

Права на всё приложение — в браузер отдавать нельзя:

rooms:read:*
rooms:write:*

Права на приложение не дают права писать: сообщение отправляется только токеном с chat:write:<room>, то есть пользовательским. Бэкенд создаёт комнаты и участников, но не пишет за них.

Звёздочка здесь буквальная, и подстановка несимметрична: право на одну комнату её не удовлетворяет. Токен, которым можно создать одну комнату, может изменить любую комнату приложения. Поэтому жизненный цикл комнат и участников живёт на вашем бэкенде.

Комнаты и участники

# Комнату создаёт ваш бэкенд — токеном со scope rooms:write:*
curl -sS -X POST "$BASE/api/sdk/rooms" \
  -H "Authorization: Bearer $BACKEND_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"room_id":"tovar-12345","title":"Товар 12345"}'
# -> 201

# Участника тоже добавляет бэкенд. Скоуп в токене — это НЕ членство.
curl -sS -X POST "$BASE/api/sdk/rooms/tovar-12345/members" \
  -H "Authorization: Bearer $BACKEND_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"buyer-42"}'
# -> 201

Право в токене — это не членство. Пользователь с chat:read:<room>, не добавленный в комнату, получит 403 на чтение. Это самое неочевидное место в API: ошибка выглядит как проблема с правами, а на деле не хватает участника.

Домен и заголовок Origin

Самое частое место, где интеграция встаёт, и единственное, где порядок действий имеет значение.

Домен регистрируется до выпуска токенов. Список разрешённых источников попадает внутрь JWT в момент выпуска. Если зарегистрировать домен позже, уже выданные токены его не подхватят — виджет откажется работать и напишет, что источник не в списке. Токены придётся выпустить заново.

После регистрации домена заголовок Origin обязателен во всех запросах с токенами этого приложения — включая серверные, которые сами по себе его не отправляют. Браузер подставляет заголовок сам; ваш бэкенд — нет, и именно там появляется неожиданный 403.

# Домен регистрируется ДО выпуска токенов: список разрешённых
# источников вшивается внутрь JWT в момент выпуска, и уже выданный
# токен новый домен не подхватит — его придётся выпустить заново.

# Все серверные вызовы после этого обязаны слать Origin:
curl -sS -X POST "$BASE/api/sdk/rooms" \
  -H "Authorization: Bearer $BACKEND_TOKEN" \
  -H "Origin: https://ваш-домен.ru" \
  -H "Content-Type: application/json" \
  -d '{"room_id":"tovar-12345"}'
# без заголовка Origin -> 403

До регистрации первого домена проверка выключена, поэтому серверные вызовы работают и без заголовка. Она включается вместе с первым доменом — то есть ровно тогда, когда вы подключаете виджет.

Сообщения

# Отправка. msg_id обязан быть UUID — иначе 422.
curl -sS -X POST "$BASE/api/sdk/messages" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"room_id":"tovar-12345","msg_id":"4f84fb52-15cf-4c4e-a841-8c8fda4179e8","sealed_b64":"…"}'
# -> 201 {"seq":2152,"msg_id":"4f84fb52-…"}

# Чтение истории
curl -sS "$BASE/api/sdk/messages?room_id=tovar-12345" \
  -H "Authorization: Bearer $USER_TOKEN"

# Поток событий. EventSource не умеет слать заголовок Authorization,
# поэтому поток открывается по короткоживущему тикету.
curl -sS -X POST "$BASE/api/sdk/messages/subscribe-ticket" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"room_id":"tovar-12345"}'
# -> {"ticket":"…"}  ->  GET /api/sdk/messages/subscribe?room_id=…&ticket=…

Поле seq — сквозной номер в пределах приложения, а не комнаты: первое сообщение в новой комнате получит не единицу, а следующий номер по приложению. Номера монотонно растут, поэтому after_seq корректно работает для докачки, но не считайте его счётчиком сообщений комнаты.

Клиентская часть: виджет

Подключение без сборки — тег скрипта и один элемент:

<script type="module" src="https://widget.starthey.com/v1/widget.js"></script>

<starthey-chat
  app-id="sapp_…"
  room="tovar-12345"
  jwt="eyJ…"
></starthey-chat>

Без jwt и без allow-anon-read элемент сознательно не рендерит ничего и не пишет ошибку — это выбранное поведение «по умолчанию закрыто», а не сбой.

АтрибутНазначение
app-idИдентификатор приложения, выдаётся вместе с ключом. Обязателен.
roomИдентификатор комнаты. Псевдоним апстримного room-id — работают оба.
jwtТокен пользователя от вашего бэкенда.
allow-anon-readЧтение без токена. Признак наличия: значение не читается, allow-anon-read="false" всё равно включает.
base-urlАдрес API. По умолчанию https://api.starthey.com — менять нужно только для своей инсталляции.
modeinline (по умолчанию) или iframe — изоляция стилей и скриптов страницы.
themelight, dark или auto.
langЯзык интерфейса, по умолчанию ru.
self-uidИдентификатор текущего пользователя — определяет, какие сообщения «свои».
allow-writeРазрешить отправку. Признак наличия — см. allow-anon-read.
write-mint-endpointURL вашего бэкенда для выпуска токена на запись из анонимного режима.
reactions-enabledРеакции. Выключаются только строкой "false"; "0" и "no" оставляют включённым.
pinned-messages-enabledЗакреплённые сообщения. Та же логика выключения, что у реакций.
seller-catalogКаталог товаров продавца для карточек.

Клиентская часть: SDK

Виджет закрывает большинство сценариев. Если нужен собственный интерфейс, есть TypeScript-клиент с тем же API под капотом — отправка, история, подписка на события, управление участниками:

const client = new SDKChatClient({
  baseUrl: 'https://api.starthey.com',
  appId: 'sapp_…',
  jwt: tokenFromYourBackend,
});

await client.send({ roomId: 'tovar-12345', text: 'Здравствуйте!' });
const history = await client.list({ roomId: 'tovar-12345' });
client.subscribe({ roomId: 'tovar-12345' }, (msg) => render(msg));

Пакет и инструкция по установке выдаются вместе с ключами: он не опубликован в открытом реестре npm, поэтому имя пакета и способ подключения приходят в комплекте доступов.

Отдельного серверного пакета для Node пока нет. Серверная часть — это четыре HTTP-вызова, описанные выше; их удобно вызывать напрямую или сгенерировать клиент из спецификации OpenAPI.

Где обычно ломается

СимптомПричина
401 на /api/sdk/tokensПодпись посчитана не по тем байтам, которые ушли в теле. Также: отозванный ключ или чужая среда.
401 на вызове с валидным на вид токеномИстёк exp (допуск нулевой) или в скоупе указана другая комната.
403 там, где ожидался 404Право есть, членства нет. Добавьте пользователя в комнату серверным вызовом.
422 при отправке сообщенияmsg_id не UUID. Поле объявлено как format: uuid.
403 на серверном вызове, который вчера работалДля приложения зарегистрирован домен, и теперь заголовок Origin обязателен — в том числе на бэкенде. Добавьте Origin с вашим доменом.
Виджет пишет, что источник не в спискеДомен зарегистрирован после выпуска токена. Список источников вшит в JWT — выпустите токен заново.
403 при отправке сообщения с бэкендаУ токена права rooms:*, а для записи нужен chat:write:<room> — то есть пользовательский токен.
Виджет не показывает ничего, ошибок нетНет ни jwt, ни allow-anon-read. Элемент в этом случае сознательно не рендерит ничего.

Поддержка

Вопросы по интеграции и доступам — через форму связи.