Widget de chat en JavaScript

Inserta el chat en vivo de Deskwoot en cualquier sitio web con una sola etiqueta <script>. Sin paso de compilación, y los visitantes no necesitan iniciar sesión en ningún panel.

En esta página

Instalación

Pega este fragmento de código en la plantilla de tu página, idealmente justo antes de la etiqueta de cierre </body>. Carga el widget de forma asíncrona, así que no bloquea la visualización de la página.

<script>
  window.deskwootSettings = {
    widgetToken: "YOUR_WIDGET_TOKEN"
  };
  (function (d, t) {
    var g = d.createElement(t);
    var s = d.getElementsByTagName(t)[0];
    g.src = "https://deskwoot.com/widget/deskwoot-loader.js";
    g.async = true;
    g.defer = true;
    s.parentNode.insertBefore(g, s);
  })(document, "script");
</script>

Eso es todo. El botón del chat aparece en tu página al instante. Sin paquete npm, sin bundler, sin paso de compilación: funciona en Next.js, React, Vue, temas de Shopify, WordPress, HTML simple y en cualquier lugar donde se ejecute JavaScript.

Encuentra el token de tu widget

  1. Inicia sesión en el panel de Deskwoot con una cuenta de administrador. Tus visitantes nunca necesitan iniciar sesión.
  2. Ve a Configuración → Bandejas de entrada.
  3. Crea una bandeja de entrada nueva del tipo Chat en vivo o abre la que ya tienes.
  4. En la pestaña Widget, la tarjeta Instalación del widget muestra el fragmento de código con tu propio widgetToken. Cópialo tal cual.
Un widgetToken = una bandeja de entrada. Las conversaciones de ese widget siempre llegan a esa bandeja de entrada, donde tus agentes las atienden. Para dirigirlas a otro equipo o a otro grupo de agentes, crea una segunda bandeja de entrada y usa su token en otra página.

Configuración

Todas las opciones van en window.deskwootSettings. Solo widgetToken es obligatorio.

OpciónTipoPredeterminadoDescripción
widgetTokenstringningunoObligatorio. Identificador de la bandeja de entrada que aparece en el panel.
userobjectningunoIdentidad firmada de un visitante con sesión iniciada. Consulta Verificación de identidad.
embeddedbooleanfalseLlena un contenedor o toda la pantalla en lugar de flotar en una esquina. Consulta Modo integrado.
containerstring | ElementningunoSolo con embedded: el selector CSS o el elemento que se va a llenar. Sin esta opción, el widget ocupa toda la ventana visible.
openAnchorstring | Element | { x, y }ningunoElemento desde el que crece el panel en computadoras: un selector CSS, un elemento o un punto { x, y }.
openAlignstring"auto"Qué esquina del panel queda sobre el ancla: auto, bottom-left, bottom-right, top-left o top-right.
El color, la posición del botón del chat, el tema claro u oscuro y los textos de bienvenida no se definen en el código. Los cambias en cada bandeja de entrada en Configuración → Bandejas de entrada → tu bandeja de entrada → Widget → Apariencia del widget, y el widget los carga cada vez que se inicia.
Ejemplo completo
window.deskwootSettings = {
  widgetToken: "YOUR_WIDGET_TOKEN",

  // Optional: a verified, logged in visitor (see Identity verification)
  user: {
    identifier: "user_4711",
    identifierHash: "<HMAC-SHA256 of identifier, from your server>",
    email: "jane@example.com",
    name: "Jane Doe"
  },

  // Optional: open the panel out of your own button on desktop
  openAnchor: "#support-button",
  openAlign: "auto"
};

Verificación de identidad

Cuando los visitantes tienen la sesión iniciada en tu sitio, dile al widget quiénes son. Así sus conversaciones llegan con nombre, correo e historial, y no como un visitante anónimo.

El widget solo acepta una identidad que haya firmado tu servidor. Firmas el id del usuario con un secreto que pertenece a la bandeja de entrada. Los visitantes verificados chatean con su nombre y correo reales y se saltan el formulario previo al chat y el paso del correo. El secreto se queda en tu servidor, así que Deskwoot confirma al visitante a partir de la firma sin ningún acceso a tus sistemas.

Encuentra tu secreto de firma en la configuración de la bandeja de entrada de tu sitio web, en la pestaña Formulario previo al chat, en la tarjeta Verificación de identidad.

Calcula el hash en tu servidor (Node.js)
import crypto from "crypto";

