Diil Docs
  1. Документація
  2. Віджет

Чат підтримки: онлайн-чат на сайті одним тегом

Оновлено:

У відвідувача питання об 11-й вечора, а ваша форма зворотного зв’язку — як записка в пляшці. Чат підтримки це виправляє: кнопка чату на кожній сторінці вашого сайту, а розмова потрапляє в CRM, де ваша команда відповідає в реальному часі. Якщо ви вже додали widget.js для редагування на сайті, до чату вас відділяє рівно нуль рядків коду.

Як працює чат підтримки

  1. Відвідувач натискає кнопку чату в кутку вашого сайту й пише повідомлення (зі скриншотом, якщо слів забракне).
  2. Повідомлення миттєво з’являється в CRM → Підтримка → Чати. Усе працює через WebSocket, тож ніхто не мусить тиснути F5.
  3. Оператор відповідає з CRM, і відвідувач одразу бачить відповідь — разом з індикатором набору тексту й позначками прочитання.

Щоб ніхто не проґавив чат, поки варить каву, оператори отримують у CRM лічильник непрочитаних, сповіщення браузера й, за бажання, Telegram-бота з тихими годинами. Усе це налаштовується в CRM, а не у вашому коді.

Якщо на сайті ввімкнено режим згоди, чат не чекає на «так» у банері: відвідувач може написати вам одразу. Доки згоди немає, віджет не зберігає ідентифікатор відвідувача, а бере для чату одноразовий — він живе лише в пам’яті сторінки. Давнішу розмову це не зачіпає: вона, як і раніше, відновлюється за власним crm_chat_token.

Єдине, чого бракуватиме до згоди, — атрибуції: без ідентифікаторів відвідувача й сесії розмову не прив’язати до каналу, UTM-міток чи рекламного посилання.

Додайте онлайн-чат на свій сайт

Окремого коду для чату немає. Той самий єдиний тег, на якому працює все інше, робить і це:

Перед </body>, на кожній сторінціhtml
<script src="https://widget.sitecog.com/widget.js" defer></script>

Коли чат доступний для вашого сайту, widget.js сам підвантажує модуль чату (widget.support.js), і з’являється плаваюча кнопка чату. Коли ні — відвідувачі не завантажують нічого зайвого. Додати тег двічі не страшно — другу копію буде проігноровано.

  1. Переконайтеся, що ваш тариф включає чат підтримки

    Чат доступний на тарифах, які його включають. Немає чату в тарифі — немає й кнопки, хоч би що казав код.
  2. Перевірте, що ваш домен є сайтом у CRM

    Чат розпізнає ваш сайт за доменом, на якому відкрито сторінку (Origin браузера; www. ігнорується). Домен має бути сайтом, який ви додали в CRM.
  3. Додайте widget.js на сторінку

    Тег вище або варіанти для Next.js / Vite з «Віджет і розмітка» → Крок 1.
  4. Налаштуйте вигляд у CRM

    Значок, кольори, розташування й тексти живуть у CRM → Підтримка → Налаштування — див. нижче.

Вигляд і тексти в CRM

Усе візуальне налаштовується в CRM → Підтримка → Налаштування. Жодних перевизначень CSS, жодних повторних деплоїв: змініть там — і віджет це підхопить.

ПараметрДопустимі значенняПримітки
Значокbubble, headset, question, envelope, spark або ваше власне зображенняВласний значок обирається зі сховища файлів вашого сайту.
Розташуванняbottom-right за замовчуванням, bottom-left, top-right, top-leftОберіть кут, у якому не сидить ваш cookie-банер.
Кольорова схемаindigo за замовчуванням, emerald, midnight, graphiteГотові палітри для кнопки й вікна чату.
Заголовокдо 80 символівШапка вікна чату.
Рядок під заголовкомдо 160 символівРядок під заголовком — гарне місце для «зазвичай відповідаємо протягом 10 хвилин».
Напис у полі введеннядо 80 символівСіра підказка в полі повідомлення.
Привітаннядо 200 символівПоказується в порожньому чаті, поки відвідувач ще нічого не написав.

Тексти для кожної мови й запасний варіант

Кожен текст можна заповнити для кожної мови вашого сайту. Віджет обирає текст для мови відвідувача так:

  1. точний збіг мови;
  2. інакше — збіг за першими двома літерами;
  3. інакше — перший заповнений вами варіант.

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

Автовідповідач

