May we use Google Analytics 4 to understand which pages and documentation sections are used? Nothing is loaded or sent before you agree. Details in our privacy policy. Privacy policy
The micro-frontend renders the ADITUS ticket shop directly into your page, without an iframe. Sites without a backend load the script and set public-key together with cart-scope="tab". Sites with a backend create the session on their server and pass the session token; your server then decides who the buyer is. Without a session the micro-frontend stays inactive.
There are two ways to a running shop. Both use the same script and the same element; they differ in who creates the session.
Without a backend
The element creates its own browser session for your publicKey, limited to the domains you allowed. Set cart-scope="tab": without a server identity, the default "shared" ends in an explicit error. Each browser tab then has its own cart. The session is anonymous: visitors browse and fill the cart. Before registration, element.config.onUserRequired must return a user-bound session token from a server; without it the shop stops at the cart with a notice.
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>
With a backend
Your server creates the session with the mint secret and decides who the buyer is. Pass the returned token to the element. With a buyer email, the default cart-scope "shared" keeps one cart across all tabs.
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>
Replace pk_your_site with your publishable key from the ADITUS admin. developers.aditus.com is the production address for the script and the API. Both ways are described in detail further down.
The technology behind the micro-frontend
The micro-frontend renders the complete purchase flow (events, articles, cart, registration, payment, completion) into your page's DOM. It uses your page's layout, and your CSS can style every part of it.
React 19, compatible with React 18
The complete flow runs as one type-safe React flow. It is built and tested with React 19 and uses only the React 18 feature set (useState, useEffect, useMemo and similar), so the source package runs with React 18 or newer (peerDependency react >=18). Apart from React it has one runtime dependency: libphonenumber-js for phone number validation. There is no UI library, data-fetching framework or state toolkit. The hosted script from ADITUS already contains React, so your page needs no React of its own.
Light DOM instead of iframe
No iframe, no shadow DOM. The shop renders directly into your page's DOM, fits into your layout and is responsive and accessible. Fonts and colors come from the --aditus-* tokens; the default font is system-ui. Set --aditus-font to use your page's font.
Framework-agnostic: one mount(), everywhere
A single call, mount(container, config), attaches the shop to any DOM element. React, Vue, Angular or plain HTML: the adapter is framework-neutral and works the same everywhere.
The styling is yours
Every element carries a stable CSS hook (BEM classes aditus-shop__* plus data-state). You style it with your own CSS or set brand colors, radii and spacing with a few --aditus-* design tokens. The optional base styles sit in CSS @layer aditus-shop, so your unlayered rules always win. This also applies to generic rules of your site such as button {}, input {} or a {} from a WordPress or Bootstrap theme: they change the shop as well. Limit such rules to your own page areas if the shop should not pick them up.
Multilingual, switchable live
DE/EN are built in. A language switch is pushed straight into the running instance via handle.update() without resetting the user's journey. Cart and progress are preserved.
Secure by design
Without a valid session the shop stays inactive and makes no shop requests. The session comes either from your server (created with the mint secret, which never reaches the browser) or, on sites without a backend, from the domain-restricted browser session for your publicKey. All shop calls run through the ADITUS API, which adds the shop credentials server-side.
Style in three levels
Use the most general level that fits: design tokens establish the brand, general semantic BEM hooks make recurring roles consistent, and existing component-specific hooks are reserved for expert overrides.
General semantic hooks
.aditus-shop__section, .aditus-shop__panel, .aditus-shop__list, .aditus-shop__list-item, .aditus-shop__actions and .aditus-shop__action. These additive hooks describe roles while each element keeps its specialized hook. The Styling Studio's URL and screenshot analysis proposes rules on exactly this level (border vs. shadow, button weight, section spacing), never on component-specific hooks.
Actions and exceptions
Where implemented, data-variant="primary" or data-variant="secondary" documents an action's role. Do not derive semantic roles from DOM nesting: reg-form is not a documented panel. Target its specialized hook only when that form intentionally needs an exception.
The requirements depend on the delivery path. The hosted script from ADITUS needs only a current browser; the source package also needs React 18+ in your app. Both need a session for a publicKey whose whitelist contains your domain.
What is required
Source package only: React 18+ and react-dom 18+ in your app bundle (peerDependency react >=18); mount() uses createRoot from react-dom/client. The hosted script brings its own React.
A browser with a DOM (client-side). The adapter mounts into a real DOM element (light DOM). SSR apps are fine as long as mount() runs on the client.
A current evergreen browser is required. The adapter uses standard web APIs (fetch, AbortController, ResizeObserver, CSS @layer), baseline since ~2022. No IE.
A session for your publicKey. Sites without a backend set public-key together with cart-scope="tab"; the element then creates its browser session itself via the domain-restricted /shop/embed-session. Sites with a backend create the session on their server with the mint secret and pass the token. Without a session the micro-frontend stays inactive. Your domain must be on the publicKey's whitelist.
What is not required
No particular framework. mount(container, config) works with React, Vue, Angular or plain HTML.
No iframe, no shadow DOM. It renders natively into your page.
No particular CSS on your site. The base styles live in @layer aditus-shop at single-class specificity, so any unlayered rule on your site wins. This includes generic element rules such as button, input or a: they also reach the shop's controls, so scope them to your own content if they should not apply there. Box-sizing, padding and spacing are additionally protected against a universal reset such as * { margin:0; padding:0 }. baseStyles: false switches the base styles off.
No other libraries on your site. The source package brings one runtime dependency besides the React peerDependencies (libphonenumber-js for phone number validation); the hosted script contains everything.
No build setup for the hosted script; the source package needs only an ESM import.
Your site is trusted: your server authenticates the user before creating an identified session. The embed deliberately renders in your page's light DOM, without iframe isolation. Scripts on your page can access the DOM and browser storage, so secure your site and its third-party scripts. Public-key origin authorization is not user authentication. Payment credentials are entered only on the external payment provider's page.
Payment security, PCI & CSP
The purchase preparation renders in your page. The payment itself happens on the payment provider's page, so no payment data passes through your page.
No payment data in your page
The complete purchase preparation (event, tickets, add-ons, cart, registration) renders natively in your page. The moment the user starts a redirect payment, the sensitive flow leaves your page entirely: a normal top-level navigation hands over to the payment provider's dedicated, external payment page, and the return trip resumes the journey and completes the order. Card or bank data is entered only there. The micro-frontend never renders, transports or stores payment credentials in your page.
Minimal PCI scope
Because no credit-card or bank data is ever processed, transported or stored in your site, your environment stays in the minimal PCI-DSS scope of the classic redirect model (typically SAQ A; the binding classification is always made by your acquirer or QSA). Payment methods without a redirect, such as invoice, complete server-side, with no payment data in the browser at all.
Content Security Policy
If your site sets a Content Security Policy, add: script-src for the ADITUS origin (hosted script), connect-src for the ADITUS API origin (every shop call goes through it), img-src for the ADITUS API origin (flags, payment logos, wallet badges) and the hosts of your event and article images, and style-src 'unsafe-inline', because the base styles are injected as a <style> element (not needed with baseStyles: false and your own stylesheet). No script-src or frame-src entries for payment providers are needed, because the payment step is a top-level redirect.
Compared with iframe and shadow DOM
Iframe
An iframe is a separate document: your page's styles do not apply inside it, its height has to be synchronised, and it has its own focus order. The micro-frontend renders the purchase preparation as HTML in your page's DOM instead. Only the payment leaves your page, via a top-level redirect to the payment provider's page.
Shadow DOM
Many web components encapsulate themselves in a shadow DOM. Your page's styles then only reach them via custom properties and ::part. The micro-frontend uses the normal CSS cascade instead: its base styles live in @layer aditus-shop, so every unlayered rule of your page wins without higher specificity or !important.
AccessibilitySeparate document with its own focus order and page structure.ID-based ARIA references (aria-labelledby, aria-describedby) do not cross the shadow boundary.HTML in your page's DOM: one focus order and one page structure with your own markup.
Styling & brandingRigid: no CSS inheritance, customization only via postMessage APIs.Global styles do not reach inside; theming only via passed-through properties and parts.Your unlayered CSS wins over the base styles in @layer; the base styles can be overridden or switched off.
Bundle & performanceTypically loads a complete second app including its runtime inside the frame.Often ships its own runtime and duplicate dependencies.Hosted script: one bundle including React. Source package: uses your app's React plus one runtime dependency (libphonenumber-js).
Payment security & PCI scopeStrongly isolated, but at the price of UX, design and responsiveness.Depends on the widget: card fields inside the widget are part of your page.Selection in your page, payment on the payment provider's page.
Our recommendation: JavaScript directly from the ADITUS URL
Load the JavaScript bundle directly from ADITUS. This is our recommended standard for every site. It applies to React and other frameworks as well as HTML and CMS sites. ADITUS maintains the internal shop logic centrally, so compatible improvements reach your integration without you downloading a new package, rebuilding or deploying your site.
The v1 URL delivers compatible releases with stable interfaces. New releases are loaded on subsequent page loads once the current one-hour HTTP cache expires or revalidates; not necessarily on every reload. An already open checkout keeps its loaded code.
Alternative: you manage delivery and updates
Choose the source package, installation of the tarball via npm, or a self-hosted JavaScript copy only if you deliberately want to manage delivery yourself. ADITUS cannot update your copy. You must monitor new versions, download or install them, test and rebuild where needed, and deploy them yourself. The package is not published on the public npm registry.
Alternative: source package (self-delivered)
Alternatively, download the source package from your ADITUS instance and bundle it into your app (Vite, webpack, Next.js or Nuxt). This is self-delivered: you must reinstall, rebuild and deploy updates yourself. It is not on public npm.
What the delivery contains
The complete adapter as readable TypeScript source with type declarations for every config option, callback and event. React and react-dom stay peerDependencies. Your page provides them, so nothing collides in your bundle and there is no second React.
Getting the package
The package is identical for every site and downloads directly from this instance, no credentials required: aditus-shop-embed.tgz. Install directly from this URL (npm install accepts tarball URLs, path /api/embed/v1/aditus-shop-embed.tgz) or unpack into your repo. Onboarding supplies your publicKey and domain whitelist; without the publicKey the embed remains inactive. Contact: contact page. Use this package only if you deliver and update your own build.
Stay current: the v1 track receives compatible updates in place. Check manifest.json regularly (before every release of your app, ideally as a scheduled job) and compare package.sourceHash with aditus.sourceHash in the installed package.json. If they differ, reinstall the tarball and run your build and tests; hooks, tokens and events stay stable within v1. The Web Component bundle updates itself through the one-hour HTTP cache.
Script integration via the ADITUS URL
The prebuilt bundle at https://developers.aditus.com/api/embed/v1/aditus-shop.js works on every site, including apps with a build step, WordPress, Typo3 and plain HTML. One script tag registers <aditus-shop> with React included: no npm or build step of your own required. This public, versioned URL needs no credentials; access to the shop still requires a whitelisted publicKey and session. An already running checkout is never hot-replaced; browser caching can delay a new bundle by up to one hour.
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>
The same hosted JS URL automatically registers <aditus-shop> and exposes window.AditusShop: mount, themeToCss, clearJourney, mintEmbedSession and onEmbedSessionRenewed, with the source package's signatures. Your site does not need React. Hosted mount, element.config and the source package accept the same AditusShopConfig, including themes, eventTheme: false, articleContent and callbacks. Only plain HTML attributes cannot carry functions or objects. The source package remains valid for bundlers and sharing your existing React runtime.
mount(container, config) is the same source mount: without sessionToken it stays inactive and does not create a session itself. Set apiBase explicitly to the full /api/shop URL. After the asynchronous login on your site, your backend creates the session server-to-server and passes only the token to this browser function. host.renewSession and host.revokeSession below are your own functions/routes that you must implement on your authenticated backend, not working ADITUS browser endpoints. Secrets never enter the browser. onUserRequired remains an async login + server-side session callback returning a token or null, available in every JavaScript variant.
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>
Plain JS: browser session, refresh and renewal
For a whitelisted publicKey, mintEmbedSession(publicKey, apiRoot = "/api", { fresh?, staleToken? }) returns Promise<string | null> without a secret. Helpers and the api-base HTML attribute use the API root ending /api; config.apiBase uses the shop base ending /api/shop. onEmbedSessionRenewed(publicKey, apiRoot, listener) passes a token string and returns an unsubscribe function. The example handles failure to create the first session, expired-request refresh, proactive renewal and teardown. Unsubscribe removes this listener only, not shared session caches, renewal timers or server sessions; browser helpers intentionally bridge reloads through 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>
Attributes and full JavaScript config
element.config accepts the full AditusShopConfig, including functions. Defined config keys override matching HTML attributes, even false; omitted/undefined keys leave attributes in effect. Assignments replace all property overrides, not patches: element.config = {} removes them. Assignment before custom-element upgrade is supported. api-base is an API root (/api), whereas config.apiBase ends /api/shop. themeToCss(theme, selector = ".aditus-shop") returns token CSS as an alternative to a theme object.
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>
AttributeMeaning
cart-scopeAditusShopConfig.cartScope?: 'tab' | 'shared' defaults to 'shared', also when omitted. 'tab' is an explicit opt-in for isolated journeys. The centrally hosted window.AditusShop.mount(), custom element element.config and framework/source-package mount() accept the same option; plain HTML uses cart-scope="tab" or cart-scope="shared". Choose before mounting. The scope is immutable for the mounted checkout instance: handle.update() rejects a change, rather than migrating a live cart. Keep it unchanged in full replacement configs.
public-keyYour publishable key (domain whitelist applies). Without session-token the element creates its own browser session. Sites without a backend use this mode and must also set cart-scope="tab"; with the default "shared" the browser session ends in an explicit error because there is no trusted server identity.
session-tokenA session token your backend created with the mint secret (see below). If set, the element never creates a session itself, and your page owns the session lifecycle.
api-baseBase URL of the ADITUS API. Defaults to the origin the bundle script was loaded from. Usually you never set this.
cultureShop culture/locale forwarded to ADITUS resource selection; valid values such as it-IT are passed through. Voucher and upsell resource text stays as supplied by ADITUS; other interface text uses German/English resources and fallback. This does not imply full Italian UI. Changing it re-localizes in place without resetting the journey.
article-layout / article-group-layout / event-layout / article-select-mode / show-headerThe flat presentation options, same values as the mount() config (cards, grid, quantity, …). The entry point (event picker vs. a pinned event) is not an attribute: it is fixed server-side when the session is created.
include-shop-assortmentsOptional assortment mixing; includeShopAssortments in mount(). Default false: load only the default assortment. Set true to also load non-default assortments with showInShop=true. An explicit start assortment always wins. If omitted, the stored client default applies; explicit false overrides it.
survey-columnsAnswer-option columns (1–4) for RadioButtonList/CheckBoxList survey questions. This corresponds to surveyColumns in the mount() config. Unset keeps the single-column default (or the client default stored server-side); narrow viewports collapse back to one column.
hide-house-numberHides the house-number field of the survey address block in every country variant; this corresponds to hideHouseNumber in the mount() config. Street and number are then entered together in one field labelled "Street and house number", which takes over the freed width; address suggestions fill street and number into it in the country's usual order (e.g. "12 Rue Cler" in France), a prefilled house number is cleared, and the separate house-number answer is always submitted as an invisible zero-width space (U+200B), without a validation error even if the survey marks it as required. The billing address form follows. Default off; if omitted, the stored client default applies, explicit "false" overrides it.
show-jump-buttonJump button in the personalisation step; this corresponds to showJumpButton in the mount() config. On by default: after a failed submit a small pill sticks to the bottom of the widget, shows how many entries are still open and jumps to the next one; once everything is filled in it leads back to the button, without ever submitting. It hides while that button or its target is visible and while the shopper is typing. Set "false" to switch it off; if omitted, the stored client default applies.
voucher-code-paramName of a query parameter in your page URL that carries a voucher/ticket code (e.g. "code" for links like …/tickets?code=ED24). This corresponds to voucherCodeParam in the mount() config. The element reads it once at mount, removes it from the address bar and redeems the code automatically (voucher form or first cart). Only for sessions pinned to an event.
base-stylesSet to "false" to drop the neutral default skin and style every hook yourself.
autoScroll / articleIconVisible / classPrefixNo attribute: set these options through element.config (same names as in the mount() config). autoScroll (default true) scrolls the shop start back into view after a step change; set false if your page manages scrolling. articleIconVisible (default false) shows an image or monogram per article in the list layout. classPrefix (default "aditus-shop") changes the prefix of every CSS hook.
Session & self-healing
An effective sessionToken (from config or attributes) disables browser session creation. With an effective publicKey but no token, the element still creates its browser session. A refreshSession provided by your site always takes precedence over built-in refresh. Without it, sessions the element created itself use the shared browser helper for refresh and proactive renewal. Light DOM remains unchanged: no iframe or shadow DOM.
Events & versioning
DOM CustomEvents bubble from the element with payload in event.detail: cart:update, phase:change, checkout:complete and analytics, with the same names/payloads as handle.on(). Config callbacks are available too.
Resetting the journey deliberately
element.clearJourney() and window.AditusShop.clearJourney() remove only the local resume pointer for this page. Call before re-adding the element or remounting to start fresh. They do not delete server carts or revoke sessions; logout revocation belongs on your backend.
Updates without remounting
mount/update configs are full replacements, not patches: keep your complete config and pass { ...config, culture } to handle.update(). element.config likewise replaces all property overrides; changing an unmasked attribute still updates in place. Presentation changes preserve the journey. Session identity changes re-establish the session. handle() unmounts; removing the element cleans up its embed and renewal listeners. Remove any DOM listeners you registered on a retained element yourself.
SSR apps: Next.js & Nuxt
The micro-frontend is deliberately client-only. Its content is live and session-bound: availability, prices and the user's cart exist only for an authenticated session at request time, so there is nothing meaningful your server could pre-render. mount() creates a fresh client-side React root (createRoot); server-rendered markup inside the container is not hydrated. That makes SSR integration simple and predictable: render a placeholder on the server, mount on the client, and reserve the space so nothing jumps.
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 }} />;
}
Avoiding layout shift (CLS)
Give the mount container a min-height that roughly matches the first shop view, and optionally render your own skeleton inside it. Server and client produce the same placeholder, so there is no hydration mismatch. The adapter renders immediately after mount and replaces the placeholder in one paint. Do not attempt hydrateRoot on the container: there is no server-rendered shop tree to attach to.
The session in an SSR app
SSR apps have a natural advantage: the server runtime that renders the page can also create the session server-to-server with the secret, exactly like the session flow below (Next.js: Route Handler or Server Component; Nuxt: server route). Hand the resulting opaque token to the client component as a prop or payload. The secret never appears in client code, and the token stays in memory.
Next.js specifics
Mark the mounting component "use client" and mount in useEffect (see snippet). With the Pages Router, next/dynamic with ssr: false achieves the same. Return the handle from the effect as cleanup so client-side route changes unmount cleanly. A culture switch on the running instance goes through handle.update() without resetting the journey; a new session token remounts (as in the snippet). The cart survives that too, because the micro-frontend restores the journey on mount.
Nuxt / Vue specifics
Wrap the mount target in <ClientOnly> or mount in onMounted. The adapter needs a real DOM element in the browser. Remember the bundle requirement: react and react-dom 18+ must be installed as dependencies of your Nuxt app; the shop renders into its own container and does not interfere with Vue's virtual DOM.
Cart scope, trust and storage
cartScope decides whether a visitor's tabs share one cart. The default is 'shared'; 'tab' deliberately keeps tabs apart. Choose it before mounting; it cannot change afterwards.
Aspect'shared' (default)'tab'
CartOne cart for all tabs with the same origin, publicKey, identity and eventEach tab has its own cart
SessionCreated by your backend: identified user, or anonymous with sharedCartKey from your own server session. Never take identity or key from the browser.Any session, including one created in the browser
Several tabsOne tab edits; the others are read-only and offer “Continue editing here”. A tab that is opened or reloaded takes over automatically, never during payment.Independent
If not metExplicit error, even when cartScope is omitted; no silent switch to 'tab'. Pages without a backend therefore set 'tab'.–
Configuration: cartScope in mount() and element.config, cart-scope in HTML. handle.update() rejects a change, so keep the value in every replacement config. Attributes · Session fields
Storage: Form drafts live per tab in sessionStorage and expire 2 hours after the last edit. Tokens never go into localStorage. Confirmed orders and registrations are not affected.
Logout: On logout or user change, your backend revokes the old session; then update the token or unmount the embed and call clearJourney(). A new user never inherits the previous user's cart.
Security: The embed renders directly into your page, without an iframe. Scripts on your page, including third-party ones, can access it, so only include scripts you trust. Payment data is entered only at the payment provider.
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();
The micro-frontend needs a session
No token
No session token: the micro-frontend stays inactive
Without a session token the micro-frontend renders a neutral notice and starts no API calls at all. No cart can be filled, no registration, no checkout. A session is mandatory before the shop activates, but it does not have to be tied to a known user: a guest session is enough to browse and fill the cart, and the real user can be established later at checkout (see the optional-user flow below).
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.
});
User of your site
Session token for a specific user
Your backend issues a short-lived token tied to one user. The micro-frontend then acts as that user: their cart, their data. The token is opaque, expiring, single-user and bound to your publicKey, and your secret never reaches the 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.
On reload, a valid journey pointer lets the embed request the live server cart under the same authorized scope and trusted identity. It is not authorization and does not guarantee recovery after session/cart expiry, storage clearing or user change. Explicit tab scope does not merge other tabs' journeys; default shared scope still enforces the single-writer checkout lock.
Payment return uses aditus-return (an opaque, short-lived, server-bound return token) plus aditus-pay. It can return in the same or a new tab, but the server requires an authorized client/origin and the matching trusted identity. Return query parameters are not proof of purchase. The server resolves payment state and finalizes the token-bound attempt; an unknown outcome stays blocked for reconciliation, never blindly retried as a new purchase. Expired or unauthorized returns are explicit errors, not permission to create a replacement order.
Assigning a payment provider only prepares payment: the summary remains editable. The lock starts at the actual redirect, after required consents. A cancel/error return query is not proof of failure: the backend rechecks the cart; Completed with order.id reconciles completion. Retry is allowed only after confirmed successful provider-assignment removal for that same cart; ambiguous outcomes stay blocked and can be rechecked. This does not guarantee a refund or rule out late settlement.
Redirect payment: what your page must do for the session
Serve the configured return URL with the embed mounted. Preserve aditus-return and aditus-pay through routing or required login redirects; never treat aditus-pay=success as authorization. Supply a valid session for the same trusted identity and publicKey. Keep the selected cartScope, and do not clear the journey or start a second checkout to work around a pending, expired or uncertain return.
Want a deliberate fresh start with a real "start over" button? The package exports clearJourney(): call it before mounting (or re-mounting) and the locally persisted journey record for the current page is dropped, so the next mount starts fresh at the entry point. Only the local resume pointer is removed; any server-side cart state is untouched.
Entry point: event home, articles or voucher
Where the journey starts is decided by your server when it creates the session, never by the browser. Without eventSlug the micro-frontend opens on the event overview. With eventSlug choose the article list (default), the event home page or the voucher form using startMode; assortment restricts the article list.
Article selection and voucher continuation follow the original shop's recommendation-based route. Without a usable upsell offer or cross-selling, go directly to ticket personalisation through the normal login/session gates; with cross-selling, show the cart. A usable upsell offer opens its dialog first. Skip goes to personalisation without cross-selling, or to the cart with it. A confirmed upgrade goes to the cart; closing the dialog leaves the selection in place. Routing never writes registration in the background.
Shop-visible assortments can have direct entry actions. If the session pins one assortment, home shows only that assortment's ticket action, not the full list or the voucher action.
startMode: "home" opens the event home page. Visitors choose Buy tickets or Redeem voucher there; no cart is created and no code redeemed merely by opening the page. Buy tickets enters the article list (limited by assortment if set); Redeem voucher opens the code form. Without eventSlug, the event overview appears first and the chosen event then opens its home page. A prepared voucher cart or resumable journey still takes priority over a fresh start.
Default
Article start
eventSlug alone: the micro-frontend opens the article selection of that event with all assortments shown in the shop.
assortment
One assortment only
assortment restricts the article start to one assortment of the event: pass its id or exact name (case-insensitive). Hidden assortments are allowed, which makes it the way to expose e.g. free press tickets on a dedicated page. A selector that matches nothing shows an explicit error instead of silently listing everything.
startMode: "voucher"
Redeem voucher / register ticket
The journey opens on a code form instead of the article list. A valid code reserves its ticket. After successful redemption, the event setting configuration.redirectAfterRegistration in ADITUS controls whether the shopper stays on the success screen or is automatically forwarded. Continuing follows the same recommendation-based routing as article selection, through the normal login/session gates before personalisation. Back preserves the reservation. "Add more vouchers" reopens code input on the voucher screen for the same cart; "Select tickets" opens article selection. A rejected code stays on the form with the reason. A fully covered ticket skips payment.
A promotion code switches to cart-priced article selection and lists every changed price. The notice now stays in the footer beside the still-usable code field: shoppers can apply more promotion codes to the same cart, redeem a voucher together with their quantities, and remove active promotion codes one by one. Later selection rounds reapply every remaining promotion before articles are added.
Where to set it
Server session or client record
Sessions created server-to-server pass startMode / assortment in the body next to eventSlug. Sessions created in the browser (publishable key only) take all three from the client record your ADITUS contact configures; a body value is ignored there. Only assortment requires eventSlug (400 start_option_requires_event_slug); startMode also works with the event picker; unknown values give 400 invalid_start_mode / invalid_assortment. GET /shop/session-context echoes the resolved startMode and assortment.
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 */
Voucher links: redeem a code from the URL
Vouchers are often sent out as links to your page with the code as a query parameter, e.g. …/tickets?code=ED24-ADITUS. The micro-frontend can take that code over and redeem it without the visitor typing anything. Two handover paths exist; both end in the same redemption call and behave identically afterwards.
voucherCodeParam: "code"
A) In the browser: the micro-frontend reads the URL
Name the parameter in the mount() config (voucherCodeParam) or on the element (voucher-code-param). At mount the adapter reads it once, removes it from the address bar via history.replaceState (no reload, everything else in the URL stays) and redeems the code. A reload, bookmark or forwarded URL therefore never redeems twice. Right for static pages and the session created in the browser.
voucherCode in the session request body
B) On the server: hand the code over with the session
Your backend reads the parameter from the incoming request and puts it into the session request body as voucherCode (eventSlug optional). The code becomes part of the session and reaches the micro-frontend only through the authenticated GET /shop/session-context, which hands it out only once and clears it; the session response echoes the accepted code for verification. Set voucherCodeParam as well so the parameter also disappears from the address bar. Right when your page is server-rendered anyway, or when you want to validate, log or rate-limit incoming links yourself. If both paths deliver a code, the server-side one wins.
What happens with the code
Same behaviour on both paths
The code is redeemed exactly once, and only on a fresh journey (a resumed journey after reload or payment return ignores it). With startMode "voucher", the code form behaves as described under "Redeem voucher / register ticket". With the articles start, the code is applied to the first cart while the article view, scroll position and quantities stay in place. Continue adds those quantities to that same cart. The micro-frontend never passes the code to onEvent. Without eventSlug it is redeemed once the visitor has picked an event.
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 });
});
Optional: reserve the voucher during server-side session creation
If your site wants to show voucher rejection before opening the embed, set redeemVoucherOnMint: true on the trusted POST /api/shop/session. Unlike ordinary voucherCode handover this creates a cart for the authenticated user and actually reserves the voucher there before session creation succeeds; it is not an extra preliminary validation request. On success, pass only the returned sessionToken to the browser. The embed resumes that same cart without creating another or redeeming twice, whether startMode is voucher or articles. Without the flag, the normal voucherCode handover applies.
This mode requires voucherCode, a pinned eventSlug, an identified user (email; not anonymous) and the Idempotency-Key header; it is only available with a mint secret on your backend, not POST /shop/embed-session in the browser. It supports voucher/ticket codes, not PromotionCode. The code is reserved in a cart, not yet registered or issued as a ticket: the shopper must still complete registration and checkout. Further checks by the shop API may still reject later steps; a successful session creation does not guarantee final issuance.
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})`);
}
Treat 422 voucher_rejected with its sanitized reason as a business rejection on your site: do not mount an embed without a token. A malformed configuration/key returns 400; 409 idempotency_conflict means the same key was used for different inputs, while a pending/uncertain operation must not be mistaken for an invalid voucher; 502/503 and network timeouts are infrastructure/unknown outcomes, not voucher rejections. Persist a new random Idempotency-Key per logical attempt on your server and retry only with the same key and identical inputs. Never retry with a new key after an uncertain outcome: the reservation may already exist. Avoid logging code, email, raw API errors or session tokens.
A successful session creation can leave a reserved cart even if the shopper never opens the page. Known-cart cleanup on rejection/revocation is best effort; a boot/60-second sweeper retries up to 25 records per tick. A valid completed attempt with the same key and input replays its result. If the original token is expired, revoked, completed, deleted or transferred, it returns 410 voucher_preparation_expired instead; do not blindly recreate the reservation. An authorized successor may extend or transfer the preparation when it first accesses the cart. Removing the code from your page URL does not release a reservation.
409 idempotency_conflict means changed inputs; voucher_preparation_pending includes Retry-After: 2 seconds, and voucher_preparation_uncertain remains uncertain on replay (not an automatic eventual success). 410 voucher_preparation_expired means the original preparation cannot be replayed. 502/503 voucher_upstream_error means upstream failure. If upstream created a cart but never returned its ID, automatic reconciliation or release is impossible with the available API: the uncertain record is retained and the server will not create another cart on retry. Arrange manual clarification with ADITUS before beginning a new attempt. If session-context cannot load, the embed shows an error/retry rather than silently starting a fresh cart.
Try all three live in the Integration Tester: create a session with eventSlug, pick startMode or an assortment, mount, and watch the request log. The demo system accepts the test voucher ED24-ADITUS on ExperienceDaysv52024 (each redemption reserves the ticket for about 20 minutes; backing out releases it). Open the Integration Tester
Optional: browse anonymously, sign in at checkout
You do not have to know who the user is up front. Mount with a guest session and the micro-frontend lets an anonymous visitor browse events, pick articles and fill the cart. Only when they move past the cart, toward registration and checkout, does it ask you to establish the real user, via the onUserRequired callback. The rest of the flow then continues with the cart preserved.
1
Mount with a guest session
A session token is still required to activate the shop, but it can be a guest session: simply create it without the email field, no placeholder address needed. The visitor browses and fills the cart under it.
2
Leaving the cart triggers onUserRequired
The moment the visitor advances past the cart, the micro-frontend calls your callback once and shows a neutral pending state. Authenticate the user (login/SSO), then create a fresh, user-bound session the exact same way as the first one: server-to-server with your secret. The later user is established via that server-to-server session creation too. The secret never reaches the browser and the user is never set in client code. Your callback only relays the resulting opaque token. Crucially, your backend derives that identity from its own authenticated session (cookie/JWT); it must never accept the email or user id from the browser, or a malicious visitor could request someone else's session and read their personal data via the prefill.
3
Return the new token. The cart carries over
Return the fresh session token and the micro-frontend swaps it in and binds the identified user to the existing cart. The cart, its articles and any timeslots survive unchanged. Then it enters registration. Return null to abort. The visitor simply stays on the cart, no error.
4
Omit onUserRequired for a fixed user
If you leave the callback out, nothing changes from the classic flow: mount with a user-bound session and the micro-frontend acts as that user from the very first step.
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);
Authentication happens on your site
ADITUS does not authenticate your end users. Your site is the single source of truth for who the user is. You run your own login, SSO or session. ADITUS only trusts a session token that your backend issues with a secret key. The trust chain is: your auth proves identity, your secret-issued token vouches for it, the micro-frontend presents it.
1
Your site authenticates the user
Login, SSO and member sessions are entirely your own. ADITUS is not involved here.
2
Your backend creates a session
Server-to-server with the secret key. You pass who the micro-frontend should act as (email) and bind it to your publicKey.
3
The page mounts the micro-frontend
The opaque token is handed to the browser and passed as sessionToken to mount(). The secret stays on your server.
4
Every call carries the session
The micro-frontend sends X-Aditus-Session on each request; the shop API resolves the cart's user from the session.
5
No token → micro-frontend stays inactive
Without a session the micro-frontend never activates: it shows a neutral notice and makes no calls. Neither the cart nor checkout is available. Create a session to switch it on.
1. Create the session on your 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. Mount with the 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. Revoke on logout
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 }
The finished order returns to your page
When a ticket purchase completes, you do not need a separate order lookup. Because the micro-frontend runs natively in your page (light DOM, not an iframe), it hands the finished order straight back to your code via the onComplete callback. This is a direct JS call, not postMessage. The micro-frontend has already resolved the order for you, so you receive the order number and the ticket links, not just an 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);
},
});
What you receive
onComplete fires once per successful checkout with the resolved order: id (the order GUID, your key for further processing), number (the order number for display), status, buyer (name & email when present) and tickets. Each ticket carries typed links with kind “pdf”, “apple” or “google”. onError fires on any unrecoverable error in the flow.
Redirect payment methods
Methods that stay on the page (invoice, prepayment) fire onComplete right away. A method that sends the browser to an external payment page (e.g. Saferpay) is handled by the Micro-Frontend end to end: it derives the return URLs from the page it runs on, sends the user out, and on the way back it finalizes the order and fires onComplete. This full round-trip requires no code on your side. Only if the return must land on a different page do you override redirectUrls.successUrl / cancelUrl / errorUrl.
GA4 / e-commerce events (optional)
The micro-frontend can report the shopping funnel to your analytics, but only if you ask it to. It bundles no gtag, no GTM and no GA SDK, loads nothing, sets no cookies and sends nothing on its own. You add one optional callback, onEvent, and receive typed events already shaped in Google's GA4 Enhanced-Ecommerce schema. Omit onEvent and nothing changes. Which tool you feed, and whether you have consent to send, stays entirely on your side.
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);
},
});
Already in the GA4 shape
Each event carries a GA4 event name plus its standard params: items (with item_id, item_name, price, quantity, item_category, and (where it applies) discount and coupon), value and currency; add_payment_info adds payment_type and purchase adds transaction_id. Destructure name and pass the rest straight to gtag("event", name, params). No re-mapping is needed.
Consent stays yours
onEvent is just a normal JavaScript function in your page that we call directly. The Micro-Frontend itself sends no analytics anywhere. Nothing leaves the page until your handler sends it, so your consent management stays in full control: gate the forward on consent, route to GTM's dataLayer instead of gtag, batch, or drop events entirely. Any error your handler throws is caught and isolated, so it never breaks the user flow. Keep the handler light (a gtag/dataLayer push) since it runs inline.
EventFires when
view_item_listThe user sees the article list (fires once per shown set).
add_to_cartAn article or add-on is added to the cart.
remove_from_cartA cart line or add-on is removed.
view_cartThe cart view is shown (once per cart).
begin_checkoutThe user leaves the cart towards registration / checkout.
add_payment_infoA payment method is chosen (carries payment_type).
purchaseThe order is placed (carries transaction_id, value, items).
Event bus: handle.on / handle.off (optional)
Besides the config callbacks, the handle returned by mount() carries a small, fully optional event bus. Subscribe with handle.on(event, listener) (it returns the matching unsubscribe function) and detach with handle.off(event, listener). If you never call on(), nothing changes: the bus adds no dependencies, sends nothing and costs nothing. It is pure observation for your page (a mini-cart badge, a confirmation banner, your own logging) and it complements the callbacks rather than replacing them.
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();
EventFires when
cart:updateThe cart changes: created, item added or removed, or emptied. Carries cartId, itemCount, value, currency and the lines as GA4-shaped items.
phase:changeThe visible step changed. The event carries phase (events · home · articles · voucher · cart · registration · legitimation · buyer · payment · summary · done, same vocabulary as data-step), previous (null for the entry step) and startPhase, the session's entry step. Lets your page drive its own Back / Restart buttons without reading CSS hooks.
checkout:completeThe order is placed. Fires right after config.onComplete with the same resolved order object, before the Micro-Frontend shows its completion page. Call preventDefault() synchronously to show your own completion page instead (see “Your own completion page”).
analyticsMirror of the GA4 funnel above: every onEvent payload is also emitted on the bus, unchanged.
Listeners survive update()
Subscriptions live on the handle, not on a render: handle.update({ ... }) re-renders the Micro-Frontend but keeps every listener attached. Unmounting via handle() detaches all listeners automatically. You only need off() (or the returned unsubscribe) when you want to stop listening while the shop keeps running.
Isolated, never blocking
A listener that throws is caught and isolated. It never breaks the user flow and never affects other listeners. The “analytics” event mirrors the GA4 funnel with payloads identical to config.onEvent, so you can consume the funnel via the bus, the callback, or both; the same consent rule applies: nothing leaves the page until your code sends it.
Your own completion page
checkout:complete fires before the Micro-Frontend shows its own completion page. If your page shows its own confirmation, call preventDefault() synchronously in the listener: event.preventDefault() on the <aditus-shop> element, preventDefault() from the payload with handle.on(), or completion.preventDefault() from the second argument of config.onComplete. The Micro-Frontend then shows no completion page, does not scroll and renders no further step; your page takes over. The order is placed either way. The call must happen in the listener itself, not after an await. Without it, the Micro-Frontend shows its completion page as before. This also applies to the return from an external payment page.
End the session afterwards. The order is complete, so your server can revoke the session in the same step: DELETE /api/shop/session/:token with your secret key (see “Revoke a session”). The placed order and the ticket links in the order object stay valid; the links do not depend on the session. For a further purchase, create a new session and mount the Micro-Frontend again. The Micro-Frontend does not end the session itself, because revoking requires your secret key. Keep the session only if your page deliberately continues selling in it.
Commands: goTo and back
Besides observing, your page can send two narrow requests to the Micro Frontend. Both return true when the shop switched and false otherwise; they never remount and never lose the journey. goTo("cart") shows the cart step for your own mini-cart; it is accepted only on the registration step while nothing has been written yet. back() goes one step back, exactly like the shop's own back button. It answers false wherever that button is missing or disabled: on the entry step (startPhase), on the completion page and while a payment or another change is running. Both exist on the mount handle and on <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" });
}
The Micro Frontend never writes browser history itself: your site owns its URLs and its router. The example keeps exactly one extra history entry while the shopper is past the entry step, so the browser's back gesture (also the swipe on iPhone) first steps back inside the shop and only leaves the page from the entry step. If your site uses a client-side router (React Router, Next.js, Vue Router), create that entry through the router instead of history.pushState so the router does not treat it as a foreign navigation.
publicKey vs. sessionToken
Keep the two separate. They answer different questions and are paired, not interchangeable.
publicKey: origin trust
Publishable. It ships in your client code, so it is not a secret and grants nothing on its own. The backend enforces a per-key domain whitelist: a key only works from its registered domains. It answers “which site is this?” Only an explicit, deliberately unsafe per-client option set by the operator can lift this whitelist for one client; the active flag, session requirement and secrets still apply then.
sessionToken: identity
Created with the secret on your backend, opaque and short-lived. It answers “who is the user?” A session is bound to the publicKey it was created for: a session request must also send that key, and a mismatch is rejected (403).
Security rules
The secret key lives only on your server, never in client code, bundles or env files shipped to the browser.
Do not store the token in localStorage or cookies yourself; keep it in memory and renew it by creating a new session. For reloads and the payment round trip, the micro-frontend itself keeps it in the tab's sessionStorage.
Tokens are short-lived (default 20 minutes, max 24 hours) and single-user.
Revoke the session on logout: a revoked token is refused immediately (401). Until it expires or is revoked, a valid token is a bearer token. Treat it like one.
Always pair the session with its publicKey. A missing or mismatched key is rejected (403).
An invalid or expired session returns 401. There is no fallback to another user.
What a session token can do
A session token is a bearer token. This is the exact scope the ADITUS API enforces for it. Use this information for your threat model and security review.
Token & lifetime
256-bit random, opaque, carries no data and cannot be decoded. Default 20 minutes, configurable from 1 minute to 24 hours when the session is created. A session is never extended. Renew it by creating a new session.
Binding
Bound to the publicKey it was created for. Every session request must send exactly that key, and the request origin is checked against the key's domain whitelist. A different key or a foreign origin is rejected (403).
Anonymous session
Created without a user: may browse events and create and edit its own cart. Registration, timeslots, payment assignment and checkout are refused server-side (403) until your page swaps in an identified session.
Identified session
Additionally: registration, timeslots, payment assignment and checkout, always limited to the carts this session owns. With protectUserData the buyer data stored for the user is protected: a write that would change it is refused (403).
Cart ownership
A cart is bound to its owner via a keyed hash (HMAC): for an anonymous session the token itself, for an identified session the user identity your backend set when creating the session, scoped to the publicKey. Sessions of other users get no access to it, and writes to one cart are serialized.
Personal data
The session context returns flags, ids and the user identifier your own backend set when creating the session. Your site already knows all of this. Buyer data is reachable only through the ownership-checked cart and registration routes.
Rate limits
Defaults: create and revoke 300 per minute per client and IP, address, company and city autocomplete 60 per minute per session and 3000 per minute per client across all sessions. Every limit is a per-client budget, so other clients never affect yours. ADITUS can set individual limits per client on request.
Audit log
Shop API requests are recorded and retained for 90 days: client, normalized route, status, origin host, a shortened hash of the session token, and cart or order id. Never the token itself, no e-mail, no IP address.
Revocation
DELETE /api/shop/session/:token invalidates the session immediately. There is no cache in between; the very next request with that token gets a 401. Call it on logout.
Endpoint & response reference
POST/api/shop/sessioncreate with Bearer secret
DELETE/api/shop/session/:tokenrevoke with Bearer secret
Both calls come from your backend: it authenticates with your client's mint secret (generated in the ADITUS admin console) as a Bearer token. One session per login is the normal pattern; the browser only ever sees the resulting token. Sites without a backend do not use these endpoints: the element creates its browser session itself via the domain-restricted POST /api/shop/embed-session (public-key with cart-scope="tab").
You can try this exact flow with your own credentials in the integration tester. Create a session, mount the shop and walk through the journey.
Create a session: request
HeaderValueMeaning
AuthorizationBearer <mint-secret>Your client's mint secret, generated in the ADITUS admin console. Server-side only. It must never be shipped to a browser or app bundle.
Content-Typeapplication/jsonThe body is a JSON object.
Idempotency-Keystring · 8–200 visible ASCII chars · required with redeemVoucherOnMintRequired when redeemVoucherOnMint is true: 8–200 visible ASCII characters. A stable key for one logical voucher attempt; store it on your backend and reuse it with the exact same request on retries. A different request with the same key returns 409 idempotency_conflict. Never send the mint secret or this key from the browser.
Body fieldTypeMeaning
sharedCartKeystring · optionalIdentified sessions created server-to-server normally share through the trusted identity. Anonymous shared carts REQUIRE your backend to create the session with sharedCartKey: an opaque, stable visitor reference derived from your own trusted visitor session, not an email. This is a server-only session field, not browser config or an HTML attribute. Never accept an arbitrary email, identity or sharedCartKey from the browser. A browser-created session alone cannot establish safe sharing and shared scope without a trusted identity returns an explicit unsupported error, including when scope is omitted. Explicit 'tab' isolation does not require sharedCartKey; there is no silent fallback.
publicKeystring · requiredThe publishable key the session is created for. It must be the same key the micro-frontend mounts with. Every later shop call is checked against it (403 on mismatch). The Bearer secret must belong to this key's client.
emailstring · optionalThe shop user the session acts as. Take it from your authenticated server session (login/SSO cookie or JWT), never from the browser request, or a visitor could obtain someone else's session. Omit it to create an anonymous session: the visitor can browse and fill the cart, but the micro-frontend blocks the step past the cart until your onUserRequired callback supplies a user-bound session (see the optional-user flow). A present but malformed email is still rejected (400).
externalUserIdstring · optionalYour stable user reference from your authenticated server session. Stored with the session and used for identity scoping when present; do not change it on token renewal or accept it from arbitrary browser input. It is not authentication by itself.
ttlSecondsnumber · optionalSession lifetime in seconds. Default 20 minutes, capped at 24 hours. When it expires, shop calls return 401 session_expired. Then issue a fresh token; with refreshSession in the config, the micro-frontend requests it automatically.
eventSlugstring · optionalFixes the journey's entry point server-side: set, the micro-frontend starts directly on this event's article selection; omitted, it starts on the event overview. Because it is part of the session created by your server, the browser cannot manipulate it. Invalid characters yield 400 invalid_event_slug; session creation fails closed for a slug that matches no live event (404 event_not_found) or an event that is closed / not on sale (409 event_not_sellable). No session is issued and there is no fallback to the event overview.
startMode"articles" Entry view of the journey. "articles" (default) opens article selection; "home" opens the event start page with its entry actions (tickets, voucher, released assortments); "voucher" opens the voucher/ticket-code form first. The voucher form behaves as described under "Redeem voucher / register ticket". Without eventSlug the event picker comes first and the mode applies to the event the shopper picks.
assortmentstring · optionalRestricts the article start to one assortment of the pinned event (requires eventSlug; ignored for startMode "voucher"). Give the assortment id or its exact name (case-insensitive); hidden assortments are allowed. A selector that matches nothing shows an explicit error in the micro-frontend instead of silently falling back to the full list. Max. 200 characters, else 400 invalid_assortment.
voucherCodestring · optionalA voucher/ticket code your server hands over with the session (both start modes). By default the embed redeems it on a fresh journey, after event selection if unpinned; rejection is shown in the form or first-cart notice. With redeemVoucherOnMint: true it is instead reserved server-side before a successful session response, for a pinned event and identified user. Format: 1–100 characters without whitespace or URL delimiters, else 400 invalid_voucher_code.
redeemVoucherOnMintboolean · optional (default false)Set true only when your server creates the session with voucherCode, a pinned eventSlug, an identified email and Idempotency-Key. This creates a user-bound cart and really reserves the voucher before issuing a session. The embed adopts that cart without creating another one or redeeming again; both start modes work. Only voucher/ticket codes are supported, not PromotionCode. Without it, the normal voucherCode handover applies. Registration and final checkout still follow.
Create a session: response (200)
FieldTypeMeaning
sessionTokenstringOpaque token (sess_…). Hand it to the browser and pass it to mount() as sessionToken; the micro-frontend sends it as X-Aditus-Session on every shop call. It contains no user data and cannot be decoded.
expiresAtnumberExpiry as a Unix timestamp in milliseconds. Purely informational for your own scheduling. The micro-frontend reacts to the 401 on its own.
voucherRedeemedboolean · only with redeemVoucherOnMinttrue when the optional session creation with a prepared voucher succeeds. The returned session already owns a cart with a voucher reservation; the embed must reuse it. This is not a completed ticket registration.
Revoke a session
DELETE /api/shop/session/:token with the same Bearer secret. The secret must belong to the client the session was created for. Call it on logout or a kiosk reset so the token dies with your own session. For anonymous (token-scoped) sessions the call also deletes the session's carts in the Shop API. Coupon reservations held by those carts are released immediately, not after the cart expiry. Response: { "revoked": true, "carts": { "scope": "token", "deleted": 1, "failed": 0 } } (revoked false if the session had already expired; scope "identity" for user-bound sessions, whose carts outlive a single token and stay untouched). Revoking is idempotent and safe to fire-and-forget.
Server-to-server reads
A backend that drives the micro-frontend (a kiosk shell, an onsite shop) usually needs tenant facts before it creates a session: which events exist and are on sale, which languages the shop speaks, which lookup lists and branding apply. Four read endpoints answer that with the same credentials as session creation (Bearer mint secret + X-Aditus-Public-Key header, same rate limit, Cache-Control: no-store). They are whitelisted projections of the Shop API, never a raw passthrough; ?language= (or Accept-Language, default de) picks the language of names. Errors: 400 invalid_public_key, 401 unauthorized, 503 shop_api_not_configured, 502 upstream_error.
EndpointReturns
GET /api/shop/events?language=deEvery event of the tenant: id, slug, name, year, start/end, location, saleState (open · registration_only · closed), the raw flags, showInOverview, languages, icon/logo URLs. Pick the slug for the session from here.
GET /api/shop/configurationTenant facts: mandator, the lookup list names (salutations, countries, titles, cookie consent) and websiteMetricTracking. Login-provider credentials of the tenant are deliberately never included.
GET /api/shop/languagessystemLanguage plus the list of shop languages ({ code, name }). These are the values the micro-frontend's culture attribute accepts.
GET /api/shop/branding?images=ONSITE_SHOP_LOGO,…The tenant's Shop theme: styles as a flat key → value map, the theme images with URLs, and (for up to 40 image keys you name (A–Z, 0–9, _ , -)) whether each key exists, its public URL and content type. Image URLs need no auth and can be used directly in your shell. Add &eventSlug=<slug> or &event=<GUID> to scope theme and keys to one event (event-level overrides, event-only keys such as ONSITE_SHOP_HEADER / ONSITE_SHOP_ATTRACT_*); the answer then names the resolved event. Unknown slug → 404 event_not_found, non-GUID → 400 invalid_event, Shop API down → 503 event_unavailable.
Errors are explicit
StatusCodeMeaning
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.
Now make it yours
Identity is set. Next, brand the micro-frontend. The styling tool generates a ready-to-paste theme config and explains every parameter, with a live preview.