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>:
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&mode=embedded&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.
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:
- Si recibe un
external_idno vacío, busca una empresa de la misma organización cuyoweb_codesea igual a ese valor - Si la encuentra, vincula el contacto. El nombre y el correo de la empresa existente no cambian
- Si no la encuentra y también recibe un
company_nameno vacío, crea la empresa conname = company_name,web_code = external_idy el correo si existe, y luego vincula el contacto - 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.
{
"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"
}
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:
- Autentica al usuario y adquiere un bloqueo sobre la fila de restauración, en una transacción
- Asigna un
attempt_idnuevo desde una secuencia después del bloqueo (CACHE 1, no cíclica; la secuencia sobrevive al rollback) - Llama a Quoting24 con
attempt_idy, si existe,saved_restore_id - Guarda
result.data.restore_idy haz commit - 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-srcyframe-src, y el origen de API indicado para la instalación enconnect-src(carga de los ajustes gráficos). Autoriza las imágenes configuradas enimg-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_namees la razón social del cliente. No existe un parámetro públicolanguageolocale; 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