Увімкніть автовідповідач і напишіть текст для кожної мови (до 1000 символів). Він відповідає на перше повідомлення нової розмови, тож відвідувач знає, що його почули, навіть коли вся команда на нараді. Розмова вважається новою після N хвилин тиші — N ви обираєте від 1 до 180, за замовчуванням 5.

Коли нікого немає в мережі

Живий чат, де ніхто не відповідає, — пастка: відвідувач ставить запитання, чекає, закриває вкладку, і ви так і не дізнаєтеся, хто це був. Офлайн-режим у такі хвилини перетворює чат на форму «залиште пошту» — і запитання не зникає разом із відвідувачем.

Вмикається він у CRM → Підтримка → Налаштування → «Коли нікого немає в мережі». Чат переходить в офлайн-режим, коли:

  • у жодного оператора не відкрита CRM цього сайту. Рахується будь-яка сторінка CRM, не лише чати: оператор «у мережі», доки не закрита остання вкладка CRM;
  • зараз неробочий час, якщо ви його задали: робочі дні, час «з» і «до» та часовий пояс. Розклад необов’язковий — без нього діє лише перше правило. Поза робочим часом чат в офлайні, навіть якщо в когось випадково відкрита CRM.

Що змінюється в офлайн-режимі:

  1. Віджет пише «Зараз ми не в мережі — залиште пошту, і ми відповімо» і просить пошту до повідомлення. Без пошти повідомлення не піде: інакше відповіді просто нікуди було б прийти.
  2. У CRM чат позначається «Офлайн-заявка», і пошта відвідувача видна одразу поруч.
  3. Сповіщення в Telegram (якщо бот підключено) надходить одразу — такий чат чекає на відповідь за визначенням.

Поки оператори в мережі, чат працює як звичайно, а пошта необов’язкова: відвідувач усе одно може натиснути «Отримувати відповіді на пошту» — знадобиться тому, хто от-от закриє вкладку.

Відповіді на пошту

Якщо відвідувач залишив пошту й не прочитав відповідь оператора у віджеті приблизно за 2 хвилини, відповідь надходить йому листом. Кілька відповідей поспіль приходять одним листом, а не чергою сповіщень. У листі лише відповіді оператора — не все листування — і посилання на ту сторінку вашого сайту, з якої писав відвідувач: продовжити розмову можна просто там.

Мова віджета

Віджет говорить мовою вашої сторінки: він читає <html lang> і бере перші дві літери, тож en-GB і en — це однаково англійська.

  • Вбудовані тексти інтерфейсу є для ru, en, uk, es, de і zh. Будь-яка інша мова отримує англійську.
  • Ваші власні тексти з CRM підкоряються правилам запасного варіанта, описаним вище.
  • Односторінковий застосунок з перемикачем мови? Просто оновіть document.documentElement.lang — віджет помітить зміну й перемалює свої тексти на льоту, без перезавантаження.
Перемикання мови в SPAjs
function setLanguage(lang) {
  // ...перемикаємо власні переклади...
  document.documentElement.lang = lang; // чат підлаштується слідом
}

Упізнавайте користувачів, які увійшли

За замовчуванням оператор бачить «Відвідувач». Якщо на вашому сайті є облікові записи, можна краще: покладіть ім’я користувача й ваш внутрішній id у localStorage — і оператор бачитиме, з ким розмовляє.

support_client_namestring, ≤ 80 charsнеобовʼязково
Показується оператору замість «Відвідувач». За ним можна шукати чати в CRM.
support_client_idstring, ≤ 64 charsнеобовʼязково
Ваш внутрішній id користувача (наприклад, user_8421). Показується в картці відвідувача, щоб оператори могли знайти акаунт у вашій власній адмінці.

Віджет ці ключі лише читає. Він надсилає їх під час підключення й знову перед кожним повідомленням, якщо вони змінилися, — тож неважливо, задасте ви їх до завантаження сторінки чи одразу після входу користувача. Під час виходу видаліть обидва ключі: інакше наступна людина за тим самим комп’ютером успадкує ім’я.

// support-identity.js
function setSupportIdentity(user) {
  try {
    if (user) {
      localStorage.setItem('support_client_name', user.name.slice(0, 80));
      localStorage.setItem('support_client_id', String(user.id).slice(0, 64));
    } else {
      localStorage.removeItem('support_client_name');
      localStorage.removeItem('support_client_id');
    }
  } catch {
    // Сховище заблоковано (суворі налаштування приватності) — чат однаково працює, просто анонімно
  }
}

// Після успішного входу
setSupportIdentity({ id: 'user_8421', name: 'Anna Schmidt' });

