Vai al contenuto

Widget — Integración técnica

Guía para desarrolladores: script, payload de init(), asociación y creación de empresa, ciclo de vida, restauración autenticada (restore_id), CSP y verificación.

Páginas de recorrido: Visión general · B2C · B2B.

Edición de referencia: 26 de octubre de 2026. La restauración entre dispositivos se añade al payload de empresa. Antes de activarla, comprueba que provisionRestoreId esté disponible en tu instalación.

La interfaz va en un iframe. El sitio anfitrión no necesita Angular, un cliente de Supabase ni acceso a la base de Quoting24.


Elegir el tipo de integración

Caso Implementación Continuidad
Sitio público, visitante desconocido Solo script. Formulario de contacto Sesión de invitado en el mismo navegador
Sitio con datos del cliente Script e init() con perfil y empresa Sesión de invitado precargada y asociación de empresa
Portal con historial personal entre dispositivos También un backend para external_user_id y restore_id Historial personal en el canal widget autorizado

Asociar una empresa y recuperar el historial son operaciones distintas. external_id, company_name, el nombre, el correo y el teléfono no autorizan las conversaciones de otra persona. Un usuario autenticado en el sitio sigue siendo un invitado del widget hasta que proporciones una credencial de restauración válida.


1. Configuración desde Quoting24

En Configuración → Canales (Impostazioni → Canali), abre el canal widget y recupera:

  • URL completa del script y clave pública
  • dominios autorizados, incluidas pruebas y desarrollo local
  • logotipo, color, posición, saludo, bienvenida y horarios

Usa la URL y la clave del mismo entorno. En los ejemplos sustituye HOST_WIDGET y CLAVE_PUBLICA_WIDGET. La clave de la URL es pública y selecciona el canal: no es una credencial personal ni un secreto de integración de backend.

Los dominios son nombres de host (shop.example.com, localhost), sin https://, puerto, ruta ni prefijo *.. example.com autoriza también sus subdominios. Una lista vacía no aplica restricciones. Para la prueba local sirve la página por HTTP, no como archivo.

Valor Dónde conservarlo Finalidad
Clave pública URL del script Selecciona el canal widget
Secreto privado de la integración Solo secretos del backend Autoriza crear y reutilizar restore_id
restore_id Almacén privado por usuario; en la página solo con sesión autenticada Indica qué historial restaurar
Credencial de sesión de invitado localStorage del sitio, por clave de widget, gestionada por el loader Continuidad de invitado en el mismo navegador

La restauración exige integración de backend, endpoint privado y secreto, separados del código público. Ve la sección 6.


2. Instalación

Botón flotante

Una sola vez, antes de </body>:

<script
  src="https://HOST_WIDGET/assets/widget/script.js?key=CLAVE_PUBLICA_WIDGET"
  async
></script>

El botón aparece después de cargar los ajustes gráficos. Un visitante nuevo abre el formulario con teléfono y nombre opcional. El loader guarda la credencial de invitado en localStorage, separada por clave. Si el almacenamiento está bloqueado, la continuidad puede limitarse a la página actual.

El loader expone un único window.Quoting24Widget. Un segundo script no crea un segundo widget.

Chat incrustado

Contenedor antes del script, altura explícita:

<section aria-labelledby="titulo-asistencia">
  <h2 id="titulo-asistencia">Asistencia</h2>
  <div id="chat-asistencia" style="width: 100%; height: 600px;"></div>
</section>

<script
  src="https://HOST_WIDGET/assets/widget/script.js?key=CLAVE_PUBLICA_WIDGET&amp;mode=embedded&amp;target=chat-asistencia"
  async
></script>

target es el ID, sin #. El iframe llena el contenedor. No hay botón flotante. open(), close() y toggle() no tienen efecto.

Esperar al loader

Para datos precargados usa este módulo en lugar de la etiqueta estática, importado una sola vez en el layout principal. Los ejemplos son módulos JavaScript: bundler, o <script type="module" src="/assets/asistencia.js"></script>.

// /assets/quoting24-loader.js
const widgetScriptUrl =
  "https://HOST_WIDGET/assets/widget/script.js?key=CLAVE_PUBLICA_WIDGET";

let widgetPromise;

