К содержанию
sapportlydocs
Панель

Живой канал (WebSocket)

Интеграторский сокет: одноразовый ticket, wire v2, ping, коды закрытия. Не Phoenix. Не NATS.

Зачем ticket

API-ключ нельзя класть в query string (логи прокси, история браузера). POST /v1/ws/ticket (право ws:connect) выдаёт одноразовый билет ~ 60 секунд (expires_in_secs). Обёртка SDK запрашивает новый на каждый reconnect.

ЧтоЗначение
КтоИнтегратор (API-ключ) или виджет (visitor JWT через bootstrap / embed-session)
TTL билетаИнтегратор ~60 с, виджет ~120 с. Один раз.
URLwss://ws.sapportly.pro/ws?ticket=…
Сессия после redeemИнтеграторский JWT ~3600 с, затем close 4401
НаправлениеТолько сервер → клиент. Текстовые кадры от вас игнорируются
рекомендуемый путь — входящиеTypeScript
import { SapportlyClient } from "@sapportly/sdk";
import { SapportlyInbox } from "@sapportly/sdk/realtime";

const client = new SapportlyClient({
  apiKey: process.env.SAPPORTLY_API_KEY ?? process.env.SUPPORTLY_API_KEY!,
});
const inbox = new SapportlyInbox(client, { channels: ["custom:shop"] });

inbox.onVisitor((m) => console.log("in", m.body));
inbox.onAgent((m) => {
  if (m.echo) return;
  console.log("out", m.body);
});
inbox.onAi((d) => {
  if (d.partial) return;
  console.log("ai", d.draft_body);
});

await inbox.connect();

Кадр wire v2

Это то, что приходит в браузер / Node. Внутренний NATS-конверт с metadata / kind / delivery на этот сокет не кладётся.

JSON
{
  "v": 2,
  "type": "message.delivered",
  "event_id": "uuid",
  "payload": {
    "type": "message.delivered",
    "message_id": "uuid",
    "role": "visitor",
    "channel": "custom:shop",
    "body": "текст",
    "content_encoding": "plain"
  }
}
typeСмысл
message.deliveredСообщение в ленте. role: visitor — входящее; agent или assistant — исходящее; system — не посетитель и не агент
ai.draftЧерновик ИИ. Стрим: partial: true, затем финал. Не строка истории. message_id = id входящего, на которое отвечает модель

Если у канала включено шифрование тенанта (enc_v1_standard), в кадре payload.body будет ciphertext, не открытый текст. Публичный SDK ключ тенанта не отдаёт — для ботов обычно оставляют канал в plaintext.

Keepalive и коды закрытия

КодКогдаЧто делать
4401Истёк JWT сессииНовый ticket, reconnect. SDK делает сам
4403Revoke или переполнен исходящий буферНовый ticket, reconnect; не долбить без backoff
1008Слишком много клиентских ping (~60/мин)Не слать прикладной ping. Сервер пингует сам каждые 25 с

Стабильное подключение

Подписи «Онлайн», «Восстановление подключения» и «Переподключение» описывают состояние транспорта, а не состояние диалога. Один клиент должен держать один сокет, переподключаться с экспоненциальной задержкой и получать новый ticket на каждую попытку. Не создавайте новый сокет при каждом рендере интерфейса и не запускайте несколько циклов reconnect параллельно.

После успешного reconnect считайте REST источником истины: дочитайте сообщения с последнего подтверждённого message_id, объедините их по id и только затем продолжайте realtime-поток. Это защищает от дублей, пропусков и гонок между вебхуком, сокетом и обновлением страницы. Если сеть недоступна долго, показывайте оператору понятный статус, но не теряйте введённый текст и не отправляйте его повторно без idempotency key.

Пропущенные события

Интеграторский сокет не реплеит историю. Очередь pending_ws — для офлайн посетителей виджета. После reconnect: client.conversations.iterateMessages(channel). Один message_id может прийти и по вебхуку — SapportlyInbox дедупит по умолчанию, но не трогает ai.draft.

Низкоуровневый сокет

TypeScript
import { SapportlyRealtime } from "@sapportly/sdk/realtime";

const stream = new SapportlyRealtime({
  tickets: client.realtime,
  onEvent: (event) => console.log(event.type, event.payload),
});
await stream.connect();

Node 18/20: нет глобального WebSocket. Node 22+, Bun, браузер — есть. Иначе npm i ws и WebSocket: (url) => new WebSocket(url).

URL

Канон: Caddy wss://ws.sapportly.pro/ws?ticket=… → realtime-плоскость. Health: https://ws.sapportly.pro/health.

Дальше

Страница полезна?

/docs/api/realtime
Живой канал WebSocket — ticket, wire v2, не Phoenix · Sapportly Docs