// Під час виходу
setSupportIdentity(null);

Вкладення й ліміти

Відвідувачі можуть прикріплювати файли до повідомлень — скриншот помилки вартий тисячі слів.

ЩоЛіміт
Зображенняjpeg, png, gif, webp, avif
Документиpdf, doc, docx, xls, xlsx, txt, csv
Розмір файлудо 10 МБ
Повідомлення20 за хвилину в одному чаті
З’єднаннядо 12 одночасно з одного IP
Нові чати10 з одного IP за годину

Ліміти щедрі для людей і дратівливі для ботів — саме в цьому й суть. Офіс, повний людей за одним IP, з сайтом, відкритим у дюжині вкладок, — це єдиний випадок, коли ви можете помітити ліміт з’єднань.

Відкриття чату власною кнопкою

Кругла кнопка в куті підходить більшості сайтів, але іноді чату місце у вашому власному дизайні: посилання «Напишіть нам» у шапці, кнопка на сторінці тарифів, значок допомоги в застосунку. Є два способи — і один із них без жодного рядка JavaScript.

Без JavaScript: data-crm-chat

Додайте data-crm-chat до будь-якого елемента — і клік по ньому відкриє чат:

Будь-яка сторінкаhtml
<a href="/contacts" data-crm-chat="open">Напишіть нам</a>

<button type="button" data-crm-chat="toggle">💬 Підтримка</button>
  • open (він же — для порожнього значення), close або toggle.
  • Елементи, додані пізніше, — модальні вікна, сторінки односторінкового застосунку — теж працюють: віджет слухає весь документ.
  • Вбудований запасний варіант. Якщо чат на сайті недоступний (або widget.js заблоковано), клік не перехоплюється, і посилання просто веде за своїм href. Спрямуйте його на сторінку контактів — і ніхто не впреться в мертву кнопку.

З JavaScript: window.crmChat

crmChat.open()functionнеобовʼязково
Відкриває панель чату.
crmChat.close()functionнеобовʼязково
Закриває її.
crmChat.toggle()functionнеобовʼязково
Відкриває, якщо закрита, і закриває, якщо відкрита.
crmChat.isOpen()() => booleanнеобовʼязково
Чи відкрита панель просто зараз.
crmChat.availableboolean | nullнеобовʼязково
true — чат на сайті працює; false — ні (тариф, налаштування); null — поки невідомо, віджет саме питає.
crmChat.unreadnumberнеобовʼязково
Відповіді оператора, яких відвідувач ще не бачив. Поки панель відкрита — завжди 0.

widget.js створює window.crmChat одразу, ще до завантаження самого чату. Виклики, зроблені в цю першу секунду, запам’ятовуються й виконуються, щойно чат готовий, — клік по вашій кнопці не загубиться. У режимі Live в CRM чат не завантажується взагалі, а crmChat тихо нічого не робить — тож і там ваш код не впаде.

document.querySelector('#help').addEventListener('click', () => {
  // ?. — на випадок, якщо блокувальник реклами не дав завантажитися widget.js
  window.crmChat?.open();
});

Сховати вбудовану кнопку

Маєте власну кнопку? Сховайте круглу кнопку віджета атрибутом data-chat-button="hidden" на тому ж тегу скрипта. Панель відкривається й закривається як зазвичай — у неї свій хрестик у шапці.

<script src="https://widget.sitecog.com/widget.js" data-chat-button="hidden" defer></script>

Показати непрочитані на своїй кнопці

Без круглої кнопки відвідувачеві потрібен інший спосіб помітити, що оператор відповів. Віджет за кожної зміни надсилає подію crm:chat на window з { available, open, unread } у detail. Зробіть за нею значок — і сховайте свою кнопку, якщо чат недоступний.

useCrmChat.tstsx
import { useEffect, useState } from 'react';

type ChatState = { available: boolean | null; open: boolean; unread: number };

export function useCrmChat(): ChatState {
  const [state, setState] = useState<ChatState>({ available: null, open: false, unread: 0 });

  useEffect(() => {
    const api = window.crmChat;
    if (api) setState({ available: api.available, open: api.isOpen(), unread: api.unread });

    const onChange = (e: Event) => setState((e as CustomEvent<ChatState>).detail);
    window.addEventListener('crm:chat', onChange);
    return () => window.removeEventListener('crm:chat', onChange);
  }, []);

  return state;
}

// Як користуватися
export function ChatButton() {
  const { available, unread } = useCrmChat();
  if (available === false) return null;

  return (
    <button type="button" onClick={() => window.crmChat?.open()}>
      Підтримка {unread > 0 && <span className="badge">{unread}</span>}
    </button>
  );
}