export function loadWidget() {
  if (window.Quoting24Widget) {
    return Promise.resolve(window.Quoting24Widget);
  }
  if (widgetPromise) return widgetPromise;

  widgetPromise = new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.id = "quoting24-widget-script";
    script.src = widgetScriptUrl;
    script.async = true;
    script.onload = () => {
      if (window.Quoting24Widget) resolve(window.Quoting24Widget);
      else {
        script.remove();
        reject(new Error("La API del widget no está disponible"));
      }
    };
    script.onerror = () => {
      script.remove();
      reject(new Error("No se pudo cargar el widget"));
    };
    document.body.appendChild(script);
  }).catch((error) => {
    widgetPromise = undefined;
    throw error;
  });

  return widgetPromise;
}
import { loadWidget } from "/assets/quoting24-loader.js";

const widget = await loadWidget();
widget.init({
  external_id: "52",
  company_name: "Ricambi Stella S.r.l.",
  name: "mario.rossi",
  phone: "+393331234567",
  email: "amministrazione@ricambi-stella.example",
});

Para el modo incrustado añade &mode=embedded&target=chat-asistencia a la URL y monta antes el contenedor. No dupliques esta carga con una etiqueta script estática.

loadWidget() indica que la API está disponible. init() devuelve void: encola el perfil si el iframe no está listo y no espera a la sesión. Para el resultado usa isReady() y hasSessionError().


3. Campos de init()

Todos los valores son cadenas. Omite los desconocidos. No envíes null, IDs numéricos ni un correo vacío. Si un campo supera los límites o el correo está mal formado, la petición se rechaza.

Campo Límite y comportamiento
external_id Máximo 100 caracteres. Referencia de empresa comparada exactamente con web_code, en la organización del widget. Los espacios no se quitan
company_name Máximo 250 caracteres tras quitar los espacios iniciales y finales. Crea una empresa que falta solo junto con external_id. Una cadena vacía o solo de espacios no habilita la creación
name Máximo 250 caracteres. Nombre del contacto; puede actualizarlo en la sesión actual
email Dirección válida. En la inicialización pública se guarda solo en una empresa nueva. No se escribe en el contacto ni en una empresa ya existente
phone Máximo 80 caracteres. Número del contacto; prefiere el formato internacional, por ejemplo +393331234567. Un conflicto con un número existente no une contactos ni chats
external_user_id Opcional en el caso base. ID estable de la persona, no vacío, máximo 100 caracteres tras el trim. Por sí solo no restaura el historial
restore_id Credencial privada, máximo 200 caracteres, obtenida del backend. Exige el external_user_id correspondiente. Usa el valor devuelto sin modificarlo

Ejemplo cuando el sitio conoce la cuenta. Los nombres de campo del ERP pueden ser distintos: asigna la referencia de empresa a external_id, la razón social a company_name, la persona a name y el correo de la cuenta a email.

widget.init({
  external_id: "52",
  company_name: "Ricambi Stella S.r.l.",
  name: "mario.rossi",
  phone: "+393331234567",
  email: "amministrazione@ricambi-stella.example",
});

Las etiquetas Nombre y Teléfono del formulario se envían como name y phone. El correo es email. No envíes Nombre, Teléfono ni Email.

widget.init({
  name: "Mario Rossi",
  phone: "+393331234567",
});
widget.init({
  email: "mario.rossi@ricambi-stella.example",
});

La primera llamada abre el chat con nombre y teléfono y no busca una empresa. La segunda basta con un correo válido. En el perfil completo de arriba, phone es el número del contacto y email se escribe en la empresa solo si esta llamada la crea.

Mantén separados name y company_name. No concatenes la razón social al nombre de usuario para obtener la asociación. No envíes un campo web_code: el nombre público es external_id. No uses el UUID interno de Quoting24 ni companies.code en lugar del valor de web_code.

init(profile) sustituye el perfil guardado por el loader. No hace merge: pasa los campos que quieras conservar.

Un invitado nuevo puede empezar con al menos un nombre, un teléfono, un correo válido o un external_user_id, o con una empresa encontrada o creada mediante external_id. init({}) y company_name solo no inician una sesión nueva. Si envías únicamente un external_id desconocido, sin company_name, el widget pide los datos mediante el formulario.

init({ restoreId: "..." }) no transmite la credencial. El campo es restore_id.


4. Asociar o crear la empresa

