¿Podemos usar Google Analytics 4 para saber qué páginas y secciones de la documentación se utilizan? No se carga ni se envía nada antes de su consentimiento. Detalles en nuestra política de privacidad. Política de privacidad
El micro-frontend muestra la tienda de entradas de ADITUS directamente en tu página, sin iframe. Los sitios sin backend cargan el script y definen public-key junto con cart-scope="tab". Los sitios con backend crean la sesión en su servidor y pasan el token de sesión; tu servidor decide entonces quién es el comprador. Sin sesión, el micro-frontend permanece inactivo.
Hay dos caminos hacia una tienda en funcionamiento. Ambos usan el mismo script y el mismo elemento; se diferencian en quién crea la sesión.
Sin backend
El elemento crea él mismo una sesión de navegador para tu publicKey, limitada a los dominios permitidos. Define cart-scope="tab": sin identidad de servidor, el valor por defecto "shared" termina en un error explícito. Cada pestaña tiene entonces su propio carrito. La sesión es anónima: los visitantes exploran y llenan el carrito. Antes del registro, element.config.onUserRequired debe devolver un token de sesión vinculado a un usuario emitido por un servidor; sin él la tienda se detiene en el carrito con un aviso.
index.html
<!-- Site without a backend: load the script once, place the element. -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
<aditus-shop
public-key="pk_your_site"
cart-scope="tab"
></aditus-shop>
Con backend
Tu servidor crea la sesión con el mint secret y decide quién es el comprador. Pasa el token devuelto al elemento. Con el email del comprador, el valor por defecto cart-scope "shared" mantiene un único carrito en todas las pestañas.
server.js
// 1. On your server: create a session. The mint secret never leaves the server.const res = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
"Content-Type": "application/json",
},
// email comes from your own login session, never from the browser request.
body: JSON.stringify({ publicKey: "pk_your_site", email: user.email }),
});
const { sessionToken } = await res.json();
index.html
<!-- 2. In your page: pass the token from your server to the element. -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
<aditus-shop
public-key="pk_your_site"
session-token="SESSION_TOKEN_FROM_YOUR_SERVER"
></aditus-shop>
Sustituye pk_your_site por tu clave publicable del admin de ADITUS. developers.aditus.com es la dirección de producción del script y de la API. Ambos caminos se describen en detalle más abajo.
La tecnología detrás del micro-frontend
El micro-frontend muestra todo el proceso de compra (eventos, artículos, carrito, registro, pago, finalización) en el DOM de tu página. Usa el diseño de tu página y tu CSS puede dar estilo a cada parte.
React 19, compatible con React 18
Todo el flujo se ejecuta como un único flujo React tipado. Se construye y prueba con React 19 y usa solo las funciones de React 18 (useState, useEffect, useMemo y similares), por lo que el paquete fuente funciona con React 18 o posterior (peerDependency react >=18). Aparte de React tiene una dependencia de runtime: libphonenumber-js para validar números de teléfono. No hay biblioteca de UI, framework de data-fetching ni toolkit de estado. El script alojado por ADITUS ya incluye React, así que tu página no necesita React propio.
Light DOM en vez de iframe
Sin iframe ni shadow DOM. La tienda se muestra directamente en el DOM de tu página, encaja en tu diseño y es responsiva y accesible. Fuentes y colores vienen de los tokens --aditus-*; la fuente por defecto es system-ui. Define --aditus-font para usar la fuente de tu página.
Agnóstico al framework: un solo mount(), en todas partes
Una sola llamada (mount(container, config)) adjunta la tienda a cualquier elemento del DOM. React, Vue, Angular o HTML puro: el adaptador es independiente del framework y funciona igual en todas partes.
El estilo es tuyo
Cada elemento lleva un hook CSS estable (clases BEM aditus-shop__* más data-state). Lo estilizas con tu propio CSS o defines colores de marca, radios y espaciados con unos pocos design tokens --aditus-*. Los estilos base opcionales están en CSS @layer aditus-shop, así que tus reglas sin layer siempre ganan. Esto también se aplica a reglas genéricas de tu sitio como button {}, input {} o a {} de un tema de WordPress o Bootstrap: también modifican la tienda. Limita esas reglas a tus propias zonas de la página si la tienda no debe adoptarlas.
Multilingüe, conmutable en vivo
DE/EN vienen integrados. Un cambio de idioma se envía directamente a la instancia en ejecución mediante handle.update() sin reiniciar el recorrido del usuario. El carrito y el progreso se conservan.
Seguro por diseño
Sin una sesión válida, la tienda permanece inactiva y no hace peticiones a la tienda. La sesión viene de tu servidor (creada con el mint secret, que nunca llega al navegador) o, en sitios sin backend, de la sesión de navegador restringida al dominio de tu publicKey. Todas las llamadas pasan por la API de ADITUS, que añade las credenciales de la tienda en el servidor.
Estilo en tres niveles
Usa el nivel más general que encaje: los tokens de diseño establecen la marca, los hooks BEM semánticos generales hacen coherentes los roles recurrentes y los hooks específicos de componentes existentes se reservan para ajustes expertos.
Hooks semánticos generales
.aditus-shop__section, .aditus-shop__panel, .aditus-shop__list, .aditus-shop__list-item, .aditus-shop__actions y .aditus-shop__action. Estos hooks aditivos describen roles mientras cada elemento conserva su hook especializado. El análisis de URL y capturas del Styling Studio propone reglas exactamente en este nivel (borde vs. sombra, peso de botones, espaciado de secciones), nunca en hooks específicos de componentes.
Acciones y excepciones
Cuando se implementa, data-variant="primary" o data-variant="secondary" documenta el rol de una acción. No deduzcas roles semánticos a partir de la anidación del DOM: reg-form no es un panel documentado. Apunta a su hook especializado solo si ese formulario requiere intencionadamente una excepción.
Los requisitos dependen de la forma de entrega. El script alojado por ADITUS solo necesita un navegador actual; el paquete fuente necesita además React 18+ en tu app. Ambos necesitan una sesión para un publicKey cuya lista blanca contenga tu dominio.
Lo que se necesita
Solo con el paquete fuente: React 18+ y react-dom 18+ en el bundle de tu app (peerDependency react >=18); mount() usa createRoot de react-dom/client. El script alojado trae su propio React.
Un browser con un DOM (lado cliente). El adaptador monta en un elemento real del DOM (light DOM). Las apps SSR están bien mientras mount() se ejecute en el cliente.
Se requiere un browser evergreen actual. El adaptador usa API web estándar (fetch, AbortController, ResizeObserver, CSS @layer), baseline desde ~2022. Sin IE.
Una sesión para tu publicKey. Los sitios sin backend definen public-key junto con cart-scope="tab"; el elemento crea entonces su propia sesión de navegador mediante /shop/embed-session, restringido por dominio. Los sitios con backend crean la sesión en su servidor con el mint secret y pasan el token. Sin sesión, el micro-frontend permanece inactivo. Tu dominio debe estar en la lista blanca del publicKey.
Lo que no se necesita
Ningún framework concreto. mount(container, config) funciona con React, Vue, Angular o HTML puro.
Sin iframe, sin shadow DOM. Se renderiza nativamente en tu página.
Ningún CSS concreto en tu sitio. Los estilos base están en @layer aditus-shop con especificidad de una sola clase, así que cualquier regla sin layer de tu sitio gana. Esto incluye reglas genéricas de elementos como button, input o a: también afectan a los controles de la tienda; limítalas a tu propio contenido si no deben aplicarse allí. Box-sizing, padding y espaciados están además protegidos frente a un reset universal como * { margin:0; padding:0 }. baseStyles: false desactiva los estilos base.
Ninguna otra biblioteca en tu sitio. El paquete fuente trae, además de las peerDependencies de React, una dependencia de runtime (libphonenumber-js para validar números de teléfono); el script alojado lo contiene todo.
Ninguna configuración de build para el script alojado; el paquete fuente solo necesita un import ESM.
Tu sitio es de confianza: tu servidor autentica al usuario antes de generar una sesión identificada. El micro-frontend se muestra deliberadamente en el Light DOM de tu página, sin aislamiento iframe. Los scripts de tu página pueden acceder al DOM y al almacenamiento del navegador; por eso, protege tu sitio y sus scripts de terceros. La autorización de origen con clave pública no es una autenticación del usuario. Los datos de pago solo se introducen en la página externa del proveedor de pagos.
Seguridad de pagos, PCI y CSP
La preparación de la compra se muestra en tu página. El pago se realiza en la página del proveedor de pagos, así que ningún dato de pago pasa por tu página.
Ningún dato de pago en tu página
La preparación completa de la compra (evento, entradas, extras, carrito, registro) se renderiza de forma nativa en tu página. En el momento en que el usuario inicia un pago por redirección, el flujo sensible abandona por completo tu página: una navegación top-level normal cede el control a la página de pago externa dedicada del proveedor, y el viaje de vuelta reanuda el recorrido y completa el pedido. Los datos de tarjeta o bancarios se introducen solo allí. El micro-frontend nunca renderiza, transporta ni almacena credenciales de pago en tu página.
Alcance PCI mínimo
Dado que en ningún momento se procesan, transportan ni almacenan datos de tarjeta de crédito o bancarios en tu sitio, tu entorno permanece en el alcance PCI-DSS mínimo del modelo de redirección clásico (típicamente SAQ A; la clasificación vinculante siempre la realiza tu adquirente o QSA). Los métodos de pago sin redirección, como la factura, se completan del lado del servidor, sin ningún dato de pago en el browser.
Content Security Policy
Si tu sitio define una Content Security Policy, añade: script-src para el origen de ADITUS (script alojado), connect-src para el origen de la API de ADITUS (todas las llamadas de la tienda pasan por él), img-src para el origen de la API de ADITUS (banderas, logotipos de métodos de pago, insignias de wallet) y los hosts de tus imágenes de eventos y artículos, y style-src 'unsafe-inline', porque los estilos base se insertan como elemento <style> (no es necesario con baseStyles: false y tu propia hoja de estilos). No hacen falta entradas script-src ni frame-src para proveedores de pago, porque el paso de pago es una redirección de nivel superior.
Comparación con iframe y shadow DOM
Iframe
Un iframe es un documento separado: los estilos de tu página no se aplican dentro, su altura debe sincronizarse y tiene su propio orden de foco. El micro-frontend, en cambio, muestra la preparación de la compra como HTML en el DOM de tu página. Solo el pago sale de tu página, mediante una redirección de nivel superior a la página del proveedor de pagos.
Shadow DOM
Muchos web components se encapsulan en un shadow DOM. Los estilos de tu página solo les llegan entonces mediante custom properties y ::part. El micro-frontend usa en cambio la cascada CSS normal: sus estilos base están en @layer aditus-shop, así que cualquier regla sin layer de tu página gana sin mayor especificidad ni !important.
AccesibilidadDocumento separado con su propio orden de foco y estructura.Las referencias ARIA por ID (aria-labelledby, aria-describedby) no cruzan el límite del shadow DOM.HTML en el DOM de tu página: un único orden de foco y una única estructura con tu propio marcado.
Estilo y brandingRígido: sin herencia de CSS, personalización solo a través de las API postMessage.Los estilos globales no llegan al interior; el theming solo mediante propiedades y parts transferidos.Tu CSS sin layer gana a los estilos base en @layer; estos se pueden sobrescribir o desactivar.
Bundle y rendimientoNormalmente carga una segunda aplicación completa, incluido su runtime, dentro del frame.A menudo trae su propio runtime y dependencias duplicadas.Script alojado: un bundle que incluye React. Paquete fuente: usa el React de tu app más una dependencia de runtime (libphonenumber-js).
Seguridad de pagos y alcance PCIFuertemente aislado, pero a costa de la UX, el diseño y la capacidad de respuesta.Depende del widget: los campos de tarjeta dentro del widget forman parte de tu página.Selección en tu página, pago en la página del proveedor de pagos.
Nuestra recomendación: JavaScript directamente desde la URL de ADITUS
Carga el bundle JavaScript directamente desde ADITUS. Es nuestra opción recomendada para todos los sitios, incluidos React y otros frameworks, no solo para sitios HTML o CMS. ADITUS mejora la lógica interna de la tienda de forma centralizada. Las mejoras compatibles llegan a tu integración sin descargar un nuevo paquete, recompilar ni desplegar tu aplicación.
La URL v1 entrega versiones compatibles con interfaces estables. Las nuevas versiones se cargan en visitas posteriores cuando caduca o se revalida la caché HTTP, actualmente de una hora, no necesariamente en cada recarga. Un proceso de compra ya abierto conserva su código.
Alternativa: gestionar la distribución y las actualizaciones
Elige el paquete fuente, la instalación del tarball mediante npm o una copia JavaScript alojada por ti solo si quieres gestionar la distribución. ADITUS no puede actualizar tu copia. Debes comprobar las nuevas versiones, descargarlas o instalarlas, probarlas, recompilar cuando sea necesario y desplegarlas. El paquete no está publicado en el registro público de npm.
Alternativa: paquete fuente (distribuido por ti)
Como alternativa, descarga el paquete fuente de tu instancia ADITUS e intégralo en tu aplicación (Vite, webpack, Next.js o Nuxt). Tú te encargas de distribuirlo: cada actualización exige reinstalar, compilar y desplegar. No está en npm público.
Qué contiene la entrega
El adaptador completo como código fuente TypeScript legible con declaraciones de tipos para cada opción de config, callback y evento. React y react-dom siguen siendo peerDependencies. Tu página los proporciona, así que nada colisiona en tu bundle y no hay un segundo React.
Obtener el paquete
El paquete es idéntico para cada sitio y se descarga directamente desde esta instancia, sin credenciales: aditus-shop-embed.tgz. Instálalo desde esta URL (npm install acepta URL de tarball, ruta /api/embed/v1/aditus-shop-embed.tgz) o extráelo en tu repositorio. El onboarding proporciona tu publicKey y la lista de dominios permitidos; sin publicKey el micro-frontend permanece inactivo. Contacto: página de contacto. Usa este paquete solo si distribuyes y actualizas tu propia compilación.
Mantente al día: la pista v1 recibe actualizaciones compatibles en el sitio. Comprueba con regularidad manifest.json (antes de cada release de tu app, idealmente como tarea programada) y compara package.sourceHash con aditus.sourceHash del package.json instalado. Si difieren, reinstala el tarball y ejecuta build y pruebas; hooks, tokens y eventos se mantienen estables dentro de v1. El bundle del Web Component se actualiza solo a través de la caché HTTP de una hora.
Integración por script desde la URL de ADITUS
El bundle precompilado en https://developers.aditus.com/api/embed/v1/aditus-shop.js funciona en cualquier sitio, incluidas aplicaciones con compilación, WordPress, Typo3 y HTML puro. Una etiqueta script registra <aditus-shop> con React incluido: no hacen falta npm ni compilación. Esta URL pública y versionada no pide credenciales; la tienda sigue exigiendo una publicKey autorizada y una sesión. Una compra en curso nunca se actualiza en caliente; la caché del navegador puede retrasar el nuevo bundle hasta una hora.
index.html
<!-- 1. Include the bundle hosted by ADITUS (once per page). -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
<!-- 2. Explicit tab isolation for anonymous sessions created automatically in the browser (not the default).
For shared: session-token from your backend; anonymous ones also need a server-side sharedCartKey. -->
<aditus-shop
public-key="pk_live_yourkey"
api-base="https://developers.aditus.com/api"
cart-scope="tab"
culture="en-GB"
></aditus-shop>
<!-- 3. Optional: consume journey events as plain DOM events. -->
<script>
document.querySelector("aditus-shop").addEventListener("cart:update", (e) => {
console.log("Items in cart:", e.detail.itemCount);
});
</script>
La misma URL JS alojada registra automáticamente <aditus-shop> Y expone window.AditusShop: mount, themeToCss, clearJourney, mintEmbedSession y onEmbedSessionRenewed, con las firmas del paquete fuente. Tu sitio no necesita React. El mount alojado, element.config y el paquete fuente aceptan la misma AditusShopConfig, incluidos temas, eventTheme: false, articleContent y callbacks. Solo los atributos HTML no pueden transmitir funciones u objetos. El paquete fuente sigue siendo válido para bundlers y compartir tu runtime React.
mount(container, config) es el mismo mount fuente: sin sessionToken queda inactivo y no genera sesiones. Define apiBase explícitamente con la URL completa /api/shop. Tras tu login asíncrono, el backend genera la sesión servidor a servidor y entrega solo el token. host.renewSession y host.revokeSession son funciones/rutas propias que debes implementar en tu backend autenticado, no endpoints de navegador ADITUS listos para usar. Nunca envíes secretos al navegador. onUserRequired sigue siendo un callback asíncrono de login y creación de sesión en el servidor que devuelve un token o null, en cada variante JavaScript.
index.html / your own login
<div id="ticketshop-slot"></div>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js"></script>
<script>
// Called after your async login and server-side session creation, with the returned sessionToken.
// host.renewSession(staleToken): Promise<string | null>
// host.revokeSession(token): Promise<void>
// These are HOST-SPECIFIC backend functions, not built-in ADITUS routes.
// Your backend authenticates the user; mint secrets never enter the browser.
window.mountKnownSessionShop = function (sessionToken, host) {
if (!sessionToken) throw new Error("A sessionToken from your backend is required");
const slot = document.getElementById("ticketshop-slot");
if (!slot) throw new Error("Missing mount container");
let disposed = false;
let config = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site",
sessionToken,
cartScope: "shared", // default; trusted S2S identity required; immutable after mount
culture: "en-GB",
refreshSession: async (staleToken) => {
const token = await host.renewSession(staleToken);
if (disposed) return null;
if (token) config = { ...config, sessionToken: token };
return token; // Adapter swaps token and retries the failed request once.
},
onError: (error) => console.error("Ticketshop:", error.message),
};
const handle = window.AditusShop.mount(slot, config);
return {
updateCulture(culture) {
if (disposed) return;
config = { ...config, culture };
handle.update(config); // full replacement, not a patch
},
async logout() {
disposed = true;
handle();
window.AditusShop.clearJourney(); // local resume pointer only
await host.revokeSession(config.sessionToken); // HOST-SPECIFIC S2S revoke
},
teardown() {
disposed = true;
handle(); // SPA route cleanup, not server-side revocation
},
};
};
// Your site supplies the real token and implementations:
// const shop = window.mountKnownSessionShop(sessionToken, host);
// Logout: await shop.logout(); route change: shop.teardown();
</script>
JS puro: sesión de navegador y renovación
Con una publicKey autorizada, mintEmbedSession(publicKey, apiRoot = "/api", { fresh?, staleToken? }) devuelve Promise<string | null> sin secreto. Los helpers y api-base usan la raíz /api; config.apiBase usa la base shop /api/shop. onEmbedSessionRenewed(publicKey, apiRoot, listener) entrega un token y devuelve una función de baja. El ejemplo trata errores iniciales, expiración, renovación proactiva y limpieza. La baja solo elimina este listener, no las cachés compartidas, temporizadores ni sesiones de servidor; los helpers mantienen continuidad mediante sessionStorage.
index.html / browser session
<div id="ticketshop-slot"></div>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js"></script>
<script>
window.mountBrowserSessionShop = async function () {
const { mount, mintEmbedSession, onEmbedSessionRenewed } = window.AditusShop;
const publicKey = "pk_your_site"; // your whitelisted key, not a secret
const apiRoot = "https://developers.aditus.com/api"; // no /shop for helpers
const slot = document.getElementById("ticketshop-slot");
if (!slot) throw new Error("Missing mount container");
const sessionToken = await mintEmbedSession(publicKey, apiRoot);
if (!sessionToken) throw new Error("Browser session failed: check key/domain");
let disposed = false;
let config = {
apiBase: apiRoot + "/shop", // mount uses the shop base
publicKey,
sessionToken,
cartScope: "tab", // explicit isolation opt-in: browser-created sessions cannot safely share
culture: "en-GB",
refreshSession: async (staleToken) => {
const token = await mintEmbedSession(publicKey, apiRoot, {
fresh: true, staleToken,
});
if (disposed) return null;
if (token) config = { ...config, sessionToken: token };
return token;
},
onError: (error) => console.error("Ticketshop:", error.message),
};
const handle = mount(slot, config);
const offRenewed = onEmbedSessionRenewed(publicKey, apiRoot, (token) => {
if (disposed) return;
config = { ...config, sessionToken: token };
handle.update(config); // preserve callbacks and all other options
});
return function teardown() {
disposed = true;
offRenewed();
handle();
};
};
// const teardown = await window.mountBrowserSessionShop();
// Start now; your SPA calls window.teardownTicketshop() on route change.
window.mountBrowserSessionShop().then((teardown) => {
window.teardownTicketshop = teardown;
}).catch((error) => {
document.getElementById("ticketshop-slot").textContent = error.message;
});
</script>
Atributos y config JavaScript completa
element.config acepta toda AditusShopConfig, incluidas funciones. Las claves definidas prevalecen sobre atributos, incluso false; las ausentes/undefined dejan activos los atributos. Cada asignación reemplaza todos los overrides, no es un parche: element.config = {} los elimina. Se admite asignar antes del upgrade. api-base es la raíz /api, config.apiBase termina en /api/shop. themeToCss(theme, selector = ".aditus-shop") devuelve CSS de tokens como alternativa al objeto tema.
index.html / element.config
<aditus-shop id="shop" public-key="pk_your_site"
api-base="https://developers.aditus.com/api" cart-scope="tab" culture="de-DE"></aditus-shop>
<script>
const element = document.getElementById("shop");
// Supported before upgrade by the following hosted script, too.
element.config = {
apiBase: "https://developers.aditus.com/api/shop",
culture: "en-GB", // overrides the HTML culture attribute
cartScope: "tab", // explicit isolation opt-in for this anonymous browser-created example
theme: { accent: "#c2410c", fontSize: "16px" },
eventTheme: false,
onComplete: (order) => console.log("Order completed:", order),
onError: (error) => console.error("Ticketshop:", error.message),
};
// No sessionToken: effective publicKey enables browser session creation.
// Optional refresh by your site: refreshSession: (staleToken) => host.renewSession(staleToken)
// host.renewSession is HOST-SPECIFIC; your refresh always takes precedence.
// Later: element.config = {}; removes overrides, attributes apply again.
// Cleanup: element.remove();
</script>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
AtributoSignificado
cart-scopeAditusShopConfig.cartScope?: 'tab' | 'shared' usa 'shared' por defecto, también si se omite. 'tab' es una elección explícita para aislar recorridos. window.AditusShop.mount() alojado centralmente, element.config y mount() del paquete fuente/framework aceptan la misma opción; HTML usa cart-scope="tab" o cart-scope="shared". Elige antes de montar. El ámbito es inmutable para la instancia de checkout montada: handle.update() rechaza cambios en lugar de migrar un carrito activo. Consérvalo en las configuraciones de sustitución completas.
public-keyTu clave publicable (se aplica la lista blanca de dominios). Sin session-token, el elemento crea él mismo su sesión de navegador. Los sitios sin backend usan este modo y definen además cart-scope="tab"; con el valor por defecto "shared", la sesión de navegador termina en un error explícito porque no hay una identidad de servidor de confianza.
session-tokenUn token de sesión que tu backend creó con el mint secret (ver abajo). Si se define, el elemento nunca crea una sesión por sí mismo; tu página gestiona el ciclo de vida de la sesión.
api-baseURL base de la API de ADITUS. Por defecto, el origen desde el que se cargó el script del bundle. Normalmente nunca lo defines.
cultureCulture/locale de la tienda enviada para seleccionar recursos de ADITUS; se transmiten valores válidos como it-IT. Los textos de recursos de voucher y upsell se mantienen tal como los proporciona ADITUS; el resto de la interfaz usa recursos en alemán/inglés con fallback. Esto no implica una UI completamente en italiano. Cambiarla relocaliza en el sitio sin reiniciar el recorrido.
article-layout / article-group-layout / event-layout / article-select-mode / show-headerLas opciones de presentación planas, mismos valores que la config de mount() (cards, grid, quantity, …). El punto de entrada (selector de evento vs. un evento fijado) no es un atributo: se fija en el servidor al generar la sesión.
include-shop-assortmentsMezcla opcional de surtidos; includeShopAssortments en mount(). Por defecto false: solo carga el surtido predeterminado. Con true añade los surtidos showInShop=true. Un surtido inicial explícito siempre tiene prioridad. Sin atributo se aplica el valor del cliente; false explícito lo sustituye.
survey-columnsNúmero de columnas (1–4) de las opciones de respuesta de las preguntas RadioButtonList/CheckBoxList. Equivale a surveyColumns en la config de mount(). Sin atributo se mantiene una columna (o el valor por defecto del cliente en el servidor); las pantallas estrechas vuelven a una columna.
hide-house-numberOculta el campo del número de la calle en el bloque de dirección de las encuestas, en todas las variantes de país; equivale a hideHouseNumber en la config de mount(). La calle y el número se introducen juntos en un campo «Calle y número» que ocupa el ancho liberado; las sugerencias de dirección escriben allí la calle y el número en el orden habitual del país (p. ej. "12 Rue Cler" en Francia), un número precargado se vacía y la respuesta separada del número se envía siempre como un espacio invisible de ancho cero (U+200B), sin error de validación, aunque la encuesta la marque como obligatoria. El formulario de dirección de facturación sigue la misma regla. Desactivado por defecto; sin atributo se aplica el valor del cliente, "false" explícito lo sustituye.
show-jump-buttonBotón de salto en el paso de personalización; equivale a showJumpButton en la config de mount(). Activado por defecto: tras un envío fallido, una pequeña píldora queda fija abajo en el widget, muestra cuántos datos faltan y salta al siguiente; con todo completo, vuelve al botón sin enviarlo nunca. Se oculta mientras ese botón o el destino están visibles y mientras se escribe. "false" lo desactiva; sin atributo se aplica el valor del cliente.
voucher-code-paramNombre de un parámetro de consulta de la URL de tu página que lleva un código de vale/entrada (p. ej. "code" para enlaces como …/tickets?code=ED24). Equivale a voucherCodeParam en la config de mount(). El elemento lo lee una vez al montar, lo elimina de la barra de direcciones y canjea el código automáticamente (formulario de vale o primer carrito). Solo para sesiones fijadas a un evento.
base-stylesCon el valor "false" se elimina el skin neutro por defecto y estilizas cada hook tú mismo.
autoScroll / articleIconVisible / classPrefixSin atributo: define estas opciones mediante element.config (mismos nombres que en la config de mount()). autoScroll (por defecto true) vuelve a mostrar el inicio de la tienda tras un cambio de paso; pon false si tu página gestiona el desplazamiento. articleIconVisible (por defecto false) muestra una imagen o un monograma por artículo en la vista de lista. classPrefix (por defecto "aditus-shop") cambia el prefijo de todos los hooks CSS.
Sesión y autorreparación
Un sessionToken efectivo (config o atributos) desactiva la creación automática de sesión. Con publicKey efectiva sin token, el elemento sigue generando su sesión. refreshSession de tu sitio siempre tiene prioridad sobre el integrado. Sin él, las sesiones automáticas usan el helper compartido para refresh y renovación proactiva. Light DOM sigue igual: sin iframe ni shadow DOM.
Eventos y versionado
Los DOM CustomEvents burbujean desde el elemento con payload en event.detail: cart:update, phase:change, checkout:complete y analytics, iguales a handle.on(). También hay callbacks de config.
Reiniciar el recorrido deliberadamente
element.clearJourney() y window.AditusShop.clearJourney() eliminan solo el puntero local de reanudación de esta página. Llama antes de volver a insertar o montar para empezar de nuevo. No eliminan carritos de servidor ni revocan sesiones; la revocación de logout corresponde al backend.
Actualizaciones sin remontar
Las configs mount/update son reemplazos completos, no parches: guarda la config y pasa { ...config, culture } a handle.update(). element.config también reemplaza todos los overrides; los atributos no cubiertos actualizan en el sitio. La presentación conserva el recorrido; la identidad restablece la sesión. handle() desmonta; quitar el elemento limpia listeners embed y renovación. Retira tus listeners DOM si conservas el elemento.
Apps SSR: Next.js y Nuxt
El micro-frontend es deliberadamente client-only. Su contenido está en vivo y ligado a la sesión: disponibilidad, precios y el carrito del usuario existen solo para una sesión autenticada en el momento de la petición, así que no hay nada significativo que tu servidor pudiera pre-renderizar. mount() crea una nueva raíz React del lado del cliente (createRoot); el marcado renderizado en el servidor dentro del contenedor no se hidrata. Eso hace que la integración SSR sea simple y predecible: renderiza un placeholder en el servidor, monta en el cliente y reserva el espacio para que nada salte.
ticketshop-section.tsx
// Next.js (App Router): the micro-frontend is deliberately client-only."use client";
import { useEffect, useRef } from"react";
import { mount, type ShopHandle } from"@aditus/shop-embed";
exportfunction TicketshopSection({ sessionToken }: { sessionToken: string }) {
const el = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!el.current) return;
const handle: ShopHandle = mount(el.current, {
publicKey: "pk_your_site",
sessionToken, // issued server-side, passed to the client component as a prop
cartScope: "shared", // the default in frameworks too; trusted server identity
});
return () => handle(); // unmount on route change
}, [sessionToken]);
// Server and client render the SAME placeholder and so avoid a hydration mismatch.// min-height reserves the space so the page does not jump on mount (CLS).return <div id="aditus-shop" ref={el} style={{ minHeight: 560 }} />;
}
Evitar el desplazamiento de diseño (CLS)
Dale al contenedor de mount una min-height que coincida aproximadamente con la primera vista de la tienda y, opcionalmente, renderiza tu propio skeleton dentro. Servidor y cliente producen el mismo placeholder, así que no hay hydration mismatch. El adaptador se renderiza inmediatamente después del mount y reemplaza el placeholder en un solo paint. No intentes hydrateRoot en el contenedor: no hay ningún árbol de tienda renderizado en el servidor al que engancharse.
La sesión en una app SSR
Las apps SSR tienen una ventaja natural: el runtime del servidor que renderiza la página también puede generar la sesión servidor a servidor con el secreto, exactamente como el flujo de creación de sesión de más abajo (Next.js: Route Handler o Server Component; Nuxt: ruta de servidor). Entrega el token opaco resultante al componente cliente como prop o payload. El secreto nunca aparece en el código cliente, y el token permanece en memoria.
Detalles de Next.js
Marca el componente de mount con "use client" y monta en useEffect (ver snippet). Con el Pages Router, next/dynamic con ssr: false consigue lo mismo. Devuelve el handle desde el efecto como cleanup para que los cambios de ruta del lado del cliente se desmonten limpiamente. Un cambio de culture en la instancia en ejecución pasa por handle.update() sin reiniciar el recorrido; un nuevo token de sesión vuelve a montar (como en el snippet). El carrito también sobrevive a eso, porque el micro-frontend restaura el recorrido en el mount.
Detalles de Nuxt / Vue
Envuelve el objetivo de mount en <ClientOnly> o monta en onMounted. El adaptador necesita un elemento real del DOM en el browser. Recuerda el requisito del bundle: react y react-dom 18+ deben estar instalados como dependencias de tu aplicación Nuxt; la tienda se renderiza en su propio contenedor y no interfiere con el DOM virtual de Vue.
Ámbito del carrito, confianza y almacenamiento
cartScope determina si las pestañas de un visitante comparten un mismo carrito. El valor predeterminado es 'shared'; 'tab' separa las pestañas a propósito. Elígelo antes del montaje; después no se puede cambiar.
Aspecto'shared' (predeterminado)'tab'
CarritoUn carrito para todas las pestañas con el mismo origen, publicKey, identidad y eventoCada pestaña tiene su propio carrito
SesiónEmitida por tu backend: usuario identificado, o anónimo con sharedCartKey de tu propia sesión de servidor. Nunca tomes la identidad ni la clave del navegador.Cualquier sesión, incluida una sesión creada en el navegador
Varias pestañasUna pestaña edita; las demás son de solo lectura y ofrecen «Seguir editando aquí». Una pestaña que se abre o se recarga toma el control automáticamente, nunca durante el pago.Independientes
Si falta el requisitoError explícito, incluso si se omite cartScope; sin cambio silencioso a 'tab'. Por eso, las páginas sin backend usan 'tab'.–
Configuración: cartScope en mount() y element.config, cart-scope en HTML. handle.update() rechaza un cambio, así que conserva el valor en cada config de reemplazo. Atributos · Campos de sesión
Almacenamiento: Los borradores de formulario se guardan por pestaña en sessionStorage y caducan 2 horas después de la última edición. Los tokens nunca van a localStorage. Los pedidos y registros confirmados no se ven afectados.
Cierre de sesión: Al cerrar sesión o cambiar de usuario, tu backend revoca la sesión anterior; después actualiza el token o desmonta el embed y llama a clearJourney(). Un usuario nuevo nunca hereda el carrito del anterior.
Seguridad: El embed se renderiza directamente en tu página, sin iframe. Los scripts de tu página, incluidos los de terceros, pueden acceder a él; por eso incluye solo scripts de confianza. Los datos de pago se introducen únicamente en el proveedor de pagos.
shared-cart.js
// Load the recommended centrally hosted aditus-shop.js first.// sessionToken comes from your trusted backend's server-side session, not a browser-created one.// Identified user: backend authenticates user and derives email/externalUserId.// Anonymous visitor: backend omits email and MUST derive sharedCartKey// from its own trusted visitor session. Neither identity nor key is browser input.const slot = document.getElementById("ticketshop-slot");
if (!slot) throw new Error("Missing mount container");
const config = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site",
sessionToken,
cartScope: "shared",
};
const handle = window.AditusShop.mount(slot, config);
// Full replacement config retains the chosen scope:
handle.update({ ...config, culture: "en-GB" });
// Do not change cartScope on this handle. Route teardown: handle();
El micro-frontend necesita una sesión
Sin token
Sin token de sesión: el micro-frontend permanece inactivo
Sin un token de sesión, el micro-frontend muestra un aviso neutro y no lanza ninguna llamada a la API. No se puede llenar ningún carrito, ni registro, ni pago. Una sesión es obligatoria antes de que la tienda se active, pero no tiene que estar ligada a un usuario conocido: una sesión de invitado basta para navegar y llenar el carrito, y el usuario real puede definirse más tarde en el pago (ver el flujo de usuario opcional más abajo).
mount.ts
import { mount } from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// Without a sessionToken the micro-frontend stays inactive: it only shows a// neutral notice and starts no API calls: no cart, no registration,// no checkout. Only a session activates the shop.
mount(el, {
publicKey: "pk_your_site",
// sessionToken missing -> the shop is not activated.
});
Usuario de tu sitio
Token de sesión para un usuario concreto
Tu backend genera un token de corta duración ligado a un solo usuario. El micro-frontend actúa entonces como ese usuario: su carrito, sus datos. El token es opaco, caduca, es mono-usuario y está ligado a tu publicKey, y tu secreto nunca llega al browser.
mount.ts
import {
mount,
type ShopHandle,
type AditusShopConfig,
} from"@aditus/shop-embed";
// getElementById can return null. Check it properly in TypeScript instead of using "!".const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// AditusShopConfig types every option and gives you autocompletion.const config: AditusShopConfig = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site", // required as soon as a session is involved
sessionToken, // issued by your backend, keep it in memory only
cartScope: "shared", // default; requires a trusted server identity
};
const handle: ShopHandle = mount(el, config);
// The micro-frontend sends the X-Aditus-Session header with every shop call.// On logout, revoke the session on the server and remount/update.
Al recargar, un puntero válido permite solicitar el carrito del servidor con el mismo ámbito autorizado e identidad de confianza. No autoriza ni garantiza recuperar tras vencimientos, borrar el almacenamiento o cambiar de usuario. tab no une recorridos de otras pestañas; shared mantiene el bloqueo de checkout con un solo escritor.
El retorno de pago usa aditus-return (un token opaco, breve y vinculado al servidor) junto con aditus-pay. Puede volver en la misma pestaña o en una nueva, pero el servidor exige cliente/origen autorizado e identidad de confianza coincidente. Los parámetros de retorno no prueban una compra. El servidor comprueba el pago y finaliza el intento vinculado al token; un resultado desconocido permanece bloqueado para aclaración, sin repetir a ciegas una nueva compra. Los retornos vencidos o no autorizados son errores explícitos, no permiso para crear un pedido sustituto.
Asignar un proveedor solo prepara el pago: el resumen sigue editable. El bloqueo comienza en la redirección efectiva, tras los consentimientos requeridos. Un parámetro de retorno cancel/error no demuestra un fallo: el backend vuelve a consultar el carrito; Completed con order.id confirma la finalización. Solo eliminar correctamente y con confirmación la asignación del proveedor para ese mismo carrito permite reintentar; los resultados ambiguos siguen bloqueados y pueden volver a comprobarse. Esto no garantiza un reembolso ni excluye una liquidación tardía.
Pago con redirección: qué debe hacer tu página con la sesión
Sirve la URL de retorno configurada con el embed montado. Conserva aditus-return y aditus-pay al enrutar o redirigir al login; aditus-pay=success nunca es autorización. Proporciona una sesión válida para la misma identidad de confianza y publicKey. Mantén cartScope y no borres el recorrido ni inicies otro checkout para eludir un retorno pendiente, vencido o incierto.
¿Prefieres un reinicio deliberado con un verdadero botón de «empezar de nuevo»? El paquete exporta clearJourney(): llámalo antes de montar (o volver a montar), y se descarta el registro de recorrido persistido localmente para la página actual, de modo que el siguiente mount arranca desde cero en el punto de entrada. Solo se elimina el puntero de reanudación local; cualquier estado de carrito en el servidor queda intacto.
Punto de entrada: inicio del evento, artículos o vale
Tu servidor decide el inicio al generar la sesión, nunca el navegador. Sin eventSlug aparece la lista de eventos. Con eventSlug, startMode elige artículos (predeterminado), inicio o formulario de vale; assortment limita los artículos.
La selección de artículos y la continuación tras un vale siguen la ruta de la tienda original basada en recomendaciones. Sin oferta upsell utilizable ni venta cruzada, se pasa directamente a la personalización de entradas con las comprobaciones normales de inicio de sesión; con venta cruzada, se muestra el carrito. Una oferta upsell utilizable abre primero su diálogo. Omitir lleva a la personalización sin venta cruzada, o al carrito si la hay. Una mejora confirmada lleva al carrito; cerrar el diálogo mantiene la selección. La ruta nunca guarda el registro en segundo plano.
Los surtidos visibles pueden tener entradas directas. Si se fija un surtido, el inicio muestra solo su acceso a entradas, no la lista completa ni la acción de vale.
startMode: "home" abre la página de inicio del evento. Allí se elige Comprar entradas o Canjear vale; abrirla no crea un carrito ni canjea un código. Comprar entradas abre la selección de artículos (limitada por assortment si está definido); Canjear vale abre el formulario del código. Sin eventSlug aparece primero la lista de eventos y después la página de inicio del evento elegido. Un carrito de vale preparado o un recorrido reanudable tiene prioridad.
Predeterminado
Inicio en artículos
Solo eventSlug: el micro-frontend abre la selección de artículos de ese evento con todos los surtidos visibles en la tienda.
assortment
Solo un surtido
assortment limita el inicio en artículos a un surtido del evento: su id o nombre exacto (sin distinguir mayúsculas). Se permiten surtidos ocultos, así se pueden ofrecer p. ej. entradas de prensa gratuitas en una página propia. Un valor sin coincidencia muestra un error explícito en lugar de listar todo en silencio.
startMode: "voucher"
Canjear vale / registrar entrada
El recorrido abre un formulario de código en lugar de la lista de artículos. Un código válido reserva su entrada. Tras canjearlo correctamente, la configuración ADITUS del evento configuration.redirectAfterRegistration determina si el visitante permanece en la pantalla de éxito o se le redirige automáticamente. La continuación sigue la misma ruta basada en recomendaciones que la selección de artículos, con las comprobaciones normales de inicio de sesión antes de la personalización. Atrás conserva la reserva. «Añadir más vales» vuelve a abrir el campo de código en la pantalla de vale para el mismo carrito; «Seleccionar entradas» abre la selección de artículos. Un código rechazado permanece en el formulario con el motivo. Una entrada totalmente cubierta omite el pago.
Un código promocional introducido en ese formulario (un código que rebaja artículos en lugar de cubrir una entrada) también se gestiona: el micro-frontend pasa a la selección de artículos con los precios que produce ese código, nombra cada precio modificado (antiguo y nuevo) y mantiene el código en el carrito para que el descuento se aplique también a los artículos elegidos. «Quitar código» vuelve al formulario con los precios normales.
Dónde configurarlo
Generación en servidor o registro de cliente
Las generaciones servidor a servidor envían startMode / assortment en el cuerpo junto a eventSlug. Las sesiones generadas por el navegador (solo clave publicable) toman los tres valores del registro de cliente que configura tu contacto de ADITUS; un valor en el cuerpo se ignora allí. Solo assortment requiere eventSlug (400 start_option_requires_event_slug); startMode también funciona con la selección de eventos; valores desconocidos dan 400 invalid_start_mode / invalid_assortment. GET /shop/session-context devuelve startMode y assortment de la sesión.
mint-entry.ts
// Server side: use the same session route as above, only the body varies.// You build user and your backend's login/session route yourself.// eventSlug sets the event. startMode also works without a slug (event// selection first, then the mode); only assortment needs the slug// (otherwise 400 start_option_requires_event_slug).// 1) Article start (default): the event's article selectionconst bodyArticles = { publicKey, email: user.email, eventSlug: "ExperienceDaysv52024" };
// 2) Assortment start: only one assortment (id or exact name, case-insensitive)const bodyAssortment = {
publicKey, email: user.email,
eventSlug: "ExperienceDaysv52024",
assortment: "Pressetickets",
};
// 3) Voucher start: "Redeem voucher / register ticket" firstconst bodyVoucher = {
publicKey, email: user.email,
eventSlug: "ExperienceDaysv52024",
startMode: "voucher" as const,
};
const res = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`, "Content-Type": "application/json" },
body: JSON.stringify(bodyVoucher),
});
const { sessionToken }: { sessionToken: string } = await res.json();
voucher-hooks.css
/* Voucher form: the same tokens and role hooks as the rest of the journey */
.aditus-shop__voucher-panel { /* panel around hints + form (general panel hook) */ }
.aditus-shop__voucher-hints { /* the two hint lines */ }
.aditus-shop__voucher-form { /* form */ }
.aditus-shop__voucher-input { /* code field (also .aditus-shop__field-input) */ }
.aditus-shop__voucher-redeem { /* "Redeem" button (data-variant="primary") */ }
.aditus-shop__field-error { /* rejection reason, role="alert" */ }
/* Event home page and event actions: structured expert hooks */
.aditus-shop__home { /* home page; root data-step="home" */ }
.aditus-shop__home-info { /* event information */ }
.aditus-shop__home-info-head { /* heading and logo */ }
.aditus-shop__home-info-name { /* event name */ }
.aditus-shop__home-info-date { /* date */ }
.aditus-shop__home-info-venue { /* venue */ }
.aditus-shop__home-info-desc { /* description */ }
.aditus-shop__home-actions[data-variant="tiles"] { /* tiles, alternatively compact */ }
.aditus-shop__home-action[data-kind="articles"] { /* buy tickets; also home / voucher / assortment */ }
.aditus-shop__home-action-icon { /* icon */ }
.aditus-shop__home-action-body { /* text group */ }
.aditus-shop__home-action-title { /* label */ }
.aditus-shop__home-action-desc { /* optional description */ }
.aditus-shop__home-action-chevron { /* arrow */ }
/* Phase hook: [data-step="voucher"] on the root */
Enlaces de vale: canjear un código desde la URL
Los vales suelen enviarse como enlaces a tu página con el código como parámetro de consulta, p. ej. …/tickets?code=ED24-ADITUS. El micro-frontend puede tomar ese código y canjearlo sin que el visitante escriba nada. Hay dos vías de entrega; ambas terminan en la misma llamada de canje y se comportan igual después.
voucherCodeParam: "code"
A) En el navegador: el micro-frontend lee la URL
Nombra el parámetro en la config de mount() (voucherCodeParam) o en el elemento (voucher-code-param). Al montar, el adaptador lo lee una vez, lo elimina de la barra de direcciones con history.replaceState (sin recarga, el resto de la URL se conserva) y canjea el código. Una recarga, un marcador o una URL reenviada nunca canjean dos veces. Adecuado para páginas estáticas y la sesión generada en el navegador.
voucherCode en el cuerpo de generación
B) En el servidor: entregar el código con la sesión
Tu backend lee el parámetro de la petición entrante y lo coloca en el cuerpo de generación como voucherCode (eventSlug opcional). El código pasa a formar parte de la sesión y llega al micro-frontend solo mediante el GET /shop/session-context autenticado, que lo entrega una sola vez y luego lo borra; la respuesta de generación devuelve el código aceptado para comprobación. Define también voucherCodeParam para que el parámetro desaparezca igualmente de la barra de direcciones. Adecuado cuando tu página ya se renderiza en el servidor o quieres validar, registrar o limitar tú mismo los enlaces entrantes. Si ambas vías entregan un código, gana el del servidor.
Qué pasa con el código
Mismo comportamiento en ambas vías
El código se canjea exactamente una vez y solo en un recorrido nuevo (un recorrido reanudado tras una recarga o un retorno de pago lo ignora). Con startMode "voucher", el formulario de código funciona como se describe en «Canjear vale / registrar entrada». Con el inicio en artículos, el código se aplica al primer carrito mientras la vista, el desplazamiento y las cantidades se mantienen. Continuar añade esas cantidades a ese mismo carrito. El micro-frontend nunca pasa el código a onEvent. Sin eventSlug se canjea tras elegir un evento.
voucher-link.ts
// Voucher links point to your site: https://your-site.com/tickets?code=ED24-ADITUS// The route, request and authentication below are examples for your own website.// The parameter name is up to you: here "code".// Option A (browser): the micro-frontend reads the parameter itself.import { mount } from"@aditus/shop-embed";
mount(document.getElementById("aditus-shop")!, {
publicKey: "pk_your_site",
sessionToken, // session with eventSlug (article or voucher start)
voucherCodeParam: "code", // read ?code=… once, remove it from the address bar, redeem it
});
// Web Component: <aditus-shop public-key="pk_your_site" voucher-code-param="code"></aditus-shop>// Option B (server): your backend hands the code over when it creates the session.// (Express example; the code never goes into the session response, only into the session context.)
app.get("/tickets", async (req, res) => {
const code = typeof req.query.code === "string" ? req.query.code : undefined;
const r = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`, "Content-Type": "application/json" },
body: JSON.stringify({
publicKey: "pk_your_site",
email: req.user?.email,
eventSlug: "ExperienceDaysv52024",
startMode: "voucher", // or omit: article start, the code lands in the first cart
voucherCode: code, // undefined = normal start without a code
}),
});
const { sessionToken }: { sessionToken: string } = await r.json();
// The page additionally mounts with voucherCodeParam: "code" so that ?code= disappears// from the address bar (the session code wins, it is redeemed only once).
res.render("tickets", { sessionToken });
});
Opcional: reservar el vale al generar la sesión en el servidor
Si quieres tratar el rechazo antes de abrir el micro-frontend, pon redeemVoucherOnMint: true en el POST /api/shop/session de confianza. A diferencia de la entrega normal del código, se crea un carrito para el usuario autenticado y se reserva realmente el vale antes de emitir la sesión: no es una comprobación extra. Si funciona, pasa solo sessionToken al navegador. El micro-frontend retoma el mismo carrito sin crearlo ni canjear dos veces, tanto en modo voucher como articles. Sin la opción se aplica la entrega normal de voucherCode.
Este modo requiere voucherCode, eventSlug fijo, un usuario identificado (email; no anónimo) y la cabecera Idempotency-Key. Solo está disponible en el backend con secreto de generación, no en POST /shop/embed-session en el navegador. Admite códigos de vale/entrada, no PromotionCode. El código queda reservado en un carrito; la entrada no está registrada ni emitida. Deben completarse registro y compra, y comprobaciones posteriores pueden fallar.
prepare-voucher.ts
// Server side of your website: one voucher attempt, no additional check call.// You implement the authentication and storage of this attempt yourself.// attemptKey is a random id your backend stores durably per logical redemption// attempt (e.g. crypto.randomUUID()). On network errors, resend the same id// and the SAME body; never generate a new one before a retry.asyncfunction prepareVoucherSession(
user: { email: string },
code: string,
attemptKey: string,
) {
const response = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
"Content-Type": "application/json",
"Idempotency-Key": attemptKey,
},
body: JSON.stringify({
publicKey: "pk_your_site",
email: user.email, // only from your authenticated server session
eventSlug: "ExperienceDaysv52024",
startMode: "voucher", // or "articles"
voucherCode: code,
redeemVoucherOnMint: true,
}),
});
const result: {
sessionToken?: string; expiresAt?: number; voucherRedeemed?: boolean;
error?: string; reason?: string; upstreamStatus?: number;
} = await response.json();
if (response.ok && result.sessionToken && result.voucherRedeemed === true) {
// Only now pass it to the browser and mount the embed.return { sessionToken: result.sessionToken, expiresAt: result.expiresAt };
}
if (response.status === 422 && result.error === "voucher_rejected") {
// Your own UI: handle the business rejection using reason, do not open the embed.// Translate only documented reason values into your own texts.return { rejected: result.reason };
}
if (response.status === 409 && result.error === "voucher_preparation_uncertain") {
// Uncertain stays uncertain; a repeated POST never turns it into a success.// If the Shop API cannot provide the cart id, manual clarification is needed.
throw new Error("Redemption uncertain; clarify with ADITUS, do not book again.");
}
if (response.status === 410 && result.error === "voucher_preparation_expired") {
throw new Error("Prepared session no longer valid; no blind new session.");
}
// 400 = invalid request/key, 409 = conflict/attempt in progress,// 502/503 voucher_upstream_error = infrastructure, not a business error.
throw new Error(`Session creation not completed (${response.status})`);
}
422 voucher_rejected con reason saneado es un rechazo de negocio: muéstralo en TU interfaz, no montes el micro-frontend sin token. 400 indica configuración/clave incorrecta; 409 idempotency_conflict es reutilizar la clave con datos diferentes. Un intento pendiente/incierto, 502/503 o un timeout no son un vale inválido. Guarda Idempotency-Key aleatorio por intento lógico en tu servidor; reintenta solo con la misma clave y datos idénticos. Puede existir ya una reserva. No registres códigos, emails, errores sin filtrar ni tokens.
Una sesión emitida puede dejar un carrito reservado aunque nunca se abra la página. La limpieza de carritos conocidos tras rechazo/revocación se intenta sin garantía; un proceso al arrancar y cada 60 segundos reintenta hasta 25 registros por ciclo. La misma clave y los mismos datos repiten el resultado terminado aún válido. Si el token original caducó, se revocó, completó, borró o transfirió, la respuesta es 410 voucher_preparation_expired: no recrees la reserva a ciegas. Un sucesor autorizado puede ampliarla/transferirla al acceder al carrito por primera vez. Borrar la URL no libera nada.
Errores exactos: 400 invalid_redeem_voucher_on_mint / voucher_preparation_requires_user_event_code / invalid_idempotency_key (o validación normal); 422 voucher_rejected contiene reason y upstreamStatus, nunca el código o mensaje bruto upstream. Motivos posibles: unknown · articleLimit · exhausted · ownership · used · reserved · limit · cartLimit · locked · canceled · otherEvent · legitimation · rejected · unsupported_code_type.
409 idempotency_conflict indica datos cambiados; voucher_preparation_pending incluye Retry-After: 2 segundos y voucher_preparation_uncertain sigue incierto tras reintentar (no hay éxito automático). 410 voucher_preparation_expired impide repetir la preparación original. 502/503 voucher_upstream_error significa fallo upstream. Si se creó un carrito pero no se recibió su ID, la API disponible no permite conciliarlo ni liberarlo automáticamente: se guarda el estado incierto y no se crea otro carrito al reintentar. Consulta a ADITUS manualmente antes de iniciar un nuevo intento. Si falla session-context, el micro-frontend muestra error/reintento sin crear un carrito nuevo.
Prueba las tres variantes en vivo en el Probador de integración: genera una sesión con eventSlug, elige startMode o un surtido, monta y observa el registro de peticiones. El sistema de demostración acepta el vale de prueba ED24-ADITUS en ExperienceDaysv52024 (cada canje reserva la entrada unos 20 minutos; volver atrás la libera). Abrir el Probador de integración
Opcional: navegar de forma anónima, iniciar sesión en el pago
No tienes que saber de antemano quién es el usuario. Monta con una sesión de invitado y el micro-frontend permite a un visitante anónimo explorar eventos, elegir artículos y llenar el carrito. Solo cuando avanza más allá del carrito (hacia el registro y el pago) te pide definir el usuario real, mediante el callback onUserRequired. El resto del flujo continúa entonces con el carrito preservado.
1
Montar con una sesión de invitado
Sigue siendo necesario un token de sesión para activar la tienda, pero puede ser una sesión de invitado: simplemente genérala sin el campo email, sin necesidad de una dirección de marcador de posición. El visitante navega y llena el carrito bajo ella.
2
Salir del carrito activa onUserRequired
En el momento en que el visitante avanza más allá del carrito, el micro-frontend llama a tu callback una vez y muestra un estado de espera neutro. Autentica al usuario (login/SSO), luego genera una nueva sesión ligada al usuario exactamente igual que la primera: servidor a servidor con tu secreto. La definición posterior del usuario también pasa por esa creación de sesión entre servidores. El secreto nunca llega al browser y el usuario nunca se define en el código cliente. Tu callback solo retransmite el token opaco resultante. Fundamental: tu backend deriva esa identidad de SU propia sesión autenticada (cookie/JWT); nunca debe aceptar el correo o el identificador de usuario desde el browser, o un visitante malicioso podría solicitar la sesión de otra persona y leer sus datos personales mediante el prellenado.
3
Devuelve el nuevo token. El carrito se conserva
Devuelve el nuevo token de sesión y el micro-frontend lo adopta y liga al usuario identificado al carrito existente. El carrito, sus artículos y cualquier franja horaria se conservan sin cambios. Luego pasa al registro. Devuelve null para cancelar. El visitante simplemente permanece en el carrito, sin error.
4
Omitir onUserRequired para un usuario fijo
Si omites el callback, nada cambia respecto al flujo clásico: monta con una sesión ligada a un usuario y el micro-frontend actúa como ese usuario desde el primer paso.
page.ts
import {
mount,
type ShopHandle,
type AditusShopConfig,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// A guest session token is enough to mount: created server-to-server as// usual, just without the email field (no placeholder address needed). The// visitor may browse and fill the cart before a specific user is known.const config: AditusShopConfig = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site",
sessionToken: guestSessionToken,
// Called once as soon as the visitor leaves the cart towards registration.// This is where you authenticate the user (login/SSO) and create a new,// user-bound session server-to-server.// Return the fresh token -> the micro-frontend adopts it and rebuilds the// cart under the identified user (articles are kept). Return null to// cancel -> the visitor stays in the cart, without an error.
onUserRequired: async () => {
// Calls only your backend, with no identity in the payload. Your backend reads// the signed-in user from its own session (cookie/JWT) and creates the session// server-side for exactly this identity. Never pass the email from the browser,// otherwise an attacker could insert someone else's address here and reach// that person's personal data through the prefill.// host.loginAndMintSession(): Promise<string | null> is your own function.// Implement login/SSO + authenticated backend call + server-side session creation on your website.// On cancel the function returns null; on failure it throws explicitly.return host.loginAndMintSession();
},
};
const handle: ShopHandle = mount(el, config);
La autenticación se hace en tu sitio
ADITUS no autentica a tus usuarios finales. Tu sitio es la única fuente de verdad sobre quién es el usuario. Tú operas tu propio login, SSO o sesión. ADITUS solo confía en un token de sesión que tu backend genera con una clave secreta. La cadena de confianza es: tu auth prueba la identidad, tu token generado con el secreto la avala, el micro-frontend la presenta.
1
Tu sitio autentica al usuario
Login, SSO y sesión de miembro son completamente tuyos. ADITUS no interviene aquí.
2
Tu backend genera una sesión
Servidor a servidor con la clave secreta. Pasas como quién debe actuar el micro-frontend (email) y lo ligas a tu publicKey.
3
La página monta el micro-frontend
El token opaco se entrega al browser y se pasa como sessionToken a mount(). El secreto permanece en tu servidor.
4
Cada llamada lleva la sesión
El micro-frontend envía X-Aditus-Session en cada petición; la API de la tienda resuelve el usuario del carrito a partir de la sesión.
5
Sin token → el micro-frontend permanece inactivo
Sin sesión, el micro-frontend nunca se activa: muestra un aviso neutro y no realiza llamadas. Ni el carrito ni el pago están disponibles. Genera una sesión para activarlo.
1. Generar en tu backend
server.ts
// Server side of your website. The secret key stays on the server and never reaches the browser.// You build user and the login/session route between the browser and your server yourself; ADITUS does not provide them.// Your backend has already authenticated the user itself (login/SSO/session).// Important: user comes from this server session, never from the browser's// request body. Otherwise an attacker could slip in someone else's email.const res = await fetch("https://developers.aditus.com/api/shop/session", {
method: "POST",
headers: {
// Your client's mint secret (generated in the ADITUS admin), server-side only.
Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
publicKey: "pk_your_site", // binds the session to your key (required)
email: user.email, // who the micro-frontend acts as (omit = anonymous guest session)
externalUserId: user.id, // optional metadata (your user id)// sharedCartKey: visitor.stableRef, // required for anonymous shared carts (default):// opaque reference from your trusted server session, never browser input
ttlSeconds: 1200, // optional: lifetime (default 20 min, max 24 h)
eventSlug: "ExperienceDaysv52024", // optional: start directly in this event (otherwise the event overview)// startMode: "voucher", // optional: start with the voucher/ticket code form (without eventSlug after the event selection)// assortment: "Pressetickets", // optional (with eventSlug): only this assortment (id or name)// voucherCode: req.query.code, // optional: redeem a code from a voucher link in the embed// redeemVoucherOnMint: true, // optional: reserve it here instead (requires email, eventSlug and Idempotency-Key)
}),
});
// Choose the response type freely and pass sessionToken to the browser (inline or via fetch).const { sessionToken }: { sessionToken: string; expiresAt: number } = await res.json();
2. Montar con el token
page.ts
import {
mount,
type ShopHandle,
type AditusShopConfig,
} from"@aditus/shop-embed";
// getElementById can return null. Check it properly in TypeScript instead of using "!".const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
// AditusShopConfig types every option and gives you autocompletion.const config: AditusShopConfig = {
apiBase: "https://developers.aditus.com/api/shop",
publicKey: "pk_your_site", // required as soon as a session is involved
sessionToken, // issued by your backend, keep it in memory only
cartScope: "shared", // default; requires a trusted server identity
};
const handle: ShopHandle = mount(el, config);
// The micro-frontend sends the X-Aditus-Session header with every shop call.// On logout, revoke the session on the server and remount/update.
3. Revocar al cerrar sesión
server.ts
// Server side, e.g. when your user logs out.// You build the logout route between the browser and your server yourself; only your backend calls ADITUS.await fetch(`https://developers.aditus.com/api/shop/session/${sessionToken}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}` },
});
// { revoked: true }
El pedido finalizado vuelve a tu página
Cuando se completa una compra de entradas, no necesitas una consulta de pedido aparte. Como el micro-frontend se ejecuta de forma nativa en tu página (light DOM, no un iframe), devuelve el pedido finalizado directamente a tu código mediante el callback onComplete. Se trata de una llamada JS directa, sin postMessage. El micro-frontend ya ha resuelto el pedido por ti, así que recibes el número de pedido y los enlaces de las entradas, no solo un id.
mount.ts
import {
mount,
type ShopHandle,
type CompletedOrder,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
const handle: ShopHandle = mount(el, {
publicKey: "pk_your_site",
sessionToken,
// Called exactly once as soon as the order is placed.// The micro-frontend has already resolved the order for you. You get// the resolved order, not just an id.
onComplete: (order: CompletedOrder) => {
order.id; // order GUID for further processing (server, reconciliation)
order.number; // order number for display
order.status; // order status
order.buyer; // buyer (name / email), if present
order.tickets; // [{ name, links: [{ kind: "pdf" | "apple" | "google", url }] }]// -> store order.id, show a confirmation, trigger DOI / newsletter ...
},
// Called on every unrecoverable error in the flow.
onError: (err: Error) => {
console.error("Shop error:", err.message);
},
});
Qué recibes
onComplete se dispara una vez por cada pago exitoso con el pedido resuelto: id (el GUID del pedido, tu clave para el procesamiento posterior), number (el número de pedido para mostrar), status, buyer (nombre y correo cuando están presentes) y tickets. Cada entrada lleva enlaces tipados de kind “pdf”, “apple” o “google”. onError se dispara ante cualquier error irrecuperable del flujo.
Métodos de pago con redirección
Los métodos que permanecen en la página (factura, prepago) disparan onComplete de inmediato. Un método que envía el browser a una página de pago externa (p. ej. Saferpay) es gestionado por el micro-frontend de principio a fin: deriva las URL de retorno de la página en la que se ejecuta, envía al usuario fuera y, a la vuelta, finaliza el pedido y dispara onComplete. Este viaje de ida y vuelta completo no requiere código por tu parte. Solo si el retorno debe aterrizar en una página distinta sobrescribes redirectUrls.successUrl / cancelUrl / errorUrl.
Eventos GA4 / e-commerce (opcional)
El micro-frontend puede reportar el embudo de compra a tu analítica, pero solo si se lo pides. No incluye gtag, ni GTM, ni SDK de GA, no carga nada, no establece cookies y no envía nada por su cuenta. Añades un callback opcional, onEvent, y recibes eventos tipados ya formados según el esquema GA4 Enhanced Ecommerce de Google. Omite onEvent y nada cambia. Qué herramienta alimentas, y si tienes consentimiento para enviar, queda enteramente de tu lado.
mount.ts
import {
mount,
type ShopHandle,
type ShopAnalyticsEvent,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
const handle: ShopHandle = mount(el, {
publicKey: "pk_your_site",
sessionToken,
// Optional. If you leave out onEvent, nothing changes: the micro-frontend// sends nothing, loads no gtag/GTM/GA SDK and sets no cookies.// Tool, consent and mapping stay entirely with you. This example uses GA4 via gtag.
onEvent: (event: ShopAnalyticsEvent) => {
// Consent is yours: only send once the user has agreed.
if (!hasAnalyticsConsent()) return;
// The events already follow the GA4 schema (name + items/value/currency ...),// so pass them to gtag 1:1.const { name, ...params } = event;
window.gtag?.("event", name, params);
},
});
Ya con el formato GA4
Cada evento lleva un nombre de evento GA4 más sus parámetros estándar: items (con item_id, item_name, price, quantity, item_category y, cuando aplica, discount y coupon), value y currency; add_payment_info añade payment_type y purchase añade transaction_id. Extrae name por desestructuración y pasa el resto directamente a gtag("event", name, params), sin necesidad de remapear.
El consentimiento sigue siendo tuyo
onEvent es simplemente una función JavaScript normal de tu página que llamamos directamente. El micro-frontend en sí no envía analítica a ningún sitio. Nada sale de la página hasta que tu handler lo envía, así que tu gestión de consentimiento mantiene el control total: condiciona el reenvío al consentimiento, enruta al dataLayer de GTM en lugar de gtag, agrupa o descarta eventos por completo. Cualquier error que lance tu handler se captura y se aísla, de modo que nunca rompe el flujo del usuario. Mantén el handler ligero (un push a gtag/dataLayer) ya que se ejecuta inline.
EventoSe dispara cuando
view_item_listEl usuario ve la lista de artículos (se dispara una vez por conjunto mostrado).
add_to_cartSe añade un artículo o complemento al carrito.
remove_from_cartSe elimina una línea del carrito o un complemento.
view_cartSe muestra la vista del carrito (una vez por carrito).
begin_checkoutEl usuario abandona el carrito hacia el registro / la compra.
add_payment_infoSe elige un método de pago (contiene payment_type).
purchaseEl pedido se realiza (contiene transaction_id, value, items).
Bus de eventos: handle.on / handle.off (opcional)
Además de los callbacks de config, el handle devuelto por mount() lleva un pequeño bus de eventos totalmente opcional. Suscríbete con handle.on(event, listener) (devuelve la función de baja correspondiente) y desconéctate con handle.off(event, listener). Si nunca llamas a on(), nada cambia: el bus no añade dependencias, no envía nada y no cuesta nada. Es pura observación para tu página (una insignia de mini-carrito, un banner de confirmación, tu propio logging) y complementa los callbacks en lugar de reemplazarlos.
mount.ts
import {
mount,
type ShopHandle,
type ShopCartUpdate,
} from"@aditus/shop-embed";
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");
const handle: ShopHandle = mount(el, {
publicKey: "pk_your_site",
sessionToken,
});
// Optional. Without on() the micro-frontend works completely normally.// The bus is pure observation; no event is required.// on() returns the unsubscribe function; alternatively handle.off(name, fn).const offCart = handle.on("cart:update", (cart: ShopCartUpdate) => {
// On every cart change: count, total, line items (GA4 item shape).
updateMiniCartBadge(cart.itemCount); // e.g. a badge in the page header
});
handle.on("checkout:complete", ({ order }) => {
// The order is placed. You receive the same resolved order as in onComplete.
showConfirmationBanner(order.number);
});
handle.on("analytics", (event) => {
// Mirror of the GA4 funnel (identical payloads to config.onEvent).
console.debug("Funnel:", event.name);
});
// Unsubscribe selectively later; the rest stays active:
offCart();
EventoSe dispara cuando
cart:updateEl carrito cambia: creado, artículo añadido o retirado, o vaciado. Contiene cartId, itemCount, value, currency y las líneas como items con formato GA4.
phase:changeEl paso visible ha cambiado. El evento contiene phase (events · home · articles · voucher · cart · registration · legitimation · buyer · payment · summary · done, mismo vocabulario que data-step), previous (null en el paso de entrada) y startPhase, el paso de entrada de la sesión. Así tu página controla sus propios botones Atrás / Reiniciar sin leer hooks CSS.
checkout:completeEl pedido se realiza. Se emite justo después de config.onComplete con el mismo objeto de pedido resuelto, antes de que el micro-frontend muestre su página de confirmación. Llama a preventDefault() de forma síncrona para mostrar tu propia página de confirmación (ver «Tu propia página de confirmación»).
analyticsReflejo del funnel GA4 anterior: cada payload de onEvent también se emite en el bus, sin cambios.
Los listeners sobreviven a update()
Las suscripciones viven en el handle, no en un render: handle.update({ ... }) vuelve a renderizar el micro-frontend pero mantiene cada listener adjunto. El desmontaje mediante handle() desconecta todos los listeners automáticamente. Solo necesitas off() (o la función de baja devuelta) cuando quieres dejar de escuchar mientras la tienda sigue funcionando.
Aislado, nunca bloqueante
Un listener que lanza un error se captura y se aísla. Nunca rompe el flujo del usuario y nunca afecta a otros listeners. El evento “analytics” refleja el embudo GA4 con payloads idénticos a config.onEvent, así que puedes consumir el embudo mediante el bus, el callback o ambos; se aplica la misma regla de consentimiento: nada sale de la página hasta que tu código lo envía.
Tu propia página de confirmación
checkout:complete se emite antes de que el micro-frontend muestre su propia página de confirmación. Si tu página muestra su propia confirmación, llama a preventDefault() de forma síncrona en el listener: event.preventDefault() en el elemento <aditus-shop>, preventDefault() del payload con handle.on(), o completion.preventDefault() del segundo argumento de config.onComplete. El micro-frontend no muestra entonces ninguna página de confirmación, no hace scroll y no muestra ningún paso más; tu página toma el control. El pedido se realiza en cualquier caso. La llamada debe hacerse en el propio listener, no después de un await. Sin ella, el micro-frontend muestra su página de confirmación como antes. Esto también se aplica al regreso desde una página de pago externa.
Termina después la sesión. El pedido está completado, así que tu servidor puede revocar la sesión en el mismo paso con DELETE /api/shop/session/:token y tu clave secreta (ver «Revocar una sesión»). El pedido realizado y los enlaces de las entradas del objeto de pedido siguen siendo válidos, porque los enlaces no dependen de la sesión. Para otra compra, genera una nueva sesión y vuelve a montar el micro-frontend. El micro-frontend no termina la sesión por sí mismo, porque la revocación requiere tu clave secreta. Conserva la sesión solo si tu página sigue vendiendo deliberadamente en ella.
Órdenes: goTo y back
Además de observar, tu página puede enviar dos peticiones acotadas al Micro Frontend. Ambas devuelven true si la tienda cambió de paso y false en caso contrario; nunca vuelven a montar el componente ni pierden el recorrido. goTo("cart") muestra el paso del carrito para tu propio minicarrito; solo se acepta en el paso de registro mientras aún no se ha escrito nada. back() retrocede un paso, exactamente como el botón Atrás de la tienda. Devuelve false allí donde ese botón falta o está desactivado: en el paso de entrada (startPhase), en la página de confirmación y mientras un pago u otro cambio está en curso. Ambas existen en el handle de mount y en <aditus-shop> (element.back(), element.goTo("cart")).
browser-back.ts
import type { ShopHandle } from"@aditus/shop-embed";
// Optional: the browser's back gesture goes one step back in the shop.// The micro-frontend itself never writes to the browser history. Your page// keeps its own history entry while the shop is past its entry step.exportfunction connectBrowserBack(shop: ShopHandle): () => void {
let inside = false; // after the entry step and before the completion pagelet entry = false; // our history entry is on topconst offPhase = shop.on("phase:change", ({ phase, startPhase }) => {
inside = phase !== startPhase && phase !== "done";
if (inside && !entry) {
history.pushState({ aditusShop: true }, "");
entry = true;
} else if (!inside && entry) {
entry = false;
history.back(); // entry no longer needed
}
});
const onPopState = () => {
if (!entry) return; // normal navigation of your page
entry = false;
// false: the shop's back button is missing or locked (e.g. payment in progress).
if (!shop.back() && inside) {
history.pushState({ aditusShop: true }, "");
entry = true;
}
};
window.addEventListener("popstate", onPopState);
return () => {
offPhase();
window.removeEventListener("popstate", onPopState);
};
}
// Your own mini cart: back to the cart step, otherwise scroll to the shop.exportfunction showCart(shop: ShopHandle, container: HTMLElement): void {
if (!shop.goTo("cart")) container.scrollIntoView({ behavior: "smooth" });
}
El Micro Frontend nunca escribe por sí mismo en el historial del navegador: las URL y el router pertenecen a tu sitio. El ejemplo mantiene exactamente una entrada de historial adicional mientras la tienda está más allá del paso de entrada, así que el gesto Atrás del navegador (también el deslizamiento en iPhone) retrocede primero un paso dentro de la tienda y solo sale de la página desde el paso de entrada. Si tu sitio usa un router del lado del cliente (React Router, Next.js, Vue Router), crea esa entrada a través del router en lugar de history.pushState para que no la trate como una navegación ajena.
publicKey vs. sessionToken
Mantén los dos separados. Responden a preguntas diferentes y se emparejan, no son intercambiables.
publicKey: confianza de origen
Publicable. Se entrega en tu código cliente, así que no es un secreto y no concede nada por sí sola. El backend aplica una lista blanca de dominios por clave: una clave solo funciona desde sus dominios registrados. Responde a «¿qué sitio es este?» Solo una opción explícita y deliberadamente insegura, fijada por el operador para un cliente concreto, puede levantar esta lista blanca; el estado activo, la obligación de sesión y los secretos siguen aplicándose.
sessionToken: identidad
Generado con el secreto en tu backend, opaco y de corta duración. Responde a «¿quién es el usuario?» Una sesión está ligada al publicKey para el que se generó: una petición de sesión también debe enviar esa clave, y una discrepancia se rechaza (403).
Reglas de seguridad
La clave secreta vive solo en tu servidor, nunca en el código cliente, los bundles o los archivos env enviados al browser.
No guardes tú el token en localStorage ni en cookies; mantenlo en memoria y renuévalo creando una nueva sesión. Para recargas y el viaje de ida y vuelta del pago, el micro-frontend lo guarda él mismo en el sessionStorage de la pestaña.
Los tokens son de corta duración (20 minutos por defecto, 24 horas máx) y mono-usuario.
Revoca la sesión al cerrar sesión: un token revocado se rechaza de inmediato (401). Hasta que expire o se revoque, un token válido es un bearer token. Trátalo como tal.
Empareja siempre la sesión con su publicKey. Una clave ausente o que no coincide se rechaza (403).
Una sesión no válida o caducada devuelve 401. No hay recurso a otro usuario.
Qué puede hacer un token de sesión
Un token de sesión es un bearer token. Este es el alcance exacto que la API de ADITUS aplica. Utiliza esta información para tu modelo de amenazas y revisión de seguridad.
Token y vigencia
Aleatorio de 256 bits, opaco, no contiene datos y no se puede decodificar. 20 minutos por defecto, configurable de 1 minuto a 24 horas al generarlo. Una sesión nunca se prolonga. Renuévala generando una nueva.
Vinculación
Vinculada a la publicKey para la que se generó. Cada petición de sesión debe enviar exactamente esa clave, y el origen de la petición se comprueba contra la lista blanca de dominios de la clave. Otra clave u otro origen se rechaza (403).
Sesión anónima
Generada sin usuario: puede explorar eventos y crear y editar su propio carrito. El registro, las franjas horarias, la asignación de pago y el checkout se rechazan en el servidor (403) hasta que tu página la sustituye por una sesión identificada.
Sesión identificada
Además: registro, franjas horarias, asignación de pago y checkout, siempre limitado a los carritos que posee esta sesión. Con protectUserData los datos del comprador almacenados para el usuario están protegidos: una escritura que los cambiaría se rechaza (403).
Propiedad del carrito
Un carrito está vinculado a su propietario mediante un hash con clave (HMAC): en una sesión anónima al propio token, en una sesión identificada a la identidad de usuario generada por tu backend, por publicKey. Las sesiones de otros usuarios no tienen acceso a él y las escrituras en un carrito se serializan.
Datos personales
El contexto de sesión devuelve indicadores, identificadores y el identificador de usuario generado por tu propio backend. Tu sitio ya conoce toda esta información. Los datos del comprador solo son accesibles mediante las rutas de carrito y registro, que comprueban la propiedad.
Límites de tasa
Valores predeterminados: generación y revocación 300 por minuto por cliente e IP, autocompletado de dirección, empresa y ciudad 60 por minuto por sesión y 3000 por minuto por cliente en todas las sesiones. Cada límite es un presupuesto propio de su cliente; otros clientes nunca lo afectan. ADITUS puede establecer límites individuales por cliente a petición.
Registro de auditoría
Las peticiones API proxificadas se registran y conservan durante 90 días: cliente, ruta normalizada, estado, host de origen, hash abreviado del token de sesión e id de carrito o pedido. Nunca el token en sí, ni correo, ni dirección IP.
Revocación
DELETE /api/shop/session/:token invalida la sesión de inmediato. No hay caché de por medio; la siguiente petición con ese token recibe un 401. Llámalo al cerrar sesión.
Referencia de endpoints y respuestas
POST/api/shop/sessiongenerar con secreto Bearer
DELETE/api/shop/session/:tokenrevocar con secreto Bearer
Ambas llamadas proceden de tu backend: se autentica con el mint secret de tu cliente (generado en la consola de administración de ADITUS) como token Bearer. Una sesión por inicio de sesión es el patrón normal; el navegador solo ve el token resultante. Los sitios sin backend no usan estos endpoints: el elemento crea su propia sesión de navegador mediante POST /api/shop/embed-session, restringido por dominio (public-key con cart-scope="tab").
Puedes probar este mismo flujo con tus propias credenciales en el probador de integración. Genera una sesión, monta la tienda y sigue el recorrido.
Generar una sesión: petición
CabeceraValorSignificado
AuthorizationBearer <mint-secret>El secreto de mint de tu cliente, generado en la consola de administración de ADITUS. Solo en el servidor. Nunca debe enviarse a un navegador o a un bundle de aplicación.
Content-Typeapplication/jsonEl cuerpo es un objeto JSON.
Idempotency-Keystring · 8–200 visible ASCII chars · required with redeemVoucherOnMintObligatorio si redeemVoucherOnMint es true: 8–200 caracteres ASCII visibles. Clave estable para un intento de canje; guárdala en el servidor y reutilízala con exactamente la misma petición al reintentar. Otra petición con la misma clave devuelve 409 idempotency_conflict. No envíes el secreto ni esta clave desde el navegador.
Campo del bodyTipoSignificado
sharedCartKeystring · opcionalLas sesiones identificadas creadas de servidor a servidor normalmente comparten mediante la identidad de confianza. Para carritos anónimos compartidos, el backend DEBE crear la sesión con sharedCartKey: una referencia opaca y estable de visitante obtenida de su propia sesión de confianza, no un correo. Es un campo de sesión del servidor, no configuración del navegador ni atributo HTML. Nunca aceptes correos, identidades o sharedCartKey arbitrarios del navegador. Una sesión creada en el navegador no permite compartir con seguridad; shared sin identidad de confianza devuelve un error explícito de no compatibilidad, también si se omite el ámbito. El aislamiento explícito 'tab' no requiere sharedCartKey; no hay alternativa silenciosa.
publicKeystring · obligatorioLa clave publicable para la que se genera la sesión. Debe ser la misma clave con la que monta el micro-frontend. Cada llamada de tienda posterior se comprueba contra ella (403 si no coincide). El secreto Bearer debe pertenecer al cliente de esta clave.
emailstring · optionalEl usuario de la tienda como el que actúa la sesión. Tómalo de TU sesión de servidor autenticada (cookie de login/SSO o JWT), nunca de la petición del navegador, o un visitante podría obtener la sesión de otra persona. Si se omite, se genera una sesión anónima: el visitante puede navegar y llenar el carrito, pero el micro-frontend bloquea el paso más allá del carrito hasta que tu callback onUserRequired proporcione una sesión ligada a un usuario (ver el flujo de usuario opcional). Un correo presente pero mal formado sigue siendo rechazado (400).
externalUserIdstring · opcionalTu referencia estable de usuario desde tu sesión autenticada del servidor. Se guarda con la sesión y delimita la identidad si está presente; no la cambies al renovar ni la aceptes de entradas arbitrarias del navegador. No autentica por sí sola.
ttlSecondsnumber · optionalDuración de la sesión en segundos. Por defecto 20 minutos, con un tope de 24 horas. Al expirar, las llamadas de tienda devuelven 401 session_expired. Emite entonces un token nuevo; con refreshSession en la config, el micro-frontend lo solicita automáticamente.
eventSlugstring · optionalFija el punto de entrada del recorrido en el servidor: definido, el micro-frontend arranca directamente en la selección de artículos de este evento; omitido, arranca en el resumen de eventos. Como forma parte de la sesión generada, el navegador no puede manipularlo. Caracteres inválidos devuelven 400 invalid_event_slug; con un slug sin evento en vivo (404 event_not_found) o un evento cerrado / fuera de venta (409 event_not_sellable) la generación falla deliberadamente: sin sesión, sin volver al resumen de eventos.
startMode"articles" Vista de entrada del recorrido. "articles" (predeterminado) abre la selección de artículos; "home" abre la página de inicio del evento con sus accesos (entradas, vale, surtidos publicados); "voucher" abre primero el formulario de código de vale/entrada. El formulario de vale funciona como se describe en «Canjear vale / registrar entrada». Sin eventSlug primero aparece la selección de eventos y después el modo se aplica al evento elegido.
assortmentstring · optionalRestringe el inicio en artículos a un surtido del evento fijado (requiere eventSlug; sin efecto con startMode "voucher"). Indica el id del surtido o su nombre exacto (sin distinguir mayúsculas); se permiten surtidos ocultos. Un valor sin coincidencia muestra un error explícito en el micro-frontend en lugar de volver en silencio a la lista completa. Máx. 200 caracteres, si no 400 invalid_assortment.
voucherCodestring · optionalCódigo de vale/entrada transmitido con la sesión (ambos modos). Por defecto el micro-frontend lo canjea en un recorrido nuevo, tras elegir el evento si no está fijado; un rechazo se muestra en el formulario o en un aviso del primer carrito. Con redeemVoucherOnMint: true se reserva en el servidor antes de responder, para un evento fijo y un usuario identificado. 1–100 caracteres sin espacios ni delimitadores de URL, si no 400 invalid_voucher_code.
redeemVoucherOnMintboolean · optional (default false)Pon true solo para la generación servidor a servidor con voucherCode, eventSlug fijo, email identificado e Idempotency-Key. El servidor crea un carrito ligado al usuario y reserva realmente el vale antes de emitir la sesión. El micro-frontend utiliza el mismo carrito sin crearlo ni canjear de nuevo; funcionan ambos modos. Solo códigos de vale/entrada, no PromotionCode. Sin la opción se aplica la entrega normal de voucherCode. Siguen siendo necesarios el registro y la finalización.
Generar una sesión: respuesta (200)
CampoTipoSignificado
sessionTokenstringToken opaco (sess_…). Entrégalo al navegador y pásalo a mount() como sessionToken; el micro-frontend lo envía como X-Aditus-Session en cada llamada de tienda. No contiene datos de usuario y no se puede decodificar.
expiresAtnumberVencimiento como marca de tiempo Unix en milisegundos. Puramente informativo para tu propia planificación. El micro-frontend reacciona por sí mismo al 401.
voucherRedeemedboolean · only with redeemVoucherOnMinttrue si la generación con reserva del vale tiene éxito. La sesión ya posee un carrito y una reserva que el micro-frontend retoma. El registro de la entrada no está terminado.
Revocar una sesión
DELETE /api/shop/session/:token con el mismo secreto Bearer. El secreto debe pertenecer al cliente para el que se generó la sesión. Llámalo al cerrar sesión para que el token muera junto con tu propia sesión. Respuesta: { "revoked": true } (o false si la sesión ya había expirado). La revocación es idempotente y se puede enviar en fire-and-forget.
Lecturas servidor a servidor
Un backend que controla el micro-frontend (una shell de quiosco, una tienda in situ) suele necesitar datos del tenant antes de generar la sesión: qué eventos existen y están en venta, qué idiomas habla la tienda, qué listas de referencia y qué branding aplican. Cuatro endpoints de lectura responden con las mismas credenciales que la generación (secreto Bearer + cabecera X-Aditus-Public-Key, mismo límite de peticiones, Cache-Control: no-store). Son proyecciones filtradas de la Shop API, nunca un reenvío en bruto; ?language= (o Accept-Language, por defecto de) elige el idioma de los nombres. Errores: 400 invalid_public_key, 401 unauthorized, 503 shop_api_not_configured, 502 upstream_error.
EndpointDevuelve
GET /api/shop/events?language=deTodos los eventos del tenant: id, slug, name, year, start/end, location, saleState (open · registration_only · closed), los indicadores brutos, showInOverview, languages, URL de icono/logo. De aquí tomas el slug para generar la sesión.
GET /api/shop/configurationDatos del tenant: mandator, los nombres de las listas de referencia (tratamientos, países, títulos, consentimiento de cookies) y websiteMetricTracking. Las credenciales de proveedores de inicio de sesión nunca se incluyen, a propósito.
GET /api/shop/languagessystemLanguage más la lista de idiomas de la tienda ({ code, name }). Estos son los valores que acepta el atributo culture del micro-frontend.
GET /api/shop/branding?images=ONSITE_SHOP_LOGO,…El tema Shop del tenant: styles como mapa plano clave → valor, las imágenes del tema con URL y (para hasta 40 claves de imagen que indiques (A–Z, 0–9, _ , -)) si la clave existe, su URL pública y su tipo de contenido. Las URL de imágenes no requieren autenticación. Añade &eventSlug=<slug> o &event=<GUID> para acotar el tema y las claves a un evento (sobrescrituras a nivel de evento, claves solo del evento como ONSITE_SHOP_HEADER / ONSITE_SHOP_ATTRACT_*); la respuesta indica entonces el evento resuelto. Slug desconocido → 404 event_not_found, no es GUID → 400 invalid_event, Shop API no disponible → 503 event_unavailable.
Los errores son explícitos
EstadoCódigoSignificado
503session_not_configuredminting is disabled for this client: no mint secret has been generated in the admin console yet.
401unauthorizedthe mint secret is wrong or missing. Check that the secret belongs to the client of exactly this public key (rotated secrets invalidate the old one immediately).
400invalid_public_keythe public key is malformed or not registered. It must look like pk_… and belong to a configured client.
400invalid_emailthe shop user email is missing or not a valid address.
400invalid_tokenthe session token in the revoke call is malformed. Pass exactly the sessionToken returned by the mint call.
429rate_limitedtoo many mint/revoke calls from your IP. Wait a minute and try again.
403public_key_invalidthe X-Aditus-Public-Key header is malformed. It must look like pk_… exactly as issued during onboarding.
403public_key_unknownthe X-Aditus-Public-Key is well-formed but not registered (or deactivated). Check for typos and that the key's client is active.
403public_key_origin_unresolvedthe request carried a publicKey but no usable Origin/Referer header, so the domain whitelist cannot be checked. Browsers send Origin automatically; server-side calls must not use the publicKey header.
403public_key_domain_not_allowedthe request's origin domain is not on this publicKey's whitelist. Add the domain during onboarding (or in the admin console) before going live on it.
401invalid_sessionthe shop call carried a malformed X-Aditus-Session token. There is no fallback to another user. Pass exactly the sessionToken (sess_…) returned by the mint call.
401session_expiredthe X-Aditus-Session token is unknown, revoked or expired. There is no fallback to another user. Mint a fresh session server-to-server.
403session_requires_public_keya session was sent without its X-Aditus-Public-Key header (origin pinning is mandatory once a session is in play).
403session_key_mismatchthe X-Aditus-Public-Key does not match the key the session was minted for.
403anonymous_sessionthe session was minted WITHOUT an email (anonymous) and only allows browsing and the cart. Registration, payment and checkout require a user-bound session. Create one via onUserRequired.
Ahora hazlo tuyo
La identidad está definida. A continuación, personaliza la marca del micro-frontend. La herramienta de estilo genera una config de tema lista para pegar y explica cada parámetro, con una vista previa en vivo.
¿Trabajas con un agente de programación? Entrégale la guía de integración del Micro Frontend legible por máquinas: copia la URI canónica de abajo y pégasela a tu agente.