Diil Docs
  1. Documentación
  2. El widget

Chat de soporte: chat en vivo en tu web con una etiqueta

Actualizado:

Un visitante tiene una duda a las once de la noche y tu formulario de contacto le parece un mensaje en una botella. El chat de soporte lo arregla: un botón de chat en cada página de tu web, y la conversación llega al CRM, donde tu equipo responde en tiempo real. Si ya añadiste widget.js para la edición en vivo, el chat está literalmente a cero líneas de código.

Cómo funciona el chat de soporte

  1. Un visitante hace clic en el botón de chat de la esquina de tu web y escribe un mensaje (con una captura de pantalla, si las palabras no le alcanzan).
  2. El mensaje aparece al instante en CRM → Soporte → Chats. Todo va por WebSocket, así que nadie tiene que darle a F5.
  3. Un operador responde desde el CRM y el visitante ve la respuesta al momento, con indicador de “escribiendo” y marcas de leído incluidos.

Para que nadie se pierda un chat mientras se prepara un café, los operadores tienen un contador de no leídos en el CRM, notificaciones del navegador y, si quieres, un bot de Telegram con horas de silencio. Todo eso se configura en el CRM, no en tu código.

Si activaste el modo de consentimiento en la etiqueta del widget, el chat no se queda esperando al banner de cookies: el visitante puede escribir desde el primer segundo. Lo que cambia antes del consentimiento:

  • Identidad de usar y tirar. El chat usa un id temporal que vive solo en memoria: no se guarda nada en el navegador. Una conversación anterior se sigue recuperando con su propio crm_chat_token.
  • Sin atribución. Sin crm_vid ni crm_sid, el chat no queda vinculado al canal, las etiquetas UTM ni el enlace publicitario por el que llegó el visitante.

Dicho de otro modo: el soporte nunca depende de que alguien haya pulsado “Aceptar”; lo único que espera al consentimiento es la analítica.

Añade un chat en vivo a tu web

No hay código específico para el chat. La misma etiqueta que mueve todo lo demás hace el trabajo:

Antes de </body>, en cada páginahtml
<script src="https://widget.sitecog.com/widget.js" defer></script>

Cuando el chat está disponible para tu web, widget.js carga por su cuenta el bundle del chat (widget.support.js) y aparece un botón de chat flotante. Cuando no lo está, los visitantes no se descargan nada extra. Poner la etiqueta dos veces no hace daño: la segunda copia se ignora.

  1. Comprueba que tu plan incluye el chat de soporte

    El chat está disponible en los planes que lo incluyen. Sin chat en el plan, no hay botón, diga lo que diga el código.
  2. Comprueba que tu dominio es un sitio del CRM

    El chat reconoce tu web por el dominio en el que se abre la página (el Origin del navegador; el www. se ignora). El dominio tiene que ser un sitio que hayas añadido en el CRM.
  3. Añade widget.js a la página

    La etiqueta de arriba, o las variantes de Next.js / Vite de Widget y marcado → Paso 1.
  4. Dale estilo en el CRM

    Icono, colores, posición y textos viven en CRM → Soporte → Ajustes: lo vemos más abajo.

Aspecto y textos en el CRM

Todo lo visual se configura en CRM → Soporte → Ajustes. Sin sobrescribir CSS y sin redesplegar: lo cambias ahí y el widget lo recoge.

OpciónValores permitidosNotas
Iconobubble, headset, question, envelope, spark o una imagen propiaUn icono propio se elige del almacenamiento de archivos de tu sitio.
Posiciónbottom-right por defecto, bottom-left, top-right, top-leftElige la esquina en la que no esté sentado tu banner de cookies.
Esquema de colorindigo por defecto, emerald, midnight, graphitePaletas listas para el botón y la ventana del chat.
Títulohasta 80 caracteresLa cabecera de la ventana del chat.
Línea bajo el títulohasta 160 caracteresLa línea de debajo del título: buen sitio para un “solemos responder en 10 minutos”.
Texto de ejemplo en el campo de entradahasta 80 caracteresLa pista gris dentro del campo del mensaje.
Saludo en un chat vacíohasta 200 caracteresSe muestra en un chat vacío, antes de que el visitante haya escrito nada.

Textos por idioma e idioma de respaldo