El flujo se dispara cuando Quoting24 procesa la inicialización, incluso sin un mensaje:

  1. Si recibe un external_id no vacío, busca una empresa de la misma organización cuyo web_code sea igual a ese valor
  2. Si la encuentra, vincula el contacto. El nombre y el correo de la empresa existente no cambian
  3. Si no la encuentra y también recibe un company_name no vacío, crea la empresa con name = company_name, web_code = external_id y el correo si existe, y luego vincula el contacto
  4. Si no puede encontrar ni crear la empresa por falta de datos, continúa con el perfil disponible. Si no basta para un invitado nuevo, pide el formulario. Los errores de base de datos o el límite de peticiones hacen fallar la inicialización
Datos enviados Situación en Quoting24 Resultado
external_id: "52", company_name: "Ricambi Stella S.r.l." Existe web_code = "52" Usa la empresa existente; no cambia su nombre
Los mismos datos, más un correo válido No existe web_code = "52" Crea la empresa con código web 52, razón social y correo, y asocia el contacto
external_id: "52", name: "mario.rossi" Ninguna coincidencia Inicia al invitado sin una asociación de empresa nueva
external_id: "52", sin otros datos Existe una coincidencia Inicia al invitado asociado, con el nombre predeterminado Visitatore
external_id: "52", sin otros datos Ninguna coincidencia Pide los datos del visitante
company_name sin external_id, más name Cualquiera Inicia al visitante. No busca ni crea una empresa

La búsqueda no usa razón social, correo, teléfono, NIF ni companies.code. "052" y "52" son valores distintos. Si importas antes el fichero, rellena web_code con el mismo valor que el sitio enviará en external_id.

La asociación afecta al contacto. Para un contacto ya vinculado, un código válido nuevo puede cambiar su empresa. Omitir el código, o enviar uno que no se resuelve, no borra la asociación anterior. Al cambiar de persona o de cuenta ejecuta reset(): no te limites a cambiar el perfil.

Límites

web_code no tiene una restricción de unicidad. Con varias coincidencias se selecciona la primera, ordenando por fecha de creación ascendente. Evita los duplicados: peticiones simultáneas que aún no encuentran la empresa pueden crear más de una. El flujo no garantiza una creación atómica y no sustituye una importación o una sincronización desde el ERP.

La creación implícita está limitada a 20 intentos por ventana de 60 segundos y dirección IP, en el proceso de backend que atiende la petición. Al superarlo, la inicialización falla con TOO_MANY_REQUESTS. Reintenta con espera. Evita bucles continuos de init().

Los datos llegan desde el navegador y el visitante puede modificarlos. La asociación no demuestra pertenencia ni identidad y no concede el historial de la empresa. Envía company_name si quieres permitir esta creación pública. Para actualizar empresas existentes usa Quoting24 o una integración de ERP autorizada.


5. Login, logout y cambio de cuenta

Mantén una única instancia en la estructura principal de una SPA. Un cambio de página no exige un script nuevo ni un reset.

Al cambiar de persona o de cuenta, y al cerrar sesión, llama a reset() antes del perfil nuevo. En una recarga con el mismo login aún activo, usa init() sin reset, como en el arranque: así conservas la sesión de invitado guardada. El sitio debe distinguir la recarga del cambio efectivo.

Llamada en los cambios de autenticación. profile es el perfil del usuario nuevo, o null al cerrar sesión.

import { loadWidget } from "/assets/quoting24-loader.js";

let identityRevision = 0;

export async function applyWidgetIdentity(profile) {
  const revision = ++identityRevision;
  const snapshot = profile ? { ...profile } : null;

  window.Quoting24Widget?.reset();
  if (!snapshot) return;

  const widget = await loadWidget();
  if (revision !== identityRevision) return;
  widget.reset();
  widget.init(snapshot);
}

Gestiona el error de carga en la interfaz del sitio, con un reintento. Si recuperas el perfil del backend antes de esta función, invalida también esas peticiones al cambiar de usuario. Durante la espera, borra de inmediato el perfil anterior con applyWidgetIdentity(null).

El reset olvida la sesión de invitado de esta instancia, la credencial guardada, el perfil y el contador de no leídos. No es una revocación en el servidor y no termina las sesiones en otros dispositivos. El cambio de autenticación debe aplicarse también en las demás pestañas abiertas.

Con restauración del historial

