У відвідувача питання об 11-й вечора, а ваша форма зворотного зв’язку — як записка в пляшці. Чат підтримки це виправляє: кнопка чату на кожній сторінці вашого сайту, а розмова потрапляє в CRM, де ваша команда відповідає в реальному часі. Якщо ви вже додали widget.js для редагування на сайті, до чату вас відділяє рівно нуль рядків коду.
Як працює чат підтримки
- Відвідувач натискає кнопку чату в кутку вашого сайту й пише повідомлення (зі скриншотом, якщо слів забракне).
- Повідомлення миттєво з’являється в CRM → Підтримка → Чати. Усе працює через WebSocket, тож ніхто не мусить тиснути F5.
- Оператор відповідає з CRM, і відвідувач одразу бачить відповідь — разом з індикатором набору тексту й позначками прочитання.
Щоб ніхто не проґавив чат, поки варить каву, оператори отримують у CRM лічильник непрочитаних, сповіщення браузера й, за бажання, Telegram-бота з тихими годинами. Усе це налаштовується в CRM, а не у вашому коді.
Чат і режим згоди
Якщо на сайті ввімкнено режим згоди, чат не чекає на «так» у банері: відвідувач може написати вам одразу. Доки згоди немає, віджет не зберігає ідентифікатор відвідувача, а бере для чату одноразовий — він живе лише в пам’яті сторінки. Давнішу розмову це не зачіпає: вона, як і раніше, відновлюється за власним crm_chat_token.
Єдине, чого бракуватиме до згоди, — атрибуції: без ідентифікаторів відвідувача й сесії розмову не прив’язати до каналу, UTM-міток чи рекламного посилання.
Додайте онлайн-чат на свій сайт
Окремого коду для чату немає. Той самий єдиний тег, на якому працює все інше, робить і це:
<script src="https://widget.sitecog.com/widget.js" defer></script>Коли чат доступний для вашого сайту, widget.js сам підвантажує модуль чату (widget.support.js), і з’являється плаваюча кнопка чату. Коли ні — відвідувачі не завантажують нічого зайвого. Додати тег двічі не страшно — другу копію буде проігноровано.
Переконайтеся, що ваш тариф включає чат підтримки
Чат доступний на тарифах, які його включають. Немає чату в тарифі — немає й кнопки, хоч би що казав код.Перевірте, що ваш домен є сайтом у CRM
Чат розпізнає ваш сайт за доменом, на якому відкрито сторінку (Originбраузера;www.ігнорується). Домен має бути сайтом, який ви додали в CRM.Додайте widget.js на сторінку
Тег вище або варіанти для Next.js / Vite з «Віджет і розмітка» → Крок 1.Налаштуйте вигляд у 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 символів | Показується в порожньому чаті, поки відвідувач ще нічого не написав. |
Тексти для кожної мови й запасний варіант
Кожен текст можна заповнити для кожної мови вашого сайту. Віджет обирає текст для мови відвідувача так:
- точний збіг мови;
- інакше — збіг за першими двома літерами;
- інакше — перший заповнений вами варіант.
Якщо текст порожній, віджет бере власний вбудований текст. Тож можна залишити все порожнім і однаково отримати цілком пристойний чат — налаштування потрібні тоді, коли хочеться, щоб він звучав як ви.
Автовідповідач
Увімкніть автовідповідач і напишіть текст для кожної мови (до 1000 символів). Він відповідає на перше повідомлення нової розмови, тож відвідувач знає, що його почули, навіть коли вся команда на нараді. Розмова вважається новою після N хвилин тиші — N ви обираєте від 1 до 180, за замовчуванням 5.
Коли нікого немає в мережі
Живий чат, де ніхто не відповідає, — пастка: відвідувач ставить запитання, чекає, закриває вкладку, і ви так і не дізнаєтеся, хто це був. Офлайн-режим у такі хвилини перетворює чат на форму «залиште пошту» — і запитання не зникає разом із відвідувачем.
Вмикається він у CRM → Підтримка → Налаштування → «Коли нікого немає в мережі». Чат переходить в офлайн-режим, коли:
- у жодного оператора не відкрита CRM цього сайту. Рахується будь-яка сторінка CRM, не лише чати: оператор «у мережі», доки не закрита остання вкладка CRM;
- зараз неробочий час, якщо ви його задали: робочі дні, час «з» і «до» та часовий пояс. Розклад необов’язковий — без нього діє лише перше правило. Поза робочим часом чат в офлайні, навіть якщо в когось випадково відкрита CRM.
Що змінюється в офлайн-режимі:
- Віджет пише «Зараз ми не в мережі — залиште пошту, і ми відповімо» і просить пошту до повідомлення. Без пошти повідомлення не піде: інакше відповіді просто нікуди було б прийти.
- У CRM чат позначається «Офлайн-заявка», і пошта відвідувача видна одразу поруч.
- Сповіщення в Telegram (якщо бот підключено) надходить одразу — такий чат чекає на відповідь за визначенням.
Поки оператори в мережі, чат працює як звичайно, а пошта необов’язкова: відвідувач усе одно може натиснути «Отримувати відповіді на пошту» — знадобиться тому, хто от-от закриє вкладку.
Відповіді на пошту
Якщо відвідувач залишив пошту й не прочитав відповідь оператора у віджеті приблизно за 2 хвилини, відповідь надходить йому листом. Кілька відповідей поспіль приходять одним листом, а не чергою сповіщень. У листі лише відповіді оператора — не все листування — і посилання на ту сторінку вашого сайту, з якої писав відвідувач: продовжити розмову можна просто там.
Мова віджета
Віджет говорить мовою вашої сторінки: він читає <html lang> і бере перші дві літери, тож en-GB і en — це однаково англійська.
- Вбудовані тексти інтерфейсу є для
ru,en,uk,es,deіzh. Будь-яка інша мова отримує англійську. - Ваші власні тексти з CRM підкоряються правилам запасного варіанта, описаним вище.
- Односторінковий застосунок з перемикачем мови? Просто оновіть
document.documentElement.lang— віджет помітить зміну й перемалює свої тексти на льоту, без перезавантаження.
function setLanguage(lang) {
// ...перемикаємо власні переклади...
document.documentElement.lang = lang; // чат підлаштується слідом
}Упізнавайте користувачів, які увійшли
За замовчуванням оператор бачить «Відвідувач». Якщо на вашому сайті є облікові записи, можна краще: покладіть ім’я користувача й ваш внутрішній id у localStorage — і оператор бачитиме, з ким розмовляє.
support_client_namestring, ≤ 80 charsнеобовʼязковоsupport_client_idstring, ≤ 64 charsнеобовʼязково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);import { useEffect } from 'react';
type User = { id: string; name: string };
// user: undefined = ще завантажується, null = вийшов, об’єкт = увійшов
export function useSupportIdentity(user: User | null | undefined) {
useEffect(() => {
if (user === undefined) return; // не стираємо ключі, поки авторизація ще вантажиться
try {
if (user) {
localStorage.setItem('support_client_name', user.name.slice(0, 80));
localStorage.setItem('support_client_id', user.id.slice(0, 64));
} else {
localStorage.removeItem('support_client_name');
localStorage.removeItem('support_client_id');
}
} catch {
// Сховище заблоковано — чат лишається анонімним, нічого не ламається
}
}, [user]);
}
// В оболонці застосунку:
// const { user } = useAuth();
// useSupportIdentity(user);// app/support-identity.tsx
'use client';
import { useEffect } from 'react';
export function SupportIdentity({ id, name }: { id?: string; name?: string }) {
useEffect(() => {
try {
if (id && name) {
localStorage.setItem('support_client_name', name.slice(0, 80));
localStorage.setItem('support_client_id', id.slice(0, 64));
} else {
localStorage.removeItem('support_client_name');
localStorage.removeItem('support_client_id');
}
} catch {}
}, [id, name]);
return null; // нічого не рендерить, лише синхронізує ключі
}
// app/layout.tsx — Server Component точно знає сесію
// const user = await getCurrentUser(); // ваша авторизація
// ...
// <body>
// {children}
// <SupportIdentity id={user?.id} name={user?.name} />
// <Script src="https://widget.sitecog.com/widget.js" strategy="afterInteractive" />
// </body>Вкладення й ліміти
Відвідувачі можуть прикріплювати файли до повідомлень — скриншот помилки вартий тисячі слів.
| Що | Ліміт |
|---|---|
| Зображення | 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 до будь-якого елемента — і клік по ньому відкриє чат:
<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необовʼязковоwidget.js створює window.crmChat одразу, ще до завантаження самого чату. Виклики, зроблені в цю першу секунду, запам’ятовуються й виконуються, щойно чат готовий, — клік по вашій кнопці не загубиться. У режимі Live в CRM чат не завантажується взагалі, а crmChat тихо нічого не робить — тож і там ваш код не впаде.
document.querySelector('#help').addEventListener('click', () => {
// ?. — на випадок, якщо блокувальник реклами не дав завантажитися widget.js
window.crmChat?.open();
});export function HelpButton() {
return (
<button type="button" onClick={() => window.crmChat?.open()}>
Потрібна допомога?
</button>
);
}Сховати вбудовану кнопку
Маєте власну кнопку? Сховайте круглу кнопку віджета атрибутом data-chat-button="hidden" на тому ж тегу скрипта. Панель відкривається й закривається як зазвичай — у неї свій хрестик у шапці.
<script src="https://widget.sitecog.com/widget.js" data-chat-button="hidden" defer></script>import Script from 'next/script';
<Script src="https://widget.sitecog.com/widget.js" data-chat-button="hidden" strategy="afterInteractive" />Показати непрочитані на своїй кнопці
Без круглої кнопки відвідувачеві потрібен інший спосіб помітити, що оператор відповів. Віджет за кожної зміни надсилає подію crm:chat на window з { available, open, unread } у detail. Зробіть за нею значок — і сховайте свою кнопку, якщо чат недоступний.
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? Пропустіть цей розділ. Якщо є, чату потрібні такі джерела:
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 і дозвольте його хост.
Для просунутих: власний інтерфейс чату
Власний клієнт — це три кроки і ще один, коли нікого немає в мережі:
Прочитайте налаштування
Чи ввімкнено чат для цього сайту? Що кажуть тексти? Чи є хтось у мережі?Почніть чат
Отримайте токен чату через HTTP і збережіть його.Залиште пошту — в офлайн-режимі
Відповісти зараз нікому? Збережіть пошту відвідувача до його першого повідомлення.Спілкуйтеся через WebSocket
Підключіться з токеном, надсилайте й отримуйте фрейми.
Усі адреси підтримки публічні: API-ключа немає. Сайт розпізнається за заголовком браузера Origin (запасний варіант — Referer), тож викликайте їх зі сторінок на вашому справжньому домені. Домен, який не є сайтом у CRM, отримує 400 з {"message":"unknown_domain"}; запит без жодного origin отримує {"message":"unknown_origin"}.
1. Прочитайте налаштування чату
GET https://back.sitecog.com/support/config?lang=enlangquerystringнеобовʼязково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"
}
}{ "enabled": false }enabledbooleanfalse — нічого не показуйте й зупиніться тут.lookobjectlook →
iconstringbubble, headset, question, envelope або spark.iconUrlstring | nullpositionstringbottom-right, bottom-left, top-right або top-left.skinstringindigo, emerald, midnight або graphite. У власному інтерфейсі зіставте це зі своїми кольорами — або проігноруйте.textsobjectnull означає «не заповнено, використайте власне значення за замовчуванням».texts →
titlestring | nullsubtitlestring | nullplaceholderstring | nullgreetingstring | nullofflineobjectenabled: true. Присутність змінюється щохвилини, тож перепитуйте під час відкриття вікна чату, а не тримайте відповідь у кеші надовго.offline →
formbooleanfalse — пошту не просіть ніколи, просто чат.awaybooleantrue — відповісти зараз нікому: покажіть форму й попросіть пошту до першого повідомлення (крок 3). При form: false завжди false.reasonstring | nullafter_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обовʼязковоA–Z a–z 0–9 _ . : -. Якщо на сторінці працює widget.js, повторно використайте його crm_vid з localStorage; інакше згенеруйте власний один раз (UUID підійде) і зберігайте його. Виняток — режим згоди без згоди: тоді crm_vid немає, і тимчасовий id краще тримати лише в пам’яті, нікуди не записуючи.langstringнеобовʼязковоen.tokenstringнеобовʼязково{
"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 }
}enabledbooleanfalse — чат для цього сайту вимкнено; більше у відповіді нічого немає.tokenstring (JWT)localStorage) і завжди перезаписуйте найсвіжішим. Він відкриває WebSocket і наступного разу продовжує цей чат.chatIdnumberlastSeqnumberseq — ваш курсор для всього, що нижче.readSeqnumberstatusstringopen або closed — як задали оператори в CRM.messagesMessage[]emailstring | nullofflineobjectoffline у налаштуваннях: { form, away, reason }, на момент запиту.errorstring | nullinvalid_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" }{ "ok": false, "error": "invalid_email" }error | Що сталося |
|---|---|
invalid_email | Це не схоже на адресу пошти. Попросіть відвідувача перевірити. |
invalid_token | Токена немає, він протермінований або виданий іншому сайту. Викличте chat/start знову. |
rate_limit_exceeded | Ліміт спільний із повідомленнями чату — 20 на хвилину. Зачекайте трохи й повторіть. |
disabled | Чат для сайту вимкнули. |
chat_not_found | Чат видалено. Забудьте токен і почніть новий. |
// 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 }history{ t, before }seq = before (0 = від найновішого). Відповідь — history.profile{ t, name?, contact?, clientId? }name ≤ 80, contact (телефон або email) ≤ 120, clientId ≤ 64. Хоча б одне має бути непорожнім.{ "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" }Фрейми, які ви отримуєте
readyframehello: стан чату плюс те, що ви пропустили.ready →
chatIdnumbervisitorOnlinebooleanlastSeqnumberstatusstringopen або closed.read{ agent, visitor }unreadnumbermessagesMessage[]since.gapbooleantrue, якщо ви пропустили більше, ніж уміщається в одне наздоганяння, — решту довантажте через history.emailstring | nullchat/start.offline{ form, away, reason }messageframemessage →
chatIdnumbermMessagem →
seqnumberauthorstringvisitor, agent або system.textstringtextContent), ніколи як HTML.atstring (ISO 8601)file{ url, name, mime, size, kind } | nullkind — 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 — старіших повідомлень немає.presenceframechat_goneframechat/start, щоб отримати новий чат.error{ code, id? }id заповнено, коли йдеться про одне з ваших повідомлень. Коди перелічено нижче.{
"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, і у вас робочий чат.
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_id | id не є рядком з 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>. Перевірте правила запасного варіанта. - Текстове поле порожнє, тож віджет показує свій вбудований текст — так задумано.