Diil Docs
  1. Doku
  2. Das Widget

Support-Chat: Live-Chat auf deiner Website mit einem Tag

Aktualisiert:

Ein Besucher hat um 23 Uhr eine Frage, und dein Kontaktformular fühlt sich an wie eine Flaschenpost. Der Support-Chat löst das: ein Chat-Button auf jeder Seite deiner Website, und die Unterhaltung landet im CRM, wo dein Team in Echtzeit antwortet. Hast du widget.js schon fürs Live-Editing eingebunden, ist der Chat buchstäblich null Zeilen Code entfernt.

So funktioniert der Support-Chat

  1. Ein Besucher klickt auf den Chat-Button in der Ecke deiner Site und schreibt eine Nachricht (mit Screenshot, wenn ihm die Worte fehlen).
  2. Die Nachricht erscheint sofort unter CRM → Support → Chats. Alles läuft über einen WebSocket, niemand muss F5 drücken.
  3. Ein Operator antwortet aus dem CRM, und der Besucher sieht die Antwort sofort — inklusive Tipp-Anzeige und Lesebestätigungen.

Damit niemand beim Kaffeekochen einen Chat verpasst, bekommen Operatoren einen Ungelesen-Zähler im CRM, Browser-Benachrichtigungen und auf Wunsch einen Telegram-Bot mit Ruhezeiten. All das stellst du im CRM ein, nicht in deinem Code.

Läuft auf deiner Site der Consent-Modus, funktioniert der Chat trotzdem schon vor der Einwilligung — wer eine Frage hat, muss dafür nicht erst den Cookie-Banner abnicken. Bis zur Zustimmung nutzt das Widget eine einmalige Identität, die nur im Arbeitsspeicher lebt und nirgends gespeichert wird. Eine ältere Unterhaltung kommt trotzdem zurück — über ihren eigenen crm_chat_token.

Nur Attribution gibt es vor der Einwilligung nicht: Ohne Besucher- und Session-ID ist der Chat mit keinem Kanal, keinen UTM-Tags und keinem Werbelink verknüpft.

Live-Chat zu deiner Website hinzufügen

Chat-spezifischen Code gibt es nicht. Derselbe einzelne Tag, der alles andere antreibt, erledigt den Job:

Vor </body>, auf jeder Seitehtml
<script src="https://widget.sitecog.com/widget.js" defer></script>

Ist der Chat für deine Site verfügbar, lädt widget.js das Chat-Bundle (widget.support.js) von selbst nach, und ein schwebender Chat-Button erscheint. Ist er es nicht, laden Besucher nichts zusätzlich herunter. Den Tag doppelt einzubinden schadet nicht — die zweite Kopie wird ignoriert.

  1. Prüfen, ob dein Tarif den Support-Chat enthält

    Der Chat ist in Tarifen verfügbar, die ihn enthalten. Kein Chat im Tarif — kein Button, egal was der Code sagt.
  2. Prüfen, ob deine Domain eine Site im CRM ist

    Der Chat erkennt deine Site an der Domain, auf der die Seite geöffnet wird (dem Origin des Browsers; www. wird ignoriert). Die Domain muss eine Site sein, die du im CRM angelegt hast.
  3. widget.js auf der Seite einbinden

    Der Tag oben oder die Next.js- / Vite-Varianten aus Widget & Markup → Schritt 1.
  4. Im CRM gestalten

    Symbol, Farben, Position und Texte findest du unter CRM → Support → Einstellungen — siehe unten.

Aussehen und Texte im CRM

Alles Visuelle wird unter CRM → Support → Einstellungen konfiguriert. Keine CSS-Overrides, keine Redeploys: Dort ändern, und das Widget übernimmt es.

OptionErlaubte WerteHinweise
Symbolbubble, headset, question, envelope, spark oder dein eigenes BildEin eigenes Symbol wählst du aus dem Dateispeicher deiner Site.
Positionbottom-right Standard, bottom-left, top-right, top-leftNimm die Ecke, in der nicht schon dein Cookie-Banner sitzt.
Farbschemaindigo Standard, emerald, midnight, graphiteFertige Paletten für Button und Chatfenster.
Überschriftbis 80 ZeichenDer Kopf des Chatfensters.
Zeile unter der Überschriftbis 160 ZeichenDie Zeile unter der Überschrift — ein guter Platz für „Wir antworten meist innerhalb von 10 Minuten“.
Platzhalter im Eingabefeldbis 80 ZeichenDer graue Hinweis im Nachrichtenfeld.
Begrüßung im leeren Chatbis 200 ZeichenWird in einem leeren Chat angezeigt, bevor der Besucher etwas geschrieben hat.