Cada texto se puede rellenar para cada idioma de tu sitio. El widget elige el texto para el idioma del visitante así:

  1. una coincidencia exacta de idioma;
  2. si no, una coincidencia por las dos primeras letras;
  3. si no, la primera entrada que hayas rellenado.

Si un texto está vacío, el widget usa su propio texto integrado. Así que puedes dejarlo todo en blanco y tener igualmente un chat de lo más decente: los ajustes son para cuando quieres que suene a ti.

Respuesta automática

Activa la respuesta automática y escribe un texto por idioma (hasta 1000 caracteres). Responde al primer mensaje de una conversación nueva, para que el visitante sepa que se le ha escuchado aunque todo el equipo esté en una reunión. Una conversación cuenta como nueva tras N minutos de silencio: tú eliges N entre 1 y 180; por defecto son 5.

Cuando no hay nadie conectado

Un chat en vivo sin nadie al otro lado es una trampa: el visitante escribe su pregunta, espera, cierra la pestaña… y nunca sabrás quién era. El modo sin conexión convierte el chat en un formulario de “déjanos tu correo” justo en esos momentos, para que la pregunta no desaparezca junto con el visitante.

Se activa en CRM → Soporte → Ajustes → “Cuando no hay nadie conectado”. El chat pasa al modo sin conexión cuando:

  • ningún operador tiene el CRM abierto para este sitio. Cuenta cualquier página del CRM, no solo los chats: el operador está “conectado” hasta que cierra la última pestaña del CRM;
  • está fuera del horario laboral, si lo configuras: días laborables, desde / hasta y una zona horaria. El horario es opcional; sin él solo se aplica la primera regla. Fuera de horario el chat está sin conexión aunque alguien tenga el CRM abierto por casualidad.

Qué cambia en el modo sin conexión:

  1. El widget muestra “Ahora no estamos conectados: deja tu correo y te responderemos” y pide el correo antes del mensaje. Sin correo no hay mensaje: si no, la respuesta no tendría adónde ir.
  2. En el CRM el chat lleva la marca “Solicitud sin conexión”, con el correo del visitante justo al lado.
  3. La notificación de Telegram (si conectaste el bot) sale de inmediato: un chat así está esperando por definición.

Mientras haya operadores conectados, el chat funciona como siempre y el correo es opcional: el visitante puede pulsar igualmente “Recibir respuestas por correo”, algo muy útil para quien está a punto de cerrar la pestaña.

Respuestas por correo

Si el visitante dejó su correo y no ha leído la respuesta del operador en el widget en unos 2 minutos, se la enviamos por correo. Varias respuestas seguidas llegan en un solo correo, no como una ráfaga de avisos. El correo contiene solo las respuestas del operador, no toda la conversación, y un enlace a la página de tu web desde la que escribió el visitante, para que pueda seguir allí mismo.

Idioma del widget

El widget habla el idioma de tu página: lee <html lang> y se queda con las dos primeras letras, así que en-GB y en significan inglés por igual.

  • Hay textos de interfaz integrados para ru, en, uk, es, de y zh. Cualquier otro idioma recibe inglés.
  • Tus propios textos del CRM siguen las reglas de respaldo de arriba.
  • ¿Una SPA con selector de idioma? Basta con actualizar document.documentElement.lang: el widget detecta el cambio y redibuja sus textos sobre la marcha, sin recargar.
Cambio de idioma en una SPAjs
function setLanguage(lang) {
  // ...cambia tus propias traducciones...
  document.documentElement.lang = lang; // el chat te sigue
}

Identifica a los usuarios con sesión iniciada

Por defecto, un operador ve “Visitante”. Si tu web tiene cuentas, puedes hacerlo mejor: pon el nombre del usuario y tu id interno en localStorage y el operador verá con quién está hablando.

support_client_namestring, ≤ 80 charsopcional
Se muestra al operador en lugar de “Visitante”. En el CRM se pueden buscar chats por este valor.
support_client_idstring, ≤ 64 charsopcional
Tu id interno de usuario (por ejemplo, user_8421). Se muestra en la ficha del visitante, para que los operadores encuentren la cuenta en tu propio panel de administración.

El widget solo lee estas claves. Las envía al conectarse y otra vez antes de cada mensaje si han cambiado, así que da igual si las pones antes de que cargue la página o justo después de que el usuario inicie sesión. Al cerrar sesión, borra las dos claves: si no, la siguiente persona en el mismo ordenador hereda el nombre.