Cuando el perfil incluye restore_id, descárgalo del endpoint autenticado del sitio después del reset(), y descarta la respuesta si el usuario cambió entretanto. La recarga con el mismo usuario reutiliza la credencial ya guardada y no debe llamar a reset().

import { loadWidget } from "/assets/quoting24-loader.js";

let authRevision = 0;
let pendingRecovery;

// Solo en el login, el logout y el cambio de usuario.
// No llamarla en una recarga con el mismo usuario: reset() descartaría la sesión ya en curso.
export async function applyWidgetAuth(user) {
  const revision = ++authRevision;
  const expectedUserId = user ? String(user.id) : null;
  pendingRecovery?.abort();
  const controller = new AbortController();
  pendingRecovery = controller;

  window.Quoting24Widget?.reset();

  try {
    const widget = await loadWidget();
    if (revision !== authRevision) return;
    widget.reset();
    if (expectedUserId === null) return;

    const response = await fetch("/api/widget/session", {
      method: "POST",
      credentials: "same-origin",
      cache: "no-store",
      signal: controller.signal,
      headers: { "Content-Type": "application/json" },
      body: "{}",
    });
    if (!response.ok) throw new Error("La restauración del widget no está disponible");
    const profile = await response.json();
    if (revision !== authRevision) return;
    if (profile.external_user_id !== expectedUserId || !profile.restore_id) {
      throw new Error("Respuesta de restauración inesperada");
    }

    const payload = {
      external_user_id: profile.external_user_id,
      restore_id: profile.restore_id,
    };
    for (const key of ["external_id", "company_name", "name", "email", "phone"]) {
      if (profile[key]) payload[key] = profile[key];
    }
    widget.init(payload);
  } catch (error) {
    if (controller.signal.aborted || revision !== authRevision) return;
    console.warn("Chat de asistencia: la restauración falló.", error);
  }
}

El bucle copia solo los campos presentes y no vacíos. Un correo "" o un ID numérico no deben enviarse.

API JavaScript

Método Uso
init(profile) Aporta o sustituye el perfil actual. El loader no fusiona los campos con un perfil anterior
open() / close() / toggle() Abre, cierra o alterna el widget flotante. Cerrar no es un logout. En modo incrustado no tienen efecto
isReady() La inicialización de la sesión de visitante tuvo éxito. Sigue en falso durante el formulario de contacto
hasSessionError() Error de sesión señalado por el iframe
reset() Borra perfil, sesión local, credencial de invitado guardada y contador de no leídos
dispose() Desmonta la interfaz y los listeners. No borra la credencial de invitado guardada ni el objeto API global

Puedes enlazar un botón del sitio a window.Quoting24Widget?.open() cuando el botón flotante haya aparecido. No esperes a isReady() para que un invitado nuevo abra el formulario. No existe un evento público de disponibilidad. Estos indicadores no describen todo el estado de red.

Que loadWidget() se resuelva no significa que el branding y la sesión estén listos.

Desmontaje completo

Si debes quitar el widget, ejecuta dispose(). Si el desmontaje coincide con el logout, ejecuta antes reset(). Antes de volver a cargar el script, elimina el global y pon a cero la Promise del loader.

export function removeWidget() {
  window.Quoting24Widget?.reset();
  window.Quoting24Widget?.dispose();
  delete window.Quoting24Widget;
  document.getElementById("quoting24-widget-script")?.remove();
  widgetPromise = undefined;
}

widgetPromise es la definida en el módulo del loader. Ejecuta el desmontaje después de terminar la carga. No lo hagas a la vez que una nueva loadWidget(). Para los cambios de autenticación usa applyWidgetIdentity() o applyWidgetAuth(), que esperan al loader.

El reset local no revoca restore_id en el servidor.


6. Restauración autenticada

La sesión de invitado lee su conversación más reciente incluso después de cerrarla. Las conversaciones visibles de la misma persona en otro navegador exigen juntos external_user_id y restore_id.

widget.init({
  external_id: "52",
  company_name: "Ricambi Stella S.r.l.",
  name: "Mario Rossi",
  phone: "+393331234567",
  email: "mario.rossi@ricambi-stella.example",
  external_user_id: "1042",
  restore_id: session.restore_id,
});

La restauración no une sesiones de invitado antiguas y no comparte el historial entre compañeros de la misma empresa.

Endpoint en el backend del sitio

Ejemplo para implementar en el portal, no una URL de Quoting24.

POST /api/widget/session
Content-Type: application/json