Texte pro Sprache und Fallback

Jeder Text lässt sich für jede Sprache deiner Site ausfüllen. Das Widget wählt den Text für die Sprache des Besuchers so:

  1. exakter Sprachtreffer;
  2. sonst ein Treffer auf den ersten zwei Buchstaben;
  3. sonst der erste Eintrag, den du ausgefüllt hast.

Ist ein Text leer, nimmt das Widget seinen eigenen eingebauten Text. Du kannst also alles leer lassen und bekommst trotzdem einen völlig ordentlichen Chat — die Einstellungen sind dafür da, dass er nach dir klingt.

Automatische Antwort

Schalte die automatische Antwort ein und schreib einen Text pro Sprache (bis 1000 Zeichen). Sie antwortet auf die erste Nachricht einer neuen Unterhaltung, damit der Besucher weiß, dass er gehört wurde, auch wenn das ganze Team im Meeting sitzt. Eine Unterhaltung gilt nach N Minuten Stille als neu — N wählst du zwischen 1 und 180, Standard ist 5.

Wenn niemand online ist

Ein Live-Chat, in dem niemand antwortet, ist eine Falle: Der Besucher stellt seine Frage, wartet, schließt den Tab – und du erfährst nie, wer das war. Der Offline-Modus macht aus dem Chat genau in solchen Momenten ein „Hinterlass deine E-Mail“-Formular, damit die Frage nicht zusammen mit dem Besucher verschwindet.

Du schaltest ihn unter CRM → Support → Einstellungen → „Wenn niemand online ist“ ein. Der Chat geht in den Offline-Modus, wenn:

  • kein Operator das CRM für diese Site offen hat. Jede CRM-Seite zählt, nicht nur die Chats – ein Operator ist „online“, bis er den letzten CRM-Tab schließt;
  • gerade keine Arbeitszeit ist, falls du eine festgelegt hast: Arbeitstage, von / bis und eine Zeitzone. Der Zeitplan ist optional – ohne ihn gilt nur die erste Regel. Außerhalb der Arbeitszeit ist der Chat offline, auch wenn zufällig jemand das CRM offen hat.

Was sich im Offline-Modus ändert:

  1. Das Widget zeigt „Wir sind gerade offline – hinterlass deine E-Mail, wir antworten“ und fragt vor der Nachricht nach der E-Mail. Ohne E-Mail keine Nachricht: Sonst wüsste die Antwort nicht, wohin.
  2. Im CRM bekommt der Chat die Markierung „Offline-Anfrage“, die E-Mail des Besuchers steht gleich daneben.
  3. Die Telegram-Benachrichtigung (falls du den Bot verbunden hast) geht sofort raus – so ein Chat wartet per Definition.

Solange Operatoren online sind, läuft der Chat wie gewohnt, und die E-Mail ist freiwillig: Besucher können trotzdem auf „Antworten per E-Mail erhalten“ klicken – praktisch für alle, die den Tab gleich schließen wollen.

Antworten per E-Mail

Hat der Besucher eine E-Mail hinterlassen und die Antwort des Operators nicht innerhalb von etwa 2 Minuten im Widget gelesen, bekommt er sie per E-Mail. Mehrere Antworten hintereinander kommen als eine E-Mail an, nicht als Flut von Benachrichtigungen. Die E-Mail enthält nur die Antworten des Operators – nicht das ganze Gespräch – und einen Link zurück auf die Seite deiner Website, von der aus der Besucher geschrieben hat, damit er direkt dort weitermachen kann.

Sprache des Widgets

Das Widget spricht die Sprache deiner Seite: Es liest <html lang> und nimmt die ersten zwei Buchstaben, de-AT und de bedeuten also beide Deutsch.

  • Eingebaute Oberflächentexte gibt es für ru, en, uk, es, de und zh. Jede andere Sprache bekommt Englisch.
  • Deine eigenen Texte aus dem CRM folgen den Fallback-Regeln oben.
  • Single-Page-App mit Sprachumschalter? Aktualisier einfach document.documentElement.lang — das Widget bemerkt die Änderung und zeichnet seine Texte im Flug neu, ohne Reload.