Content Security Policy для чату

На вашому сайті немає CSP? Пропустіть цей розділ. Якщо є, чату потрібні такі джерела:

Додайте до наявної Content-Security-Policytext
script-src  https://widget.sitecog.com
connect-src https://back.sitecog.com wss://back.sitecog.com
style-src   'unsafe-inline'
img-src     <хост, з якого віддаються файли вашої CRM>
  • script-src — щоб завантажити widget.js і модуль чату.
  • connect-src — для запиту налаштувань і WebSocket. Зверніть увагу на запис wss://: самого https:// для WebSocket недостатньо.
  • style-src — вікно чату живе в shadow DOM з інлайн-тегом <style>, тож суворій політиці може знадобитися 'unsafe-inline' (наприклад, style-src 'self' 'unsafe-inline').
  • img-src — вкладення й власний значок віддаються зі сховища файлів вашого сайту. Відкрийте будь-яке зображення з CRM і дозвольте його хост.

Для просунутих: власний інтерфейс чату

Власний клієнт — це три кроки і ще один, коли нікого немає в мережі:

  1. Прочитайте налаштування

    Чи ввімкнено чат для цього сайту? Що кажуть тексти? Чи є хтось у мережі?
  2. Почніть чат

    Отримайте токен чату через HTTP і збережіть його.
  3. Залиште пошту — в офлайн-режимі

    Відповісти зараз нікому? Збережіть пошту відвідувача до його першого повідомлення.
  4. Спілкуйтеся через WebSocket

    Підключіться з токеном, надсилайте й отримуйте фрейми.

Усі адреси підтримки публічні: API-ключа немає. Сайт розпізнається за заголовком браузера Origin (запасний варіант — Referer), тож викликайте їх зі сторінок на вашому справжньому домені. Домен, який не є сайтом у CRM, отримує 400 з {"message":"unknown_domain"}; запит без жодного origin отримує {"message":"unknown_origin"}.

1. Прочитайте налаштування чату

GET https://back.sitecog.com/support/config?lang=en
langquerystringнеобовʼязково
Мова текстів, наприклад en. Діють ті самі правила запасного варіанта.
{
  "enabled": true,
  "look": {
    "icon": "bubble",
    "iconUrl": null,
    "position": "bottom-right",
    "skin": "indigo",
    "texts": {
      "title": null,
      "subtitle": null,
      "placeholder": null,
      "greeting": null
    }
  },
  "offline": {
    "form": true,
    "away": true,
    "reason": "no_agents"
  }
}
enabledboolean
Чи доступний чат для цього сайту. false — нічого не показуйте й зупиніться тут.
lookobject
Те, що оператор обрав у CRM → Підтримка → Налаштування. Є лише тоді, коли enabled дорівнює true.
look →
iconstring
bubble, headset, question, envelope або spark.
iconUrlstring | null
URL зображення власного значка; null, коли використовується вбудований.
positionstring
bottom-right, bottom-left, top-right або top-left.
skinstring
indigo, emerald, midnight або graphite. У власному інтерфейсі зіставте це зі своїми кольорами — або проігноруйте.
textsobject
Тексти для запитаної мови. null означає «не заповнено, використайте власне значення за замовчуванням».
texts →
titlestring | null
Заголовок вікна чату, ≤ 80 символів.
subtitlestring | null
Рядок під заголовком, ≤ 160 символів.
placeholderstring | null
Напис у полі введення, ≤ 80 символів.
greetingstring | null
Привітання в порожньому чаті, ≤ 200 символів.
offlineobject
Чи може хтось відповісти просто зараз — див. Коли нікого немає в мережі. Є лише при enabled: true. Присутність змінюється щохвилини, тож перепитуйте під час відкриття вікна чату, а не тримайте відповідь у кеші надовго.
offline →
formboolean
Офлайн-режим увімкнено в CRM. false — пошту не просіть ніколи, просто чат.
awayboolean
true — відповісти зараз нікому: покажіть форму й попросіть пошту до першого повідомлення (крок 3). При form: false завжди false.
reasonstring | null
Чому чат в офлайні: after_hours — неробочий час, no_agents — у жодного оператора не відкрита CRM. null, коли away дорівнює false.

2. Почніть або продовжте чат

POST https://back.sitecog.com/support/chat/start
Content-Type: application/json