const identifierHash = crypto
  .createHmac("sha256", process.env.DESKWOOT_IDENTITY_SECRET)
  .update(userId)          // your stable user id, as a string
  .digest("hex");
Visitante verificado
window.deskwootSettings = {
  widgetToken: "YOUR_WIDGET_TOKEN",
  user: {
    identifier: "user_4711",            // your stable user id
    identifierHash: "<HMAC-SHA256 of identifier, computed on your server>",
    email: "jane@example.com",          // optional
    name: "Jane Doe",                   // optional
    attributes: { plan: "Pro" },        // optional, shown on the contact
    attributesIssuedAt: 1760781600,     // optional: unix seconds you signed them at
    attributesHash: "<HMAC-SHA256 of the signed attributes, from your server>"  // optional
  }
};

identifier es el id estable de tu usuario y el único campo firmado. email, name y attributes (un mapa simple de textos, como un plan o un id de empresa, que se muestra en el contacto) son datos opcionales. Un visitante verificado llega siempre al mismo contacto, con sus conversaciones anteriores, notas y atributos a la vista. El bot de IA responde a este visitante en el chat en vivo solo con atributos firmados (ver abajo), y solo cuando puede consultar contactos en “Lo que Fynn puede hacer”.

Si tu app inicia la sesión del usuario después de que la página cargó, llama a window.deskwoot.identify(user) en cuanto tengas el usuario firmado. Actualiza al visitante actual sin recargar. Si el usuario ya tiene la sesión iniciada cuando carga la página, configura deskwootSettings.user antes del script del widget: así el chat sigue abierto aunque se recargue la página. Con identify() después de la carga, cada recarga empieza con un chat vacío que vuelve en cuanto el usuario queda verificado.

El hash debe venir de tu servidor. Nunca lo calcules en el navegador y nunca envíes el secreto al cliente. Cualquiera que tenga el secreto puede hacerse pasar por tus usuarios.

Atributos firmados

identifierHash solo firma el id del usuario, así que un visitante puede cambiar attributes en el navegador. Para que el bot de IA responda a partir de ellos, fírmalos en tu servidor y envía attributesHash y attributesIssuedAt junto con ellos. Los atributos sin firmar, y los atributos escritos a través de la API o por tu equipo, se siguen viendo en el contacto, pero el bot nunca los lee. El bot puede decirle al visitante lo que dice un atributo firmado, así que firma solo lo que el visitante puede ver.

attributesHash es el HMAC-SHA256 en hexadecimal en minúsculas, con el mismo secreto de la bandeja de entrada, de cuatro líneas unidas por un salto de línea (\n): el texto deskwoot-attributes-v1, el id del usuario, attributesIssuedAt y el JSON canónico de los atributos. JSON canónico: claves ordenadas por punto de código Unicode, sin espacios, caracteres fuera de ASCII escritos tal cual (sin escapar) y solo cadenas, números, true, false y null como valores (sin objetos ni listas anidados). Eso es exactamente lo que escribe json.dumps(attributes, sort_keys=True, separators=(",", ":"), ensure_ascii=False) de Python.

Envía los números decimales y los enteros muy grandes (mayores que 9007199254740991) como cadenas. Python escribe 1.0 donde JavaScript escribe 1, así que esos números no darían el mismo hash en ambos lados.

attributesIssuedAt es la hora unix en segundos enteros en que firmaste el conjunto. Deskwoot usa un conjunto de hasta 24 horas de antigüedad y hasta 5 minutos adelantado respecto a su reloj, y nunca uno más antiguo que el último conjunto que usó para ese usuario, así que vuelve a firmar en cada carga de página. Fuera de eso, el visitante sigue verificado, pero los atributos de esa solicitud se ignoran (nada cambia en el contacto) y la respuesta de verificación indica attributesIgnored con out_of_range o stale. Un hash incorrecto se rechaza igual que un identifierHash incorrecto, y el visitante chatea entonces de forma anónima. Lo mismo pasa con un conjunto firmado que supera los límites: como máximo 30 claves, claves de hasta 64 caracteres, valores de hasta 1024 caracteres y ninguna clave que Deskwoot se reserve, como source.

Cada conjunto firmado reemplaza lo que el bot puede leer de ese usuario: una clave que dejas fuera se queda en el contacto, pero el bot ya no la lee, y una clave que firmas con el valor null se elimina del contacto.

Firma los atributos en tu servidor (Node.js)
import crypto from "crypto";