Sprachwechsel in einer SPAjs
function setLanguage(lang) {
  // ...eigene Übersetzungen umschalten...
  document.documentElement.lang = lang; // der Chat zieht mit
}

Angemeldete Nutzer erkennen

Standardmäßig sieht ein Operator „Besucher“. Hat deine Site Konten, geht das besser: Leg den Namen des Nutzers und deine interne ID in localStorage ab, und der Operator sieht, mit wem er spricht.

support_client_namestring, ≤ 80 charsoptional
Wird dem Operator statt „Besucher“ angezeigt. Im CRM lassen sich Chats danach durchsuchen.
support_client_idstring, ≤ 64 charsoptional
Deine interne Nutzer-ID (z. B. user_8421). Erscheint in der Besucherkarte, damit Operatoren das Konto in deinem eigenen Admin-Panel finden.

Das Widget liest diese Keys nur. Es sendet sie beim Verbinden und erneut vor jeder Nachricht, falls sie sich geändert haben — es ist also egal, ob du sie vor dem Laden der Seite setzt oder direkt nach dem Login. Beim Logout entfernst du beide Keys: Sonst erbt die nächste Person am selben Rechner den Namen.

// 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 {
    // Speicher blockiert (strenge Privatsphäre-Einstellungen) — der Chat funktioniert trotzdem, nur anonym
  }
}

// Nach erfolgreichem Login
setSupportIdentity({ id: 'user_8421', name: 'Anna Schmidt' });

// Beim Logout
setSupportIdentity(null);

Anhänge und Limits

Besucher können ihren Nachrichten Dateien anhängen — ein Screenshot vom Fehler sagt mehr als tausend Worte.

WasLimit
Bilderjpeg, png, gif, webp, avif
Dokumentepdf, doc, docx, xls, xlsx, txt, csv
Dateigrößebis 10 MB
Nachrichten20 pro Minute pro Chat
Verbindungenbis zu 12 gleichzeitig von einer IP
Neue Chats10 pro IP pro Stunde

Die Limits sind großzügig für Menschen und lästig für Bots, und genau darum geht es. Ein Büro voller Leute hinter einer IP, das die Site in einem Dutzend Tabs offen hat, ist der eine Fall, in dem du das Verbindungslimit bemerken könntest.

Den Chat über deinen eigenen Button öffnen

Der runde Button in der Ecke passt für die meisten Sites, aber manchmal gehört der Chat in dein eigenes Design: ein „Chatte mit uns“-Link im Header, ein Button auf der Preisseite, ein Hilfe-Icon in der App. Dafür gibt es zwei Wege — einer davon kommt ganz ohne JavaScript aus.

Ohne JavaScript: data-crm-chat

Setz data-crm-chat auf ein beliebiges Element, und ein Klick öffnet den Chat:

Beliebige Seitehtml
<a href="/contacts" data-crm-chat="open">Chatte mit uns</a>

<button type="button" data-crm-chat="toggle">💬 Support</button>
  • open (auch der Standard bei leerem Wert), close oder toggle.
  • Später hinzugefügte Elemente — Modals, clientseitige Routen — funktionieren ebenfalls: Das Widget lauscht auf dem ganzen Dokument.
  • Eingebauter Fallback. Ist der Chat auf der Site nicht verfügbar (oder widget.js blockiert), wird der Klick nicht abgefangen, und der Link führt einfach zu seinem href. Lass ihn auf deine Kontaktseite zeigen, dann landet niemand bei einem toten Button.

Aus JavaScript: window.crmChat

crmChat.open()functionoptional
Öffnet das Chat-Panel.
crmChat.close()functionoptional
Schließt es.
crmChat.toggle()functionoptional
Öffnet es, wenn es zu ist, und schließt es, wenn es offen ist.
crmChat.isOpen()() => booleanoptional
Ob das Panel gerade offen ist.
crmChat.availableboolean | nulloptional
true — der Chat funktioniert auf dieser Site; false — tut er nicht (Tarif, Einstellungen); null — wissen wir noch nicht, das Widget fragt gerade nach.
crmChat.unreadnumberoptional
Antworten des Operators, die der Besucher noch nicht gesehen hat. Solange das Panel offen ist, immer 0.