{
  "visitorId": "5f1c2a9e-3b7d-4c1e-9a0f-8d2b6e4c7a10",
  "lang": "en",
  "token": "<токен, отриманий минулого разу, якщо є>"
}
visitorIdstringобовʼязково
Стабільний id цього браузера: 6–128 символів з A–Z a–z 0–9 _ . : -. Якщо на сторінці працює widget.js, повторно використайте його crm_vid з localStorage; інакше згенеруйте власний один раз (UUID підійде) і зберігайте його. Виняток — режим згоди без згоди: тоді crm_vid немає, і тимчасовий id краще тримати лише в пам’яті, нікуди не записуючи.
langstringнеобовʼязково
Мова відвідувача, наприклад en.
tokenstringнеобовʼязково
Токен чату з попереднього старту. Дійсний токен — ви отримуєте той самий чат з історією. Немає токена або він недійсний — створюється новий чат (а нових чатів — не більше 10 з одного IP за годину, тож не губіть токен).
Відповідьjson
{
  "enabled": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "chatId": 123,
  "lastSeq": 5,
  "readSeq": 5,
  "status": "open",
  "messages": [
    { "seq": 1, "author": "visitor", "text": "Hi! Do you ship to Austria?", "at": "2026-10-01T09:12:03.000Z" },
    { "seq": 2, "author": "agent", "text": "Hi Anna! Yes, 2–4 days.", "at": "2026-10-01T09:13:40.000Z" }
  ],
  "email": null,
  "offline": { "form": true, "away": false, "reason": null }
}
enabledboolean
false — чат для цього сайту вимкнено; більше у відповіді нічого немає.
tokenstring (JWT)
Токен чату, дійсний 180 днів. Збережіть його (наприклад, у localStorage) і завжди перезаписуйте найсвіжішим. Він відкриває WebSocket і наступного разу продовжує цей чат.
chatIdnumber
Id чату.
lastSeqnumber
Порядковий номер останнього повідомлення. Повідомлення в чаті нумеруються 1, 2, 3…; seq — ваш курсор для всього, що нижче.
readSeqnumber
До якого місця дочитав відвідувач.
statusstring
open або closed — як задали оператори в CRM.
messagesMessage[]
Останні 50 повідомлень, від найстарішого. Форма Message така сама, як у фреймах WebSocket.
emailstring | null
Пошта, яку відвідувач уже залишив у цьому чаті. Удруге не питайте — покажіть «Відповімо на …» з можливістю змінити адресу.
offlineobject
Та сама форма, що й offline у налаштуваннях: { form, away, reason }, на момент запиту.
errorstring | null
Приходить замість токена, коли чат не вдалося почати: invalid_visitor (поганий visitorId) або rate_limit_exceeded (забагато нових чатів з цього IP).

3. Залиште пошту

Коли offline.away дорівнює true, збережіть пошту відвідувача до надсилання його першого повідомлення — так робить вбудований віджет, і лише так відповідь дійде до людини, яка вже закрила вкладку. Поки оператори в мережі, той самий виклик працює для необов’язкової кнопки «Отримувати відповіді на пошту».

POST https://back.sitecog.com/support/chat/contact
Content-Type: application/json

{
  "token": "<токен чату з chat/start>",
  "email": "anna@example.com",
  "page": "https://shop.example/delivery?utm_source=newsletter"
}
tokenstringобовʼязково
Токен чату з chat/start. Пошту можна прив’язати лише до власного чату.
emailstringобовʼязково
Куди надсилати відповіді. Повторний виклик з іншою адресою замінює попередню.
pagestringнеобовʼязково
Адреса сторінки, з якої пише відвідувач. З неї вийде посилання «продовжити на сайті» в листі. Приймаються лише сторінки вашого сайту, а параметри й #фрагмент відкидаються — адреса з прикладу збережеться як https://shop.example/delivery.
{ "ok": true, "email": "anna@example.com" }
errorЩо сталося
invalid_emailЦе не схоже на адресу пошти. Попросіть відвідувача перевірити.
invalid_tokenТокена немає, він протермінований або виданий іншому сайту. Викличте chat/start знову.
rate_limit_exceededЛіміт спільний із повідомленнями чату — 20 на хвилину. Зачекайте трохи й повторіть.
disabledЧат для сайту вимкнули.
chat_not_foundЧат видалено. Забудьте токен і почніть новий.
Офлайн-режим у власному клієнтіjs
// chat — відповідь chat/start (або свіжіший фрейм ready)
async function sendFirstMessage(text, email) {
  if (chat.offline.away && !chat.email) {
    const res = await fetch('https://back.sitecog.com/support/chat/contact', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ token: chatToken, email, page: location.href }),
    }).then((r) => r.json());

    if (!res.ok) return showEmailError(res.error); // повідомлення не губимо, хай виправить адресу
    chat.email = res.email;
  }
  send(text); // фрейм send через WebSocket із кроку 4
}