// Keys sorted by Unicode code point, no spaces, non ASCII characters raw.
// Values: strings, integers, true, false or null.
function canonicalJson(attributes) {
  const keys = Object.keys(attributes).sort((a, b) => {
    const x = Array.from(a), y = Array.from(b);
    for (let i = 0; i < Math.min(x.length, y.length); i++) {
      const d = x[i].codePointAt(0) - y[i].codePointAt(0);
      if (d !== 0) return d;
    }
    return x.length - y.length;
  });
  return "{" + keys.map((k) => JSON.stringify(k) + ":" + JSON.stringify(attributes[k])).join(",") + "}";
}

const attributesIssuedAt = Math.floor(Date.now() / 1000);
const message = "deskwoot-attributes-v1\n" + userId + "\n" + attributesIssuedAt + "\n" + canonicalJson(attributes);
const attributesHash = crypto
  .createHmac("sha256", process.env.DESKWOOT_IDENTITY_SECRET)
  .update(message)
  .digest("hex");
Lo mismo en Python
import hashlib, hmac, json, os, time

attributes_issued_at = int(time.time())
canonical = json.dumps(attributes, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
message = "deskwoot-attributes-v1\n" + user_id + "\n" + str(attributes_issued_at) + "\n" + canonical
attributes_hash = hmac.new(
    os.environ["DESKWOOT_IDENTITY_SECRET"].encode(),
    message.encode("utf-8"),
    hashlib.sha256,
).hexdigest()
Vector de prueba: tu código debe generar exactamente este JSON canónico y este hash
secret              test_identity_secret
identifier          user_4711
attributesIssuedAt  1760781600
attributes          { "plan": "Pro", "status": "verified", "trades": 12, "vip": true,
                      "referrer": null, "city": "Zürich", "Zone": "EU" }

canonical JSON      {"Zone":"EU","city":"Zürich","plan":"Pro","referrer":null,"status":"verified","trades":12,"vip":true}

signed message (4 lines, joined by \n, no line break at the end):
deskwoot-attributes-v1
user_4711
1760781600
{"Zone":"EU","city":"Zürich","plan":"Pro","referrer":null,"status":"verified","trades":12,"vip":true}

attributesHash      bd363335977152a3b73cfb6c042c9afc823f18eab73faff36acddfffd0b1e1f5

API de JavaScript

En cuanto se ejecuta el cargador, window.deskwoot (también disponible como window.deskwootWidget) te permite controlar el widget desde tu propia interfaz, por ejemplo con un enlace Contáctanos en tu navegación que abre el chat en lugar de una página de soporte.

MétodoQué hace
open(options?)Abre el panel del chat. Con las opciones anchor, align y offset, que son opcionales, el panel crece desde tu propio elemento en computadoras.
close()Cierra el panel.
toggle(options?)Abre el panel, o lo cierra si ya está abierto. Acepta las mismas opciones que open().
identify(user)Inicia la sesión de un usuario verificado después de que la página se cargó. Consulta Verificación de identidad.
window.__deskwootUnreadCantidad de mensajes de agentes sin leer para este visitante. Se actualiza mientras el panel está cerrado.

Las llamadas que se hacen mientras el widget todavía se está iniciando esperan en una cola y se ejecutan cuando está listo, así que no hace falta esperar ni consultar una y otra vez.

Abrir el chat desde tu propio código
// window.deskwoot (alias window.deskwootWidget) exists as soon as the
// loader script has run. Calls made while the widget is still starting
// are queued, so there is no need to wait or poll. Only code that runs
// before the loader itself needs the guard below.
function openDeskwoot() {
  if (window.deskwoot) {
    window.deskwoot.open();
  }
}

openDeskwoot();                 // open the chat panel
// window.deskwoot.close();     // close it again
// window.deskwoot.toggle();    // open or close

// Unread agent messages for this visitor:
console.log(window.__deskwootUnread);

Modo integrado

Configura embedded: true cuando el widget vive dentro de un contenedor que debe ocupar toda la pantalla, normalmente una WebView de una app móvil, una pantalla de contacto a página completa o un modal que trae su propio marco.

En modo integrado, el botón del chat, la ventana emergente del disparador y el botón de cerrar están ocultos, y el panel llena position:fixed; inset:0 sin sombra, sin esquinas redondeadas y sin el margen de 20px. Apunta container a un elemento para llenar solo ese elemento. La app móvil de Deskwoot usa este modo en su pantalla Contactar a soporte.

<div id="support-pane" style="height: 600px"></div>

<script>
  window.deskwootSettings = {
    widgetToken: "YOUR_WIDGET_TOKEN",
    embedded: true,
    container: "#support-pane"   // leave out to fill the whole viewport
  };
</script>

Se abre automáticamente y llena su contenedor.

En modo integrado, el panel se abre al cargar y llena su contenedor. El botón del chat y el botón de cerrar están ocultos, así que tu app o página se encarga de la navegación. La API de JavaScript sigue disponible para abrirlo, cerrarlo o alternarlo:

Disparador mínimo
<button onclick="window.deskwoot.open()">Contact Support</button>

Ubicaciones recomendadas

Tres ubicaciones que funcionan bien para el soporte dentro de una app:

Elemento en la barra lateral o en la navegación inferior (como lo hace Deskwoot)

Coloca un elemento Contactar a soporte en la navegación que ya tienes, junto a Configuración o Ayuda. Se ve como cualquier otro enlace, pero abre el chat en lugar de una página nueva.

<a href="#" onclick="window.deskwoot.open(); return false;" class="nav-item">
  <svg class="icon"><!-- chat bubble icon --></svg>
  Contact Support
</a>

Elemento en el menú de ayuda o en un menú desplegable

Si ya tienes un menú de Ayuda o de Cuenta, agrega una fila Chatear con soporte arriba. Queda bien junto a elementos como Documentación, Atajos de teclado o Comentarios.

<div role="menuitem" onclick="window.deskwoot.open()">
  Chat with support
  <span class="kbd">?</span>
</div>

Botón flotante en la esquina

Un pequeño botón flotante que reemplaza al botón del chat oculto. Útil cuando tu app no tiene una barra de navegación siempre visible.

<button
  id="support-fab"
  onclick="window.deskwoot.open({ anchor: this })"
  style="position:fixed;bottom:20px;right:20px;border:0;border-radius:9999px;padding:12px 18px;cursor:pointer;font-weight:600"
>
  Help
</button>

<!--
  open() and toggle() take an optional anchor, so the panel grows out of
  your own element instead of the default corner:

    open({ anchor: this })                  // grow from this button
    open({ anchor: "#support-fab" })        // or from a CSS selector
    open({ anchor: { x: 120, y: 640 } })    // or from a viewport point

  anchor   CSS selector, DOM element or { x, y } (null = default corner)
  align    "auto" (default), "bottom-left", "bottom-right",
           "top-left" or "top-right": the corner it grows from
  offset   { x, y } in px, to fine tune the position

  window.deskwootSettings.openAnchor and .openAlign make this the
  default for every open(); open({ anchor: null }) opens one time in
  the default corner. Desktop only; phones always get full screen.
-->

Consejos

  • Llama a deskwoot.toggle() en lugar de open() si quieres que el mismo disparador cierre el panel cuando ya está abierto.
  • Muestra un contador de no leídos en tu disparador con window.__deskwootUnread, un número que se actualiza mientras el panel está cerrado.
  • Pon el disparador en el mismo lugar en todas las páginas que requieren inicio de sesión, para que los usuarios siempre sepan dónde está el soporte.

Para la instalación normal en un sitio web no necesitas un disparador propio: deskwoot-loader.js dibuja el botón del chat y se encarga del clic.

Content Security Policy

Si tu sitio envía un encabezado Content Security Policy, permite el origen de Deskwoot. Una política mínima típica:

script-src  'unsafe-inline' https://deskwoot.com;
connect-src https://deskwoot.com;
frame-src   https://deskwoot.com;
style-src   'unsafe-inline';

El fragmento de instalación es un script en línea, así que script-src necesita 'unsafe-inline', un nonce o un hash. style-src 'unsafe-inline' es necesario porque el widget agrega una pequeña hoja de estilos a tu página, y frame-src porque el chat se ejecuta en frames que sirve deskwoot.com. ¿Tienes preguntas sobre tu política? Escribe a hello@deskwoot.com.

Preguntas frecuentes

Aquí tienes las respuestas cortas. Para todo lo demás, pregúntale a Fynn abajo.

¿Mis visitantes necesitan una cuenta de Deskwoot para chatear?

No. Solo tú y tu equipo inician sesión en el panel para responder. Los visitantes solo escriben en el widget.

¿El widget funciona en SPA que cambian de ruta sin recargar?

Sí. Carga el script una vez cuando se inicia tu app (por ejemplo, en _app.tsx o root.tsx) y se mantiene durante la navegación. No hace falta volver a ejecutar nada cuando cambia la ruta.

Demo interactiva de Deskwoot