widget.js legt window.crmChat sofort an, noch bevor der Chat selbst geladen ist. Aufrufe aus dieser ersten Sekunde werden gemerkt und ausgeführt, sobald der Chat bereit ist — ein Klick auf deinen Button geht also nie verloren. Im Live-Modus des CRM lädt der Chat gar nicht, und crmChat tut einfach still nichts — dein Code bricht also auch dort nicht.

document.querySelector('#help').addEventListener('click', () => {
  // ?. — falls ein Adblocker widget.js am Laden gehindert hat
  window.crmChat?.open();
});

Den eingebauten Button ausblenden

Du hast deinen eigenen Button? Blende den runden Button des Widgets mit data-chat-button="hidden" am selben Script-Tag aus. Das Panel öffnet und schließt sich weiterhin wie gewohnt — es hat sein eigenes × im Header.

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

Ungelesene Antworten an deinem Button anzeigen

Ohne den runden Button braucht der Besucher einen anderen Weg, um zu merken, dass der Operator geantwortet hat. Das Widget feuert bei jeder Änderung ein crm:chat-Event auf window, mit { available, open, unread } in detail. Nutz es für ein Badge — und um deinen Button auszublenden, wenn der Chat nicht verfügbar ist.

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;
}

// Verwendung
export function ChatButton() {
  const { available, unread } = useCrmChat();
  if (available === false) return null;

  return (
    <button type="button" onClick={() => window.crmChat?.open()}>
      Support {unread > 0 && <span className="badge">{unread}</span>}
    </button>
  );
}

Content Security Policy für den Chat

Keine CSP auf deiner Site? Überspring diesen Abschnitt. Hast du eine, braucht der Chat diese Quellen:

Zu deiner bestehenden Content-Security-Policy hinzufügentext
script-src  https://widget.sitecog.com
connect-src https://back.sitecog.com wss://back.sitecog.com
style-src   'unsafe-inline'
img-src     <der Host, von dem deine CRM-Dateien ausgeliefert werden>
  • script-src — um widget.js und das Chat-Bundle zu laden.
  • connect-src — für den Einstellungs-Request und den WebSocket. Achte auf den wss://-Eintrag: https:// allein deckt WebSockets nicht ab.
  • style-src — das Chatfenster lebt in einem Shadow DOM mit einem Inline-<style>, eine strenge Policy braucht also eventuell 'unsafe-inline' (z. B. style-src 'self' 'unsafe-inline').
  • img-src — Anhänge und ein eigenes Symbol kommen aus dem Dateispeicher deiner Site. Öffne ein beliebiges Bild aus dem CRM und erlaube dessen Host.

Für Fortgeschrittene: eigene Chat-UI bauen

Ein eigener Client sind drei Schritte, plus ein vierter, wenn niemand online ist:

  1. Einstellungen lesen

    Ist der Chat für diese Site an? Was steht in den Texten? Ist jemand online?
  2. Einen Chat starten

    Hol dir per HTTP einen Chat-Token und bewahr ihn auf.
  3. E-Mail hinterlassen – im Offline-Modus

    Gerade kann niemand antworten? Speicher die E-Mail des Besuchers vor seiner ersten Nachricht.
  4. Über den WebSocket reden

    Mit dem Token verbinden, Frames senden und empfangen.

Alle Support-Endpoints sind öffentlich: Es gibt keinen API-Key. Die Site wird am Origin-Header des Browsers erkannt (mit Referer als Fallback), ruf sie also von Seiten auf deiner echten Domain auf. Eine Domain, die keine Site im CRM ist, bekommt 400 mit {"message":"unknown_domain"}; ein Request ganz ohne Origin bekommt {"message":"unknown_origin"}.

1. Chat-Einstellungen lesen

GET https://back.sitecog.com/support/config?lang=en
langquerystringoptional
Sprache der Texte, z. B. en. Es gelten dieselben Fallback-Regeln.
{
  "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
Ob der Chat für diese Site verfügbar ist. false — nichts anzeigen und hier aufhören.
lookobject
Was der Operator unter CRM → Support → Einstellungen gewählt hat. Nur vorhanden, wenn enabled true ist.
look →
iconstring
bubble, headset, question, envelope oder spark.
iconUrlstring | null
URL eines eigenen Symbolbilds; null, wenn ein eingebautes Symbol verwendet wird.
positionstring
bottom-right, bottom-left, top-right oder top-left.
skinstring
indigo, emerald, midnight oder graphite. In deiner eigenen UI auf deine Farben mappen — oder ignorieren.
textsobject
Texte für die angefragte Sprache. null heißt „nicht ausgefüllt, nimm deinen eigenen Standard“.
texts →
titlestring | null
Überschrift des Chatfensters, ≤ 80 Zeichen.
subtitlestring | null
Zeile unter der Überschrift, ≤ 160 Zeichen.
placeholderstring | null
Platzhalter im Eingabefeld, ≤ 80 Zeichen.
greetingstring | null
Begrüßung für einen leeren Chat, ≤ 200 Zeichen.
offlineobject
Ob gerade jemand antworten kann – siehe Wenn niemand online ist. Nur vorhanden, wenn enabled true ist. Anwesenheit ändert sich minütlich, also frag beim Öffnen deines Chatfensters neu, statt die Antwort lange zu cachen.
offline →
formboolean
Der Offline-Modus ist im CRM eingeschaltet. false – nie nach der E-Mail fragen, einfach chatten.
awayboolean
true – gerade kann niemand antworten: Zeig das E-Mail-Formular und frag vor der ersten Nachricht nach der Adresse (Schritt 3). Immer false, wenn form false ist.
reasonstring | null
Warum der Chat offline ist: after_hours – außerhalb der Arbeitszeit, no_agents – kein Operator hat das CRM offen. null, wenn away false ist.

2. Einen Chat starten oder fortsetzen

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

{
  "visitorId": "5f1c2a9e-3b7d-4c1e-9a0f-8d2b6e4c7a10",
  "lang": "en",
  "token": "<the token you got last time, if any>"
}
visitorIdstringPflicht
Eine stabile ID dieses Browsers: 6–128 Zeichen aus A–Z a–z 0–9 _ . : -. Läuft widget.js auf der Seite, nimm dessen crm_vid aus localStorage; sonst erzeug einmal deine eigene (eine UUID passt) und bewahr sie auf. Ist der Consent-Modus an und hat der Besucher noch nicht zugestimmt, gibt es kein crm_vid — dann erzeug eine temporäre ID nur im Arbeitsspeicher, statt eine zu speichern.
langstringoptional
Sprache des Besuchers, z. B. en.
tokenstringoptional
Der Chat-Token aus einem früheren Start. Gültiger Token — du bekommst denselben Chat samt Verlauf zurück. Kein oder ein ungültiger Token — ein neuer Chat wird angelegt (und neue Chats sind auf 10 pro IP pro Stunde begrenzt, also verlier den Token nicht).
Responsejson
{
  "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 — der Chat ist für diese Site aus; sonst steht nichts in der Response.
tokenstring (JWT)
Der Chat-Token, 180 Tage gültig. Speicher ihn (z. B. in localStorage) und überschreib ihn immer mit dem neuesten. Er öffnet den WebSocket und setzt diesen Chat beim nächsten Mal fort.
chatIdnumber
ID des Chats.
lastSeqnumber
Laufende Nummer der neuesten Nachricht. Nachrichten werden innerhalb eines Chats mit 1, 2, 3… nummeriert; seq ist dein Cursor für alles Weitere.
readSeqnumber
Bis wohin der Besucher gelesen hat.
statusstring
open oder closed, wie von den Operatoren im CRM gesetzt.
messagesMessage[]
Die letzten 50 Nachrichten, älteste zuerst. Die Form von Message ist dieselbe wie in WebSocket-Frames.
emailstring | null
Die E-Mail, die der Besucher in diesem Chat schon hinterlassen hat. Frag nicht noch einmal – zeig „Wir antworten an …“ mit der Möglichkeit, sie zu ändern.
offlineobject
Dieselbe Form wie offline in den Einstellungen: { form, away, reason }, Stand zum Zeitpunkt der Anfrage.
errorstring | null
Steht statt eines Tokens da, wenn der Chat nicht gestartet werden konnte: invalid_visitor (fehlerhafte visitorId) oder rate_limit_exceeded (zu viele neue Chats von dieser IP).

3. E-Mail hinterlassen

Ist offline.away true, speicher die E-Mail des Besuchers, bevor du seine erste Nachricht sendest – so macht es das eingebaute Widget, und nur so erreicht die Antwort jemanden, der den Tab längst geschlossen hat. Solange Operatoren online sind, steckt derselbe Aufruf hinter einem optionalen Button „Antworten per E-Mail erhalten“.

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

{
  "token": "<Chat-Token aus chat/start>",
  "email": "anna@example.com",
  "page": "https://shop.example/delivery?utm_source=newsletter"
}
tokenstringPflicht
Der Chat-Token aus chat/start. Eine E-Mail lässt sich nur an den eigenen Chat hängen.
emailstringPflicht
Wohin die Antworten gehen. Ein erneuter Aufruf mit einer anderen Adresse ersetzt die alte.
pagestringoptional
URL der Seite, von der aus der Besucher schreibt. Daraus wird der Link „auf der Website weitermachen“ in der E-Mail. Akzeptiert werden nur Seiten deiner eigenen Site; Query-String und #Fragment fallen weg – das Beispiel oben wird als https://shop.example/delivery gespeichert.
{ "ok": true, "email": "anna@example.com" }
errorWas passiert ist
invalid_emailDas sieht nicht nach einer E-Mail-Adresse aus. Bitte den Besucher, sie zu prüfen.
invalid_tokenDer Token fehlt, ist abgelaufen oder gehört zu einer anderen Site. Ruf chat/start erneut auf.
rate_limit_exceededTeilt sich das Limit von 20 pro Minute mit den Nachrichten des Chats. Kurz warten und erneut versuchen.
disabledDer Chat wurde für die Site ausgeschaltet.
chat_not_foundDer Chat wurde gelöscht. Verwirf den Token und starte einen neuen.
Offline-Modus in einem eigenen Clientjs
// chat – die Response von chat/start (oder ein frischerer ready-Frame)
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); // Nachricht behalten, Adresse korrigieren lassen
    chat.email = res.email;
  }
  send(text); // der WebSocket-Frame send aus Schritt 4
}

4. Mit dem WebSocket verbinden

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

Der Token kommt in die Subprotokoll-Liste, nicht in die URL — URLs landen in Logs, Subprotokolle nicht. Gib zwei Subprotokolle mit: die Protokollversion und token. + deinen Chat-Token. Den Origin schickt der Browser von selbst, und er muss deine Site sein.

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

socket.onopen = () => {
  // Hallo sagen und dem Server die letzte Nachricht nennen, die du schon hast
  socket.send(JSON.stringify({ t: 'hello', since: lastSeq }));
};

Nach hello antwortet der Server mit einem ready-Frame, der alles enthält, was du seit since verpasst hast. Jeder Frame ist ein JSON-Objekt mit dem Typ in t, höchstens 8 KB groß. Der Server pingt alle 25 Sekunden; Browser beantworten Pings automatisch, du musst nichts tun.

Frames, die du sendest

hello{ t, since }
Beginn der Session. since — die letzte seq, die du hast (0, wenn keine). Wird mit ready beantwortet.
send{ t, id, text, file? }
Eine Nachricht senden. id — deine eigene Nachrichten-ID, [A-Za-z0-9_-], 1–64 Zeichen; dieselbe id erneut zu senden erzeugt nie ein Duplikat, also wiederhol ruhig. text — bis 4000 Zeichen. file — eine Quittung aus einem Upload (siehe Anhänge).
read{ t, seq }
„Ich habe alles bis seq gelesen“. Steuert die Lesebestätigungen, die der Operator sieht.
typing{ t }
Der Besucher tippt. Sende das höchstens alle ~2 Sekunden, solange getippt wird.
history{ t, before }
Ältere Nachrichten laden: 50 pro Seite vor seq = before (0 = ab der neuesten). Wird mit history beantwortet.
profile{ t, name?, contact?, clientId? }
Dem Operator sagen, wer das ist: name ≤ 80, contact (Telefon oder E-Mail) ≤ 120, clientId ≤ 64. Mindestens eins davon darf nicht leer sein.
Beispielejson
{ "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" }

Frames, die du empfängst

readyframe
Antwort auf hello: der Zustand des Chats plus das, was du verpasst hast.
ready →
chatIdnumber
ID des Chats.
visitorOnlineboolean
Ob der Besucher in diesem Chat online ist.
lastSeqnumber
Nummer der neuesten Nachricht.
statusstring
open oder closed.
read{ agent, visitor }
Bis wohin jede Seite gelesen hat (seq).
unreadnumber
Nachrichten, die der Besucher noch nicht gelesen hat — praktisch für ein Badge.
messagesMessage[]
Nachrichten nach since.
gapboolean
true, wenn du mehr verpasst hast, als in ein Nachholen passt — lade den Rest mit history.
emailstring | null
Die E-Mail, die der Besucher in diesem Chat hinterlassen hat – wie in chat/start.
offline{ form, away, reason }
Ob gerade jemand antworten kann – dieselbe Form wie in den Einstellungen. Kommt bei jedem Reconnect mit, so merkt auch eine stundenlang offene Seite, wenn die Operatoren gehen oder zurückkommen.
messageframe
Eine neue Nachricht im Chat — eine Operator-Antwort, eine automatische Antwort und so weiter.
message →
chatIdnumber
ID des Chats.
mMessage
Die Nachricht selbst.
m →
seqnumber
Laufende Nummer im Chat. Nutz sie zum Sortieren und Deduplizieren.
authorstring
visitor, agent oder system.
textstring
Reiner Text. Render ihn als Text (textContent), niemals als HTML.
atstring (ISO 8601)
Wann die Nachricht gespeichert wurde.
file{ url, name, mime, size, kind } | null
Nur bei Anhängen vorhanden. kind ist image oder doc.
ack{ id, seq, at }
Dein send mit dieser id wurde als Nachricht seq gespeichert. Mach aus „wird gesendet…“ ein „gesendet“.
read{ chatId, by, seq }
Jemand (by) hat bis seq gelesen — z. B. hat der Operator deine Nachrichten gelesen.
typing{ chatId, by }
Die andere Seite tippt. Zeig ein paar Sekunden lang die Pünktchen.
history{ chatId, before, messages, done }
Antwort auf deine history-Anfrage. done: true — es gibt nichts Älteres.
presenceframe
Jemand im Chat ist online gekommen oder offline gegangen.
chat_goneframe
Der Chat wurde im CRM gelöscht. Verwirf den gespeicherten Token und ruf chat/start erneut auf, um einen frischen Chat zu bekommen.
error{ code, id? }
Mit einem Frame ist etwas schiefgegangen; id ist gesetzt, wenn es um eine deiner Nachrichten geht. Die Codes sind unten aufgelistet.
Eine Nachricht vom Operatorjson
{
  "t": "message",
  "chatId": 123,
  "m": { "seq": 6, "author": "agent", "text": "Your parcel left the warehouse today 🚚", "at": "2026-10-01T09:20:11.000Z" }
}

Ein kompletter minimaler Client

Starten → verbinden → senden → empfangen, mit Reconnects. Etwa 90 Zeilen, keine Abhängigkeiten — steck dein eigenes render und markSent ein, und du hast einen funktionierenden Chat.

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 {} },
};

// Die Besucher-ID des Widgets wiederverwenden, falls da, sonst eine eigene behalten
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; // dieselbe Nachricht kann aus start, ready und message kommen
  seen.add(m.seq);
  lastSeq = Math.max(lastSeq, m.seq);
  render(m); // deine UI: m.author, m.text (als 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;            // Chat ist für diese Site aus
  if (data.error) throw new Error(data.error);

  store.set('my_chat_token', data.token); // 180 Tage gültig
  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();                           // ein brandneuer Chat
    } else if (frame.t === 'error') console.warn('[chat]', frame.code, frame.id);
  };

  socket.onclose = (event) => {
    if (event.code === 4402 || event.code === 4403) return; // abgeschaltet / verboten: aufhören
    failures += 1;
    const delay = Math.min(30000, 1000 * 2 ** failures) + Math.random() * 1000;
    // Mehrere Fehlschläge hintereinander können heißen, dass der Token nicht mehr gilt — neu anfangen
    setTimeout(() => (failures > 3 ? boot() : connect(token)), delay);
  };
}

function send(text) {
  const id = crypto.randomUUID();       // gefahrlos erneut sendbar: gleiche id = gleiche Nachricht
  socket.send(JSON.stringify({ t: 'send', id, text }));
  return id;                            // als „wird gesendet…“ anzeigen, bis das ack mit dieser id kommt
}

boot();

Reconnects und verpasste Nachrichten

  • Merk dir die höchste seq, die du angezeigt hast. Nach einem Reconnect sende { "t": "hello", "since": lastSeq } — der ready-Frame bringt genau das, was du verpasst hast.
  • Ist ready.gap true, hast du viel verpasst; füll die Lücke mit history-Anfragen.
  • Nachrichten können doppelt ankommen (zum Beispiel aus chat/start und aus ready). Dedupliziere nach seq.
  • Warte zwischen den Versuchen immer länger (1 s, 2 s, 4 s… mit etwas Zufall) — ein Server-Neustart soll nicht in einer Reconnect-Stampede enden.
  • Unsicher, ob eine Nachricht den Server erreicht hat? Schick sie noch mal mit derselben id. Sie wird einmal gespeichert.

Anhänge in einer eigenen UI

Eine Datei braucht drei kurze Hops: Erlaubnis holen, Datei hochladen, Quittung senden.

// 1. Eine Upload-Erlaubnis — 5 Minuten gültig
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. Datei hochladen (Bilder oder Dokumente, bis 10 MB)
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. Eine Nachricht mit der Quittung senden (text darf leer sein)
socket.send(JSON.stringify({ t: 'send', id: crypto.randomUUID(), text: '', file: upload.receipt }));

Fehler und Close-Codes

Fehler auf einer laufenden Verbindung kommen als { "t": "error", "code": "…", "id": "…" }:

CodeWas passiert ist
rate_limit_exceededMehr als 20 Nachrichten pro Minute in diesem Chat. Warte und sende erneut mit derselben id.
invalid_textDer Text fehlt oder ist länger als 4000 Zeichen.
invalid_fileDie Upload-Quittung ließ sich nicht verifizieren — abgelaufen oder aus einem anderen Chat. Lade erneut hoch.
invalid_message_idDie id besteht nicht aus 1–64 Zeichen aus A–Z a–z 0–9 _ -.
frame_too_largeDer Frame ist größer als 8 KB.
invalid_jsonDer Frame ist kein gültiges JSON.
unknown_frameUnbekanntes t. Tippfehler oder ein Frame aus einem neueren Protokoll.
empty_profileEin profile-Frame, in dem alle Felder leer sind.
chat_not_foundDen Chat gibt es nicht mehr. Starte einen neuen.
disabledDer Chat wurde für die Site abgeschaltet.

Die Verbindung selbst kann abgelehnt oder geschlossen werden:

WannCodeBedeutung
Handshake401 unauthorizedDer Token fehlt, ist ungültig oder abgelaufen. Ruf chat/start erneut auf.
Handshake429 too_many_connectionsMehr als 12 Verbindungen von dieser IP. Schließ die überzähligen.
Close4402Der Chat wurde für die Site abgeschaltet. Nicht neu verbinden.
Close4403Verboten. Nicht neu verbinden.

Fehlersuche

Der Chat-Button erscheint nicht

  • Der Tarif enthält den Chat nicht, oder der Chat ist abgeschaltet. Der Einstellungs-Endpoint antwortet mit { "enabled": false }.
  • Unbekannte Domain. Die Seite ist auf einer Domain geöffnet, die keine Site im CRM ist — ein Staging-Host, localhost, eine neue Domain, die du noch nicht eingetragen hast. (www. spielt keine Rolle, es wird entfernt.)
  • Die CSP blockiert es. Such in der Browser-Konsole nach „Refused to load“ oder „Refused to connect“ und prüf den CSP-Abschnitt.
  • Du schaust dir die Site im CRM an. Im Live-Modus lädt das Widget nur den Editor — kein Chat, keine Analytics. Öffne die Site in einem normalen Tab. Den Editor gibt es dabei nur im Live-Frame des CRM; bettet jemand anderes deine Site in ein fremdes iframe ein, lädt das Widget dort keinen Editor.
  • widget.js fehlt auf dieser Seite — leicht zu übersehen, wenn eine Site mehrere Layouts hat.
  • Der Chat wurde gerade erst aktiviert, und dein Browser erinnert sich noch bis zu ~10 Minuten an die alten Einstellungen (siehe unten).

Du kannst den Server direkt fragen, was er von deiner Domain hält:

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

Änderungen aus dem CRM sind nicht zu sehen

  • Die Einstellungen werden etwa 10 Minuten im Browser gecacht. Entweder warten oder den Key crm_chat_cfg unter DevTools → Application → Local Storage löschen und neu laden.
  • Du hast die Texte für eine Sprache bearbeitet, aber die Seite hat ein anderes <html lang>. Prüf die Fallback-Regeln.
  • Ein Textfeld ist leer, also zeigt das Widget seinen eingebauten Text — so ist es gewollt.