4. Підключіться до WebSocket

wss://back.sitecog.com/support/ws

Токен передається в списку підпротоколів, а не в URL — URL потрапляють у логи, підпротоколи ні. Передайте два підпротоколи: версію протоколу і token. + ваш токен чату. Браузер сам надсилає Origin, і це має бути ваш сайт.

const socket = new WebSocket('wss://back.sitecog.com/support/ws', [
  'crm.support.v1',
  'token.' + chatToken,
]);

socket.onopen = () => {
  // Привітайтеся й скажіть серверу, яке останнє повідомлення у вас уже є
  socket.send(JSON.stringify({ t: 'hello', since: lastSeq }));
};

Після hello сервер відповідає фреймом ready, у якому є все, що ви пропустили після since. Кожен фрейм — це JSON-об’єкт із типом у t, не більше 8 КБ. Сервер пінгує кожні 25 секунд; браузери відповідають на пінги автоматично, вам нічого робити не треба.

Фрейми, які ви надсилаєте

hello{ t, since }
Початок сесії. since — останній seq, який у вас є (0, якщо жодного). Відповідь — ready.
send{ t, id, text, file? }
Надіслати повідомлення. id — ваш власний id повідомлення, [A-Za-z0-9_-], 1–64 символи; повторне надсилання з тим самим id ніколи не створює дубліката, тож повторюйте сміливо. text — до 4000 символів. file — квитанція із завантаження (див. вкладення).
read{ t, seq }
«Я прочитав усе до seq». Керує позначками прочитання, які бачить оператор.
typing{ t }
Відвідувач набирає текст. Надсилайте не частіше ніж раз на ~2 секунди, поки він друкує.
history{ t, before }
Завантажити старіші повідомлення: по 50 на сторінку до seq = before (0 = від найновішого). Відповідь — history.
profile{ t, name?, contact?, clientId? }
Розкажіть оператору, хто це: name ≤ 80, contact (телефон або email) ≤ 120, clientId ≤ 64. Хоча б одне має бути непорожнім.
Прикладиjson
{ "t": "hello", "since": 5 }
{ "t": "send", "id": "m-1727775123-1", "text": "Do you ship to Austria?" }
{ "t": "read", "seq": 7 }
{ "t": "typing" }
{ "t": "history", "before": 51 }
{ "t": "profile", "name": "Anna Schmidt", "contact": "anna@example.com", "clientId": "user_8421" }

Фрейми, які ви отримуєте

readyframe
Відповідь на hello: стан чату плюс те, що ви пропустили.
ready →
chatIdnumber
Id чату.
visitorOnlineboolean
Чи відвідувач онлайн у цьому чаті.
lastSeqnumber
Номер останнього повідомлення.
statusstring
open або closed.
read{ agent, visitor }
До якого місця дочитала кожна сторона (seq).
unreadnumber
Повідомлення, які відвідувач ще не прочитав, — зручно для бейджа.
messagesMessage[]
Повідомлення після since.
gapboolean
true, якщо ви пропустили більше, ніж уміщається в одне наздоганяння, — решту довантажте через history.
emailstring | null
Пошта, яку відвідувач залишив у цьому чаті, — як у chat/start.
offline{ form, away, reason }
Чи може хтось відповісти просто зараз — та сама форма, що й у налаштуваннях. Приходить під час кожного перепідключення, тож довго відкрита сторінка помітить, що оператори пішли або повернулися.
messageframe
Нове повідомлення в чаті — відповідь оператора, автовідповідь тощо.
message →
chatIdnumber
Id чату.
mMessage
Саме повідомлення.
m →
seqnumber
Порядковий номер у чаті. Використовуйте для сортування й прибирання дублікатів.
authorstring
visitor, agent або system.
textstring
Звичайний текст. Рендерте його як текст (textContent), ніколи як HTML.
atstring (ISO 8601)
Коли повідомлення збережено.
file{ url, name, mime, size, kind } | null
Є лише у вкладень. kind — image або doc.
ack{ id, seq, at }
Ваш send з цим id збережено як повідомлення seq. Змініть стан «надсилається…» на «надіслано».
read{ chatId, by, seq }
Хтось (by) прочитав до seq — наприклад, оператор прочитав ваші повідомлення.
typing{ chatId, by }
Інша сторона набирає текст. Покажіть крапки на кілька секунд.
history{ chatId, before, messages, done }
Відповідь на ваш запит history. done: true — старіших повідомлень немає.
presenceframe
Хтось у чаті з’явився онлайн або пішов.
chat_goneframe
Чат видалено в CRM. Викиньте збережений токен і знову викличте chat/start, щоб отримати новий чат.
error{ code, id? }
Щось пішло не так із фреймом; id заповнено, коли йдеться про одне з ваших повідомлень. Коди перелічено нижче.
Повідомлення від оператораjson
{
  "t": "message",
  "chatId": 123,
  "m": { "seq": 6, "author": "agent", "text": "Your parcel left the warehouse today 🚚", "at": "2026-10-01T09:20:11.000Z" }
}