// 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 {
    // El almacenamiento está bloqueado (ajustes de privacidad estrictos): el chat sigue funcionando, solo que de forma anónima
  }
}

// Tras iniciar sesión correctamente
setSupportIdentity({ id: 'user_8421', name: 'Anna Schmidt' });

// Al cerrar sesión
setSupportIdentity(null);

Adjuntos y límites

Los visitantes pueden adjuntar archivos a sus mensajes: una captura del error vale más que mil palabras.

QuéLímite
Imágenesjpeg, png, gif, webp, avif
Documentospdf, doc, docx, xls, xlsx, txt, csv
Tamaño de archivohasta 10 MB
Mensajes20 por minuto por chat
Conexioneshasta 12 a la vez desde una IP
Chats nuevos10 por IP por hora

Los límites son generosos para las personas y fastidiosos para los bots, que es justo la idea. Una oficina llena de gente detrás de una sola IP con la web abierta en una docena de pestañas es el único caso en el que podrías notar el límite de conexiones.

Abrir el chat desde tu propio botón

El botón redondo de la esquina le va bien a la mayoría de las webs, pero a veces el chat pide encajar en tu propio diseño: un enlace “Habla con nosotros” en la cabecera, un botón en la página de precios, un icono de ayuda en la app. Hay dos formas de hacerlo, y una no necesita ni una línea de JavaScript.

Sin JavaScript: data-crm-chat

Pon data-crm-chat en cualquier elemento y un clic abrirá el chat:

Cualquier páginahtml
<a href="/contacts" data-crm-chat="open">Habla con nosotros</a>

<button type="button" data-crm-chat="toggle">💬 Soporte</button>
  • open (también el valor por defecto si lo dejas vacío), close o toggle.
  • Los elementos que se añaden más tarde (modales, rutas del lado del cliente) también funcionan: el widget escucha todo el documento.
  • Plan B incorporado. Si el chat no está disponible en la web (o widget.js está bloqueado), el clic no se intercepta y el enlace simplemente va a su href. Apúntalo a tu página de contacto y nadie se topará con un botón muerto.

Desde JavaScript: window.crmChat

crmChat.open()functionopcional
Abre el panel del chat.
crmChat.close()functionopcional
Lo cierra.
crmChat.toggle()functionopcional
Lo abre si está cerrado y lo cierra si está abierto.
crmChat.isOpen()() => booleanopcional
Si el panel está abierto ahora mismo.
crmChat.availableboolean | nullopcional
true: el chat funciona en esta web; false: no funciona (plan, ajustes); null: aún no lo sabemos, el widget lo está preguntando.
crmChat.unreadnumberopcional
Respuestas del operador que el visitante aún no ha visto. Siempre 0 mientras el panel está abierto.

widget.js crea window.crmChat al instante, antes de que se cargue el propio chat. Las llamadas que se hagan en ese primer segundo se guardan y se ejecutan en cuanto el chat está listo, así que un clic en tu botón nunca se pierde. En el modo Live del CRM el chat no se carga en absoluto y crmChat simplemente no hace nada: tu código tampoco se romperá ahí.

document.querySelector('#help').addEventListener('click', () => {
  // ?. por si un bloqueador de anuncios no dejó cargar widget.js
  window.crmChat?.open();
});

Oculta el botón integrado

¿Tienes tu propio botón? Oculta el botón redondo del widget con data-chat-button="hidden" en la misma etiqueta script. El panel se sigue abriendo y cerrando como siempre: tiene su propia × en la cabecera.

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

Muestra las respuestas sin leer en tu botón

Sin el botón redondo, el visitante necesita otra forma de enterarse de que el operador ha contestado. El widget lanza un evento crm:chat en window cada vez que algo cambia, con { available, open, unread } en detail. Úsalo para un contador y para ocultar tu botón cuando el chat no esté disponible.

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

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

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

Content Security Policy para el chat

¿Tu web no tiene CSP? Sáltate esta sección. Si la tiene, el chat necesita estas fuentes:

