Документация для интеграторов
Здесь всё, что нужно для подключения: адреса, контракт API, сущности, порядок интеграции, серверная и клиентская части. Каждый пример ниже проверен на живом стенде, а не составлен по исходникам.
Адреса
| Назначение | Адрес |
|---|---|
| API | https://api.starthey.com |
| Виджет | https://widget.starthey.com/v1/widget.js |
| Справочник API | api.starthey.com/api/sdk/docs |
| Спецификация OpenAPI | api.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. Поддерживает правку, удаление, реакции, треды, закрепление и вложения. |
Порядок интеграции
- Получаете
app_id,key_idи секрет ключа. Секрет показывается один раз и не восстанавливается — храните его в секрет-хранилище или переменной окружения. - Сообщаете домены, на которых будет стоять виджет, и мы регистрируем их для вашего приложения. Это делается до выпуска первых токенов — см. раздел ниже, порядок здесь важен.
- Ваш бэкенд создаёт комнату под товар или заказ.
- Ваш бэкенд добавляет участников в комнату.
- Ваш бэкенд выпускает пользовательский токен под конкретную комнату.
- Фронтенд получает токен и подключает виджет или 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 — менять нужно только для своей инсталляции. |
mode | inline (по умолчанию) или iframe — изоляция стилей и скриптов страницы. |
theme | light, dark или auto. |
lang | Язык интерфейса, по умолчанию ru. |
self-uid | Идентификатор текущего пользователя — определяет, какие сообщения «свои». |
allow-write | Разрешить отправку. Признак наличия — см. allow-anon-read. |
write-mint-endpoint | URL вашего бэкенда для выпуска токена на запись из анонимного режима. |
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. Элемент в этом случае сознательно не рендерит ничего. |
Поддержка
Вопросы по интеграции и доступам — через форму связи.