Повний мінімальний клієнт

Старт → підключення → надсилання → отримання, з перепідключеннями. Близько 90 рядків, без залежностей — під’єднайте власні render і markSent, і у вас робочий чат.

support-chat.jsjs
const SUPPORT = 'https://back.sitecog.com/support';
const WS_URL = 'wss://back.sitecog.com/support/ws';

const store = {
  get: (k) => { try { return localStorage.getItem(k); } catch { return null; } },
  set: (k, v) => { try { localStorage.setItem(k, v); } catch {} },
  del: (k) => { try { localStorage.removeItem(k); } catch {} },
};

// Беремо id відвідувача з віджета, якщо він є, інакше тримаємо власний
// (у режимі згоди до згоди crm_vid немає — тоді власний id тримайте лише в пам’яті)
function visitorId() {
  let id = store.get('crm_vid') || store.get('my_chat_vid');
  if (!id) {
    id = crypto.randomUUID();
    store.set('my_chat_vid', id);
  }
  return id;
}

let socket = null;
let lastSeq = 0;
let failures = 0;
const seen = new Set();

function show(m) {
  if (seen.has(m.seq)) return; // те саме повідомлення може прийти зі start, ready і message
  seen.add(m.seq);
  lastSeq = Math.max(lastSeq, m.seq);
  render(m); // ваш інтерфейс: m.author, m.text (як текст!), m.at, m.file
}

async function boot() {
  const res = await fetch(SUPPORT + '/chat/start', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      visitorId: visitorId(),
      lang: (document.documentElement.lang || 'en').slice(0, 2),
      token: store.get('my_chat_token') || undefined,
    }),
  });
  const data = await res.json();
  if (!data.enabled) return;            // чат для цього сайту вимкнено
  if (data.error) throw new Error(data.error);

  store.set('my_chat_token', data.token); // дійсний 180 днів
  data.messages.forEach(show);
  lastSeq = Math.max(lastSeq, data.lastSeq);
  connect(data.token);
}

function connect(token) {
  socket = new WebSocket(WS_URL, ['crm.support.v1', 'token.' + token]);

  socket.onopen = () => {
    failures = 0;
    socket.send(JSON.stringify({ t: 'hello', since: lastSeq }));
  };

  socket.onmessage = (event) => {
    const frame = JSON.parse(event.data);
    if (frame.t === 'ready') frame.messages.forEach(show);
    else if (frame.t === 'message') show(frame.m);
    else if (frame.t === 'ack') markSent(frame.id, frame.seq);
    else if (frame.t === 'chat_gone') {
      store.del('my_chat_token');
      socket.onclose = null;
      socket.close();
      boot();                           // зовсім новий чат
    } else if (frame.t === 'error') console.warn('[chat]', frame.code, frame.id);
  };

  socket.onclose = (event) => {
    if (event.code === 4402 || event.code === 4403) return; // вимкнено / заборонено: зупиняємося
    failures += 1;
    const delay = Math.min(30000, 1000 * 2 ** failures) + Math.random() * 1000;
    // Кілька невдач поспіль можуть означати, що токен уже недійсний, — починаємо спочатку
    setTimeout(() => (failures > 3 ? boot() : connect(token)), delay);
  };
}

function send(text) {
  const id = crypto.randomUUID();       // повторювати безпечно: той самий id = те саме повідомлення
  socket.send(JSON.stringify({ t: 'send', id, text }));
  return id;                            // показуйте «надсилається…», доки не прийде ack із цим id
}

boot();