Añádelo a tu Content-Security-Policy actualtext
script-src  https://widget.sitecog.com
connect-src https://back.sitecog.com wss://back.sitecog.com
style-src   'unsafe-inline'
img-src     <el host desde el que se sirven los archivos de tu CRM>
  • script-src: para cargar widget.js y el bundle del chat.
  • connect-src: para la petición de ajustes y el WebSocket. Fíjate en la entrada wss://: https:// por sí solo no cubre los WebSockets.
  • style-src: la ventana del chat vive en un shadow DOM con un <style> inline, así que una política estricta puede necesitar 'unsafe-inline' (por ejemplo, style-src 'self' 'unsafe-inline').
  • img-src: los adjuntos y el icono propio se sirven desde el almacenamiento de archivos de tu sitio. Abre cualquier imagen del CRM y permite su host.

Avanzado: monta tu propia interfaz de chat

Un cliente propio son tres pasos, y uno más cuando no hay nadie conectado:

  1. Lee los ajustes

    ¿Está activado el chat para este sitio? ¿Qué dicen los textos? ¿Hay alguien conectado?
  2. Inicia un chat

    Consigue un token de chat por HTTP y guárdalo.
  3. Deja un correo, en modo sin conexión

    ¿Ahora no puede responder nadie? Guarda el correo del visitante antes de su primer mensaje.
  4. Habla por el WebSocket

    Conéctate con el token, envía y recibe frames.

Todos los endpoints de soporte son públicos: no hay clave de API. El sitio se reconoce por la cabecera Origin del navegador (con Referer como alternativa), así que llámalos desde páginas de tu dominio real. Un dominio que no es un sitio del CRM recibe 400 con {"message":"unknown_domain"}; una petición sin ningún origen recibe {"message":"unknown_origin"}.

1. Lee los ajustes del chat