{}
{
  "external_user_id": "1042",
  "external_id": "52",
  "company_name": "Ricambi Stella S.r.l.",
  "name": "Mario Rossi",
  "phone": "+393331234567",
  "email": "mario.rossi@ricambi-stella.example",
  "restore_id": "<credencial-devuelta-por-quoting24>"
}

Define Cache-Control: no-store. Identidad y empresa solo desde la sesión del servidor. Aplica la auth y el CSRF de la aplicación. No registres cuerpos que contengan la credencial.

Si el portal ya expone un endpoint autenticado que devuelve el restore_id del usuario actual para este widget, usa esa respuesta y combínala con el ID de la persona y la referencia de empresa.

Provisioning de servidor a servidor

Ruta típica: /fastify/trpc/widget.provisionRestoreId
(o /trpc/widget.provisionRestoreId si el backend está expuesto directamente).

POST /fastify/trpc/widget.provisionRestoreId
Authorization: Bearer <SECRETO_INTEGRACION>
Content-Type: application/json

{
  "widget_api_key": "<uuid-publico-del-widget>",
  "external_user_id": "1042",
  "external_id": "52",
  "name": "Mario Rossi",
  "phone": "+393331234567",
  "email": "mario.rossi@ricambi-stella.example",
  "attempt_id": "1042"
}
{
  "result": {
    "data": {
      "restore_id": "<credencial-opaca>"
    }
  }
}

Los valores generados empiezan por q24-restore-v1:. No los inventes ni los derives de IDs públicos.

Entrada Requisito
widget_api_key UUID obligatorio, autorizado para el secreto
external_user_id ID de persona estable, igual al de init()
attempt_id Entero positivo estrictamente creciente, como cadena decimal, máximo 9223372036854775807
saved_restore_id Credencial ya guardada. Omítela en el primer provisioning
external_id, name, email, phone Opcionales, con los mismos límites que init()

Patrón ensure, recupera o crea:

  1. Autentica al usuario y adquiere un bloqueo sobre la fila de restauración, en una transacción
  2. Asigna un attempt_id nuevo desde una secuencia después del bloqueo (CACHE 1, no cíclica; la secuencia sobrevive al rollback)
  3. Llama a Quoting24 con attempt_id y, si existe, saved_restore_id
  4. Guarda result.data.restore_id y haz commit
  5. Solo después del commit devuelve la credencial al navegador

Un saved_restore_id válido debe reutilizarse. Omitirlo cuando ya existe una clave válida sustituye la clave e invalida las sesiones restauradas. No generes una clave nueva en cada vista de página.

Si el provisioning remoto tiene éxito y el guardado local falla, devuelve un error. No devuelvas la clave no guardada. Tiempos de espera limitados.

company_name se envía en init(). El cuerpo de provisioning de arriba no lo incluye: la asociación y la creación de la empresa, descritas en la sección 4, ocurren cuando el navegador inicializa la sesión. El correo de la inicialización pública solo se escribe en una empresa nueva.

Resultado en el widget

Quoting24 comprueba la credencial y carga el historial del widget visible de ese usuario. Los chats ya cerrados pueden aparecer como conversación anterior: consultarlos no reabre la conversación antigua; el envío se queda en la conversación actual.

Una credencial no válida, de otro usuario o revocada se rechaza, sin conversión automática a sesión de invitado. El botón «Riprova» del widget no pide por sí solo un restore_id nuevo. La interfaz actual está en italiano.


7. Página, red y aspecto

  • Una sola instancia por página
  • CSP. Permite el origen del widget en script-src y frame-src, y el origen de API indicado para la instalación en connect-src (carga de los ajustes gráficos). Autoriza las imágenes configuradas en img-src. El loader inserta estilos en línea y no expone una opción pública de nonce: comprueba la CSP del sitio. El CSS de la página no modifica el interior del iframe de otro origen
  • Conexiones del iframe. Debe alcanzar la API, Realtime/WebSocket y el almacenamiento de su propia instalación. Para filtros de red usa los hosts efectivos del entorno
  • Aspecto e idioma. Logotipo, color, posición y mensajes se configuran en Quoting24. company_name es la razón social del cliente. No existe un parámetro público language o locale; la interfaz actual está en italiano
  • Móvil. El chat flotante puede ocupar el viewport. En modo incrustado el sitio controla altura y anchura
  • Adjuntos y horarios. PDF, DOC/DOCX, JPG/JPEG y PNG. Hasta 10 MiB por archivo y 100 MiB de adjuntos referenciados por conversación. Fuera de horario el widget muestra el banner y deja disponible la redacción

8. Verificar la integración

Usa una cuenta y un canal de prueba. Comprueba el fichero en Quoting24, además del chat.

Prueba Resultado esperado
Navegador nuevo, solo script Formulario de contacto, luego conversación
Payload completo con external_id ya presente como web_code Contacto asociado a la empresa existente; nombre y correo de la empresa sin cambios
Payload completo con código nuevo Empresa creada con código web, razón social y correo; contacto asociado
Código nuevo y nombre, sin company_name Chat iniciado, ninguna empresa creada
Solo código nuevo Formulario de contacto
Solo razón social y nombre Ninguna empresa buscada o creada por nombre
Correo vacío, ID numérico o razón social demasiado larga Payload rechazado
Segundo init() en la misma sesión Continúa la sesión de invitado. El correo y el teléfono no recuperan el chat de otra persona
Recarga, navegación SPA, móvil Una sola instancia; continuidad de invitado si hay almacenamiento; diseño utilizable
Logout o cambio de cuenta durante la carga Sesión anterior borrada; los resultados tardíos no restauran el perfil antiguo
Mismo usuario autenticado, otro navegador, restore_id válido Historial de esa persona
Dos usuarios de la misma empresa Credenciales e historiales distintos
Invitado y luego restauración reset() en medio; las sesiones no se unen
Credencial no válida o revocada Error, sin respaldo automático a invitado
Conversaciones anteriores Solo consulta; el envío se queda en la actual
Dominio no autorizado Acceso rechazado
Red interrumpida Reconexión; borrador conservado si el envío falla

Resolución de problemas

Síntoma Comprobación
El botón no aparece URL, clave, canal habilitado, dominios, CSP, petición widget.getBranding. El solo global JavaScript no prueba que el botón exista. open() no tiene efecto hasta que se carga el branding, y no tiene efecto en modo incrustado
Quoting24Widget no está definido Espera a la carga. Comprueba el parámetro key
El chat incrustado es invisible Contenedor antes del loader, ID correcto, altura mayor que cero
Domain not allowed Nombre de host del sitio, sin esquema, puerto ni ruta. Comprueba también las pruebas. La página debe servirse por HTTP
Formulario de teléfono pese al perfil Orden asíncrono, snake_case, valores vacíos, reset() anterior, perfil insuficiente para un invitado nuevo
La empresa no se encuentra Compara external_id con web_code en la misma organización, espacios y ceros iniciales incluidos
La empresa que falta no se crea Hacen falta external_id y company_name ambos no vacíos. Comprueba la validación y el límite de creación
El nombre o el correo de la empresa no cambian Previsto: init() crea las empresas que faltan y no actualiza las existentes
El correo no aparece en el contacto Previsto en la inicialización pública: el correo solo se usa al crear la empresa
TOO_MANY_REQUESTS Espera y reintenta. Evita llamadas repetidas o importaciones masivas a través del widget
Falta el historial en otro dispositivo El payload de empresa no lo restaura. Hacen falta external_user_id y restore_id
Sesiones restauradas invalidadas Las llamadas ensure deben reenviar saved_restore_id. No emitas una clave nueva en cada vista de página
«Riprova» no basta Un restore_id nuevo desde tu endpoint, luego reset() e init()
Tras un cambio de cuenta queda el chat antiguo reset() de inmediato al cambiar de identidad. Descarta las respuestas asíncronas anteriores
Tras dispose() el script no recrea el widget Elimina el global y el script, pon a cero la Promise del loader y vuelve a cargar
La interfaz sigue en italiano Ninguna opción pública language o locale en el loader

Referencias de implementación

  • Loader y API JavaScript: @frontend/src/public/widget/script.ts
  • Inicialización, asociación de empresa y creación implícita: @backend/fastify/routes/widget/initialize.ts
  • Pruebas de inicialización y empresas: @backend/fastify/__tests__/widget/widgetInitialize.test.ts
  • Dominio y canal: @backend/fastify/routes/widget/procedures/publicWidgetProcedure.ts
  • Provisioning de restore_id: @backend/fastify/routes/widget/provisionRestoreId.ts