Перепідключення й пропущені повідомлення

  • Зберігайте найбільший seq, який ви показали. Після перепідключення надішліть { "t": "hello", "since": lastSeq } — фрейм ready принесе рівно те, що ви пропустили.
  • Якщо ready.gap дорівнює true, ви пропустили багато; заповніть прогалину запитами history.
  • Повідомлення можуть прийти двічі (наприклад, з chat/start і з ready). Прибирайте дублікати за seq.
  • Збільшуйте паузу між спробами (1 с, 2 с, 4 с… з дрібкою випадковості) — перезапуск сервера не повинен перетворитися на тисняву перепідключень.
  • Не впевнені, чи дійшло повідомлення до сервера? Надішліть його ще раз із тим самим id. Воно збережеться один раз.

Вкладення у власному інтерфейсі

Файл проходить три короткі кроки: отримати дозвіл, завантажити файл, надіслати квитанцію.

// 1. Дозвіл на завантаження — дійсний 5 хвилин
const { permit } = await fetch('https://back.sitecog.com/support/chat/upload-permit', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ token: chatToken }),
}).then((r) => r.json());

// 2. Завантажте файл (зображення або документи, до 10 МБ)
const form = new FormData();
form.append('file', fileInput.files[0]);
const upload = await fetch('https://back.sitecog.com/storage/support/upload', {
  method: 'POST',
  headers: { 'x-support-permit': permit },
  body: form,
}).then((r) => r.json());
// upload = { receipt, url, name, mime, size, kind }

// 3. Надішліть повідомлення з квитанцією (текст може бути порожнім)
socket.send(JSON.stringify({ t: 'send', id: crypto.randomUUID(), text: '', file: upload.receipt }));

Помилки й коди закриття

Помилки на живому з’єднанні приходять як { "t": "error", "code": "…", "id": "…" }:

КодЩо сталося
rate_limit_exceededПонад 20 повідомлень за хвилину в цьому чаті. Зачекайте й надішліть знову з тим самим id.
invalid_textТексту немає або він довший за 4000 символів.
invalid_fileКвитанцію завантаження не вдалося перевірити — прострочена або з іншого чату. Завантажте ще раз.
invalid_message_idid не є рядком з 1–64 символів A–Z a–z 0–9 _ -.
frame_too_largeФрейм більший за 8 КБ.
invalid_jsonФрейм не є коректним JSON.
unknown_frameНевідомий t. Одруківка або фрейм із новішої версії протоколу.
empty_profileФрейм profile, у якому всі поля порожні.
chat_not_foundЧату більше не існує. Почніть новий.
disabledЧат для сайту вимкнено.

Саме з’єднання можуть відхилити або закрити:

КолиКодЩо означає
Рукостискання401 unauthorizedТокена немає, він недійсний або прострочений. Викличте chat/start ще раз.
Рукостискання429 too_many_connectionsПонад 12 з’єднань з цього IP. Закрийте зайві.
Закриття4402Чат для сайту вимкнено. Не перепідключайтеся.
Закриття4403Заборонено. Не перепідключайтеся.

Якщо щось не так

Кнопка чату не з’являється

  • Тариф не включає чат або чат вимкнено. Адреса налаштувань відповідає { "enabled": false }.
  • Невідомий домен. Сторінку відкрито на домені, який не є сайтом у CRM, — тестовий хост, localhost, новий домен, який ви ще не додали. (www. значення не має, його відкидають.)
  • Його блокує CSP. Пошукайте «Refused to load» або «Refused to connect» у консолі браузера й перевірте розділ про CSP.
  • Ви дивитеся на сайт усередині CRM. У режимі Live віджет завантажує лише редактор — ні чату, ні аналітики. Відкрийте сайт у звичайній вкладці. А от у чужих iframe (хтось вбудував ваш сайт у свою сторінку) редактор не вантажиться: лише в рамці Live самої CRM.
  • На цій сторінці немає widget.js — легко проґавити, коли на сайті кілька layout-ів.
  • Чат щойно ввімкнули, а ваш браузер ще до ~10 хвилин пам’ятає старі налаштування (див. нижче).

Можна просто спитати в сервера, що він думає про ваш домен:

curl "https://back.sitecog.com/support/config?lang=en" -H "Origin: https://your-site.com"

Зміни з CRM не видно

  • Налаштування кешуються в браузері приблизно на 10 хвилин. Або зачекайте, або видаліть ключ crm_chat_cfg у DevTools → Application → Local Storage і перезавантажте сторінку.
  • Ви редагували тексти для однієї мови, а в сторінки інший <html lang>. Перевірте правила запасного варіанта.
  • Текстове поле порожнє, тож віджет показує свій вбудований текст — так задумано.