GET https://back.sitecog.com/support/config?lang=en
langquerystringopcional
Idioma de los textos, por ejemplo en. Se aplican las mismas reglas de respaldo.
{
  "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
Si el chat está disponible para este sitio. false: no muestres nada y para aquí.
lookobject
Lo que el operador eligió en CRM → Soporte → Ajustes. Solo aparece cuando enabled es true.
look →
iconstring
bubble, headset, question, envelope o spark.
iconUrlstring | null
URL de la imagen de un icono propio; null cuando se usa un icono integrado.
positionstring
bottom-right, bottom-left, top-right o top-left.
skinstring
indigo, emerald, midnight o graphite. En tu propia interfaz, tradúcelo a tus colores, o ignóralo.
textsobject
Textos para el idioma pedido. null significa “sin rellenar, usa tu propio texto por defecto”.
texts →
titlestring | null
Título de la ventana del chat, ≤ 80 caracteres.
subtitlestring | null
Línea bajo el título, ≤ 160 caracteres.
placeholderstring | null
Texto de ejemplo del campo de entrada, ≤ 80 caracteres.
greetingstring | null
Saludo para un chat vacío, ≤ 200 caracteres.
offlineobject
Si alguien puede responder ahora mismo; consulta Cuando no hay nadie conectado. Solo aparece cuando enabled es true. La presencia cambia de un minuto a otro, así que vuelve a preguntar al abrir la ventana del chat en lugar de guardarlo en caché mucho tiempo.
offline →
formboolean
El modo sin conexión está activado en el CRM. false: nunca pidas el correo, simplemente chatea.
awayboolean
true: ahora no puede responder nadie; muestra el formulario y pide el correo antes del primer mensaje (paso 3). Siempre false cuando form es false.
reasonstring | null
Por qué el chat está sin conexión: after_hours, fuera del horario laboral; no_agents, ningún operador tiene el CRM abierto. null cuando away es false.

2. Inicia o reanuda un chat

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

{
  "visitorId": "5f1c2a9e-3b7d-4c1e-9a0f-8d2b6e4c7a10",
  "lang": "en",
  "token": "<el token que recibiste la última vez, si lo tienes>"
}
visitorIdstringobligatorio
Un id estable de este navegador: de 6 a 128 caracteres de A–Z a–z 0–9 _ . : -. Si widget.js está en la página, reutiliza su crm_vid de localStorage; si no, genera el tuyo una vez (un UUID sirve) y guárdalo. Con el modo de consentimiento activo y sin consentimiento todavía, no hay crm_vid: genera un id temporal en memoria y no lo guardes.
langstringopcional
Idioma del visitante, por ejemplo en.
tokenstringopcional
El token de chat de un inicio anterior. Con un token válido recuperas el mismo chat con su historial. Sin token o con uno no válido se crea un chat nuevo (y los chats nuevos están limitados a 10 por IP por hora, así que no pierdas el token).
Respuestajson
{
  "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: el chat está desactivado para este sitio; la respuesta no trae nada más.
tokenstring (JWT)
El token del chat, válido durante 180 días. Guárdalo (por ejemplo, en localStorage) y sobrescríbelo siempre con el más reciente. Abre el WebSocket y reanuda este chat la próxima vez.
chatIdnumber
Id del chat.
lastSeqnumber
Número de secuencia del último mensaje. Los mensajes se numeran 1, 2, 3… dentro de un chat; seq es tu cursor para todo lo que viene a continuación.
readSeqnumber
Hasta dónde ha leído el visitante.
statusstring
open o closed, según lo hayan dejado los operadores en el CRM.
messagesMessage[]
Los últimos 50 mensajes, del más antiguo al más reciente. La forma de Message es la misma que en los frames del WebSocket.
emailstring | null
El correo que el visitante ya dejó en este chat. No lo pidas otra vez: muestra “Te responderemos a …” con la opción de cambiarlo.
offlineobject
La misma forma que offline en los ajustes: { form, away, reason }, en el momento de la petición.
errorstring | null
Aparece en lugar del token cuando no se pudo iniciar el chat: invalid_visitor (visitorId incorrecto) o rate_limit_exceeded (demasiados chats nuevos desde esta IP).

3. Deja un correo

Cuando offline.away es true, guarda el correo del visitante antes de enviar su primer mensaje: es lo que hace el widget integrado, y la única forma de que la respuesta llegue a alguien que ya cerró la pestaña. Mientras haya operadores conectados, la misma llamada sirve para un botón opcional de “Recibir respuestas por correo”.

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

{
  "token": "<token del chat de chat/start>",
  "email": "anna@example.com",
  "page": "https://shop.example/delivery?utm_source=newsletter"
}
tokenstringobligatorio
El token del chat de chat/start. Solo puedes asociar un correo a tu propio chat.
emailstringobligatorio
Adónde enviar las respuestas. Si vuelves a llamar con otra dirección, sustituye a la anterior.
pagestringopcional
URL de la página desde la que escribe el visitante. Se convierte en el enlace “seguir en la web” del correo. Solo se aceptan páginas de tu propio sitio, y se descartan la query string y el #fragmento: el ejemplo de arriba se guarda como https://shop.example/delivery.
{ "ok": true, "email": "anna@example.com" }
errorQué ha pasado
invalid_emailEso no parece una dirección de correo. Pide al visitante que la revise.
invalid_tokenEl token falta, ha caducado o es de otro sitio. Vuelve a llamar a chat/start.
rate_limit_exceededComparte el límite de 20 por minuto con los mensajes del chat. Espera un poco y reintenta.
disabledEl chat se ha desactivado para el sitio.
chat_not_foundEl chat se eliminó. Olvida el token e inicia uno nuevo.
Modo sin conexión en un cliente propiojs
// chat: la respuesta de chat/start (o un frame ready más reciente)
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); // no perdemos el mensaje, que corrija la dirección
    chat.email = res.email;
  }
  send(text); // el frame send del WebSocket, paso 4
}

4. Conéctate al WebSocket

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

El token va en la lista de subprotocolos, no en la URL: las URL acaban en los logs y los subprotocolos no. Pasa dos subprotocolos: la versión del protocolo y token. + tu token de chat. El navegador envía Origin por su cuenta, y tiene que ser tu sitio.

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

socket.onopen = () => {
  // Saluda y dile al servidor cuál es el último mensaje que ya tienes
  socket.send(JSON.stringify({ t: 'hello', since: lastSeq }));
};

Tras el hello, el servidor responde con un frame ready que contiene todo lo que te perdiste desde since. Cada frame es un objeto JSON con su tipo en t, de 8 KB como máximo. El servidor manda un ping cada 25 segundos; los navegadores responden a los pings solos, no tienes que hacer nada.

Frames que envías

hello{ t, since }
Inicio de la sesión. since: el último seq que tienes (0 si no tienes ninguno). Se responde con ready.
send{ t, id, text, file? }
Enviar un mensaje. id: tu propio id de mensaje, [A-Za-z0-9_-], de 1 a 64 caracteres; reenviar el mismo id nunca crea un duplicado, así que reintenta sin miedo. text: hasta 4000 caracteres. file: el recibo de una subida (mira adjuntos).
read{ t, seq }
“He leído todo hasta seq”. Alimenta las marcas de leído que ve el operador.
typing{ t }
El visitante está escribiendo. Envíalo como mucho una vez cada ~2 segundos mientras escribe.
history{ t, before }
Cargar mensajes anteriores: 50 por página antes del seq = before (0 = desde el más reciente). Se responde con history.
profile{ t, name?, contact?, clientId? }
Dile al operador quién es: name ≤ 80, contact (teléfono o email) ≤ 120, clientId ≤ 64. Al menos uno no debe estar vacío.
Ejemplosjson
{ "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 que recibes

readyframe
Respuesta a hello: el estado del chat más lo que te perdiste.
ready →
chatIdnumber
Id del chat.
visitorOnlineboolean
Si el visitante está conectado en este chat.
lastSeqnumber
Número del último mensaje.
statusstring
open o closed.
read{ agent, visitor }
Hasta dónde ha leído cada parte (seq).
unreadnumber
Mensajes que el visitante aún no ha leído: muy útil para un badge.
messagesMessage[]
Mensajes posteriores a since.
gapboolean
true si te perdiste más de lo que cabe en una sola puesta al día: carga el resto con history.
emailstring | null
El correo que el visitante dejó en este chat, como en chat/start.
offline{ form, away, reason }
Si alguien puede responder ahora mismo; la misma forma que en los ajustes. Llega en cada reconexión, así que una página abierta durante horas se entera de que los operadores se han ido o han vuelto.
messageframe
Un mensaje nuevo en el chat: una respuesta del operador, una respuesta automática, etc.
message →
chatIdnumber
Id del chat.
mMessage
El mensaje en sí.
m →
seqnumber
Número de secuencia dentro del chat. Úsalo para ordenar y para eliminar duplicados.
authorstring
visitor, agent o system.
textstring
Texto plano. Renderízalo como texto (textContent), nunca como HTML.
atstring (ISO 8601)
Cuándo se guardó el mensaje.
file{ url, name, mime, size, kind } | null
Solo aparece en los adjuntos. kind es image o doc.
ack{ id, seq, at }
Tu send con este id se ha guardado como el mensaje seq. Cambia el estado “enviando…” a “enviado”.
read{ chatId, by, seq }
Alguien (by) ha leído hasta seq: por ejemplo, el operador ha leído tus mensajes.
typing{ chatId, by }
La otra parte está escribiendo. Muestra los puntitos durante unos segundos.
history{ chatId, before, messages, done }
Respuesta a tu petición history. done: true: no hay nada más antiguo.
presenceframe
Alguien del chat se ha conectado o desconectado.
chat_goneframe
El chat se ha eliminado en el CRM. Descarta el token guardado y vuelve a llamar a chat/start para tener un chat nuevo.
error{ code, id? }
Algo ha ido mal con un frame; id viene relleno cuando se refiere a uno de tus mensajes. Los códigos están más abajo.
Un mensaje del operadorjson
{
  "t": "message",
  "chatId": 123,
  "m": { "seq": 6, "author": "agent", "text": "Your parcel left the warehouse today 🚚", "at": "2026-10-01T09:20:11.000Z" }
}

Un cliente mínimo completo

Iniciar → conectar → enviar → recibir, con reconexiones. Unas 90 líneas, sin dependencias: enchufa tus propios render y markSent y tendrás un chat funcionando.

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

// Reutiliza el id de visitante del widget si está; si no, guarda el nuestro
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; // el mismo mensaje puede llegar de start, ready y message
  seen.add(m.seq);
  lastSeq = Math.max(lastSeq, m.seq);
  render(m); // tu interfaz: m.author, m.text (¡como texto!), 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;            // el chat está desactivado para este sitio
  if (data.error) throw new Error(data.error);

  store.set('my_chat_token', data.token); // válido 180 días
  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();                           // un chat completamente nuevo
    } else if (frame.t === 'error') console.warn('[chat]', frame.code, frame.id);
  };

  socket.onclose = (event) => {
    if (event.code === 4402 || event.code === 4403) return; // desactivado / prohibido: paramos
    failures += 1;
    const delay = Math.min(30000, 1000 * 2 ** failures) + Math.random() * 1000;
    // Varios fallos seguidos pueden significar que el token ya no vale: empezamos de nuevo
    setTimeout(() => (failures > 3 ? boot() : connect(token)), delay);
  };
}

function send(text) {
  const id = crypto.randomUUID();       // se puede reenviar: mismo id = mismo mensaje
  socket.send(JSON.stringify({ t: 'send', id, text }));
  return id;                            // muéstralo como "enviando…" hasta el ack con este id
}

boot();

Reconexiones y mensajes perdidos

  • Guarda el seq más alto que hayas mostrado. Tras reconectar, envía { "t": "hello", "since": lastSeq }: el frame ready trae exactamente lo que te perdiste.
  • Si ready.gap es true, te has perdido mucho; rellena el hueco con peticiones history.
  • Los mensajes pueden llegar dos veces (de chat/start y de ready, por ejemplo). Elimina duplicados por seq.
  • Espera cada vez más entre intentos (1 s, 2 s, 4 s… con algo de aleatoriedad): un reinicio del servidor no debería convertirse en una estampida de reconexiones.
  • ¿No sabes si un mensaje llegó al servidor? Envíalo de nuevo con el mismo id. Se guarda una sola vez.

Adjuntos en una interfaz propia

Un archivo va en tres saltos cortos: consigue un permiso, sube el archivo y envía el recibo.

// 1. Un permiso para subir: válido durante 5 minutos
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. Sube el archivo (imágenes o documentos, hasta 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. Envía un mensaje con el recibo (el texto puede ir vacío)
socket.send(JSON.stringify({ t: 'send', id: crypto.randomUUID(), text: '', file: upload.receipt }));

Errores y códigos de cierre

Los errores en una conexión activa llegan como { "t": "error", "code": "…", "id": "…" }:

CódigoQué ha pasado
rate_limit_exceededMás de 20 mensajes por minuto en este chat. Espera y reenvía con el mismo id.
invalid_textFalta el texto o tiene más de 4000 caracteres.
invalid_fileNo se pudo verificar el recibo de la subida: ha caducado o es de otro chat. Vuelve a subir el archivo.
invalid_message_idEl id no tiene de 1 a 64 caracteres de A–Z a–z 0–9 _ -.
frame_too_largeEl frame pesa más de 8 KB.
invalid_jsonEl frame no es un JSON válido.
unknown_framet desconocido. Una errata, o un frame de una versión más nueva del protocolo.
empty_profileUn frame profile con todos los campos vacíos.
chat_not_foundEl chat ya no existe. Inicia uno nuevo.
disabledEl chat se ha desactivado para el sitio.

La propia conexión puede rechazarse o cerrarse:

CuándoCódigoSignificado
Handshake401 unauthorizedEl token falta, no es válido o ha caducado. Vuelve a llamar a chat/start.
Handshake429 too_many_connectionsMás de 12 conexiones desde esta IP. Cierra las que sobran.
Cierre4402El chat se ha desactivado para el sitio. No vuelvas a conectar.
Cierre4403Prohibido. No vuelvas a conectar.

Solución de problemas

No aparece el botón del chat

  • El plan no incluye el chat, o el chat está desactivado. El endpoint de ajustes responde { "enabled": false }.
  • Dominio desconocido. La página se abre en un dominio que no es un sitio del CRM: un host de staging, localhost, un dominio nuevo que aún no has añadido. (El www. da igual, se quita).
  • Lo bloquea la CSP. Busca “Refused to load” o “Refused to connect” en la consola del navegador y revisa la sección de CSP.
  • Estás mirando la web dentro del CRM. En el modo Live el widget solo carga el editor: ni chat ni analítica. Abre la web en una pestaña normal. (El editor, por cierto, solo se carga en el frame Live del propio CRM: si otra web mete la tuya en su iframe, allí no hay editor).
  • Falta widget.js en esta página: es fácil que se escape cuando una web tiene varios layouts.
  • Acabas de activar el chat y tu navegador aún recuerda los ajustes antiguos durante hasta ~10 minutos (mira más abajo).

Puedes preguntarle directamente al servidor qué opina de tu dominio:

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

No se ven los cambios hechos en el CRM

  • Los ajustes se guardan en caché en el navegador durante unos 10 minutos. Espera, o borra la clave crm_chat_cfg en DevTools → Application → Local Storage y recarga.
  • Has editado los textos de un idioma, pero la página tiene otro <html lang>. Revisa las reglas de respaldo.
  • Un campo de texto está vacío, así que el widget muestra su texto integrado: